ndhc/src/state.c

692 lines
24 KiB
C
Raw Normal View History

/* state.c - high level DHCP state machine
*
2015-02-13 12:24:57 +05:30
* Copyright (c) 2011-2015 Nicholas J. Kain <njkain at gmail dot com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
2014-03-31 02:32:48 +05:30
#include "nk/log.h"
#include "nk/random.h"
#include "state.h"
#include "ifchange.h"
#include "arp.h"
#include "options.h"
#include "ndhc.h"
#include "sys.h"
#include "netlink.h"
#include "coroutine.h"
#define SEL_SUCCESS 0
#define SEL_FAIL -1
#define REQ_SUCCESS 0
#define REQ_TIMEOUT -1
#define REQ_FAIL -2
#define ANP_SUCCESS 0
#define ANP_IGNORE -1
#define ANP_REJECTED -2
#define ANP_CHECK_IP -3
#define ANP_FAIL -4
#define BTO_WAIT 0
#define BTO_EXPIRED -1
#define BTO_HARDFAIL -2
#define IFUP_REVALIDATE 0
#define IFUP_NEWLEASE 1
#define IFUP_FAIL -1
static int delay_timeout(struct client_state_t cs[static 1], size_t numpackets)
{
int to = 64;
char tot[] = { 4, 8, 16, 32, 64 };
if (numpackets < sizeof tot)
to = tot[numpackets];
// Distribution is a bit biased but it doesn't really matter.
return to * 1000 + (nk_random_u32(&cs->rnd32_state) & 0x7fffffffu) % 1000;
}
static int reinit_shared_deconfig(struct client_state_t cs[static 1])
{
arp_close_fd(cs);
cs->clientAddr = 0;
cs->num_dhcp_requests = 0;
cs->got_router_arp = 0;
cs->got_server_arp = 0;
memset(&cs->routerArp, 0, sizeof cs->routerArp);
memset(&cs->serverArp, 0, sizeof cs->serverArp);
arp_reset_send_stats();
return 0;
}
static int reinit_selecting(struct client_state_t cs[static 1], int timeout)
{
if (reinit_shared_deconfig(cs) < 0)
return -1;
cs->dhcp_wake_ts = curms() + timeout;
start_dhcp_listen(cs);
return 0;
}
// Triggered after a DHCP lease request packet has been sent and no reply has
// been received within the response wait time. If we've not exceeded the
// maximum number of request retransmits, then send another packet and wait
// again. Otherwise, return to the DHCP initialization state.
static int requesting_timeout(struct client_state_t cs[static 1],
long long nowts)
{
if (cs->num_dhcp_requests >= 5) {
if (reinit_selecting(cs, 0) < 0)
return REQ_FAIL;
return REQ_TIMEOUT;
}
int r = send_selecting(cs);
if (r < 0) {
log_warning("%s: Failed to send a selecting request packet.",
client_config.interface);
return REQ_FAIL;
}
cs->dhcp_wake_ts = nowts + delay_timeout(cs, cs->num_dhcp_requests);
cs->num_dhcp_requests++;
return REQ_SUCCESS;
}
static bool is_renewing(struct client_state_t cs[static 1], long long nowts)
{
long long rnt = cs->leaseStartTime + cs->renewTime * 1000;
return nowts >= rnt;
}
static bool is_rebinding(struct client_state_t cs[static 1], long long nowts)
{
long long rbt = cs->leaseStartTime + cs->rebindTime * 1000;
return nowts >= rbt;
}
// Triggered when a DHCP rebind request has been sent and no reply has been
// received within the response wait time. Check to see if the lease is still
// valid, and if it is, send a broadcast DHCP renew packet. If it is not, then
// change to the SELECTING state to get a new lease.
static int rebinding_timeout(struct client_state_t cs[static 1],
long long nowts)
{
long long elt = cs->leaseStartTime + cs->lease * 1000;
if (nowts >= elt) {
log_line("%s: Lease expired. Searching for a new lease...",
client_config.interface);
if (reinit_selecting(cs, 0) < 0)
return BTO_HARDFAIL;
return BTO_EXPIRED;
}
if (elt - nowts < 30000) {
cs->dhcp_wake_ts = elt;
return BTO_WAIT;
}
int r = send_rebind(cs);
if (r < 0) {
log_warning("%s: Failed to send a rebind request packet.",
client_config.interface);
return BTO_HARDFAIL;
}
cs->dhcp_wake_ts = nowts + ((elt - nowts) / 2);
return BTO_WAIT;
}
// Triggered when a DHCP renew request has been sent and no reply has been
// received within the response wait time. This function is also directly
// called by bound_timeout() when it is time to renew a lease before it
// expires. Check to see if the lease is still valid, and if it is, send
// a unicast DHCP renew packet. If it is not, then change to the REBINDING
// state to send broadcast queries.
static int renewing_timeout(struct client_state_t cs[static 1],
long long nowts)
{
long long rbt = cs->leaseStartTime + cs->rebindTime * 1000;
if (nowts >= rbt)
return rebinding_timeout(cs, nowts);
if (rbt - nowts < 30000) {
cs->dhcp_wake_ts = rbt;
return BTO_WAIT;
}
int r = send_renew(cs);
if (r < 0) {
log_warning("%s: Failed to send a renew request packet.",
client_config.interface);
return BTO_HARDFAIL;
}
cs->dhcp_wake_ts = nowts + ((rbt - nowts) / 2);
return BTO_WAIT;
}
// Triggered when the lease has been held for a significant fraction of its
// total time, and it is time to renew the lease so that it is not lost.
static int bound_timeout(struct client_state_t cs[static 1], long long nowts)
{
long long rnt = cs->leaseStartTime + cs->renewTime * 1000;
if (nowts < rnt) {
cs->dhcp_wake_ts = rnt;
return BTO_WAIT;
}
start_dhcp_listen(cs);
return renewing_timeout(cs, nowts);
}
static int validate_serverid(struct client_state_t cs[static 1],
struct dhcpmsg packet[static 1],
const char typemsg[static 1])
{
int found;
uint32_t sid = get_option_serverid(packet, &found);
if (!found) {
log_line("%s: Received %s with no server id. Ignoring it.",
client_config.interface, typemsg);
return 0;
}
if (cs->serverAddr != sid) {
char svrbuf[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(struct in_addr){.s_addr=sid},
svrbuf, sizeof svrbuf);
log_line("%s: Received %s with an unexpected server id: %s. Ignoring it.",
client_config.interface, typemsg, svrbuf);
return 1; // XXX! HACK! Change back to 0!
}
return 1;
}
static int an_packet(struct client_state_t cs[static 1],
struct dhcpmsg packet[static 1], uint8_t msgtype,
uint32_t srcaddr, bool is_requesting)
{
(void)srcaddr;
if (msgtype == DHCPACK) {
if (!validate_serverid(cs, packet, "a DHCP ACK"))
return ANP_IGNORE;
cs->lease = get_option_leasetime(packet);
cs->leaseStartTime = curms();
if (!cs->lease) {
log_line("%s: No lease time received; assuming 1h.",
client_config.interface);
cs->lease = 60 * 60;
} else {
if (cs->lease < 60) {
log_warning("Server sent lease of <1m. Forcing lease to 1m.",
client_config.interface);
cs->lease = 60;
}
}
// Always use RFC2131 'default' values. It's not worth validating
// the remote server values, if they even exist, for sanity.
cs->renewTime = cs->lease >> 1;
cs->rebindTime = (cs->lease >> 3) * 0x7; // * 0.875
cs->dhcp_wake_ts = cs->leaseStartTime + cs->renewTime * 1000;
// Only check if we are either in the REQUESTING state, or if we
// have received a lease with a different IP than what we had before.
if (is_requesting || memcmp(&packet->yiaddr, &cs->clientAddr, 4)) {
char clibuf[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(struct in_addr){.s_addr=cs->clientAddr},
clibuf, sizeof clibuf);
log_line("%s: Accepted a firm offer for %s. Validating...",
client_config.interface, clibuf);
return ANP_CHECK_IP;
} else {
log_line("%s: Lease refreshed to %u seconds.",
client_config.interface, cs->lease);
arp_set_defense_mode(cs); // XXX: Can return error: fail to open
stop_dhcp_listen(cs);
return ANP_SUCCESS;
}
} else if (msgtype == DHCPNAK) {
if (!validate_serverid(cs, packet, "a DHCP NAK"))
return ANP_IGNORE;
log_line("%s: Our request was rejected. Searching for a new lease...",
client_config.interface);
if (reinit_selecting(cs, 3000) < 0)
return ANP_FAIL;
return ANP_REJECTED;
}
return ANP_IGNORE;
}
static int selecting_packet(struct client_state_t cs[static 1],
struct dhcpmsg packet[static 1], uint8_t msgtype,
uint32_t srcaddr)
{
if (msgtype == DHCPOFFER) {
int found;
uint32_t sid = get_option_serverid(packet, &found);
if (!found) {
log_line("%s: Invalid offer received: it didn't have a server id.",
client_config.interface);
return ANP_IGNORE;
}
char clibuf[INET_ADDRSTRLEN];
char svrbuf[INET_ADDRSTRLEN];
char srcbuf[INET_ADDRSTRLEN];
cs->serverAddr = sid;
cs->xid = packet->xid;
cs->clientAddr = packet->yiaddr;
cs->srcAddr = srcaddr;
cs->dhcp_wake_ts = curms();
cs->num_dhcp_requests = 0;
inet_ntop(AF_INET, &(struct in_addr){.s_addr=cs->clientAddr},
clibuf, sizeof clibuf);
inet_ntop(AF_INET, &(struct in_addr){.s_addr=cs->serverAddr},
svrbuf, sizeof svrbuf);
inet_ntop(AF_INET, &(struct in_addr){.s_addr=cs->srcAddr},
srcbuf, sizeof srcbuf);
log_line("%s: Received IP offer: %s from server %s via %s.",
client_config.interface, clibuf, svrbuf, srcbuf);
return ANP_SUCCESS;
}
return ANP_IGNORE;
}
// Triggered after a DHCP discover packet has been sent and no reply has
// been received within the response wait time. If we've not exceeded the
// maximum number of discover retransmits, then send another packet and wait
// again. Otherwise, background or fail.
static int selecting_timeout(struct client_state_t cs[static 1],
long long nowts)
{
if (cs->init && cs->num_dhcp_requests >= 2) {
if (client_config.background_if_no_lease) {
log_line("%s: No lease; going to background.",
client_config.interface);
cs->init = 0;
background();
} else if (client_config.abort_if_no_lease)
suicide("%s: No lease; failing.", client_config.interface);
}
if (cs->num_dhcp_requests == 0)
cs->xid = nk_random_u32(&cs->rnd32_state);
int r = send_discover(cs);
if (r < 0) {
log_warning("%s: Failed to send a discover request packet.",
client_config.interface);
return SEL_FAIL;
}
cs->dhcp_wake_ts = nowts + delay_timeout(cs, cs->num_dhcp_requests);
cs->num_dhcp_requests++;
return SEL_SUCCESS;
}
// Called for a release signal during SELECTING or REQUESTING.
static int print_release(struct client_state_t cs[static 1])
{
log_line("%s: ndhc going to sleep. Wake it by sending a SIGUSR1.",
client_config.interface);
if (reinit_shared_deconfig(cs) < 0)
return -1;
cs->dhcp_wake_ts = -1;
stop_dhcp_listen(cs);
return 0;
}
// Called for a release signal during BOUND, RENEWING, or REBINDING.
static int xmit_release(struct client_state_t cs[static 1])
{
char clibuf[INET_ADDRSTRLEN];
char svrbuf[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(struct in_addr){.s_addr=cs->clientAddr},
clibuf, sizeof clibuf);
inet_ntop(AF_INET, &(struct in_addr){.s_addr=cs->serverAddr},
svrbuf, sizeof svrbuf);
log_line("%s: Unicasting a release of %s to %s.", client_config.interface,
clibuf, svrbuf);
int r = send_release(cs);
if (r < 0) {
log_warning("%s: Failed to send a release request packet.",
client_config.interface);
return -1;
}
print_release(cs);
return 0;
}
// Called for a renewing signal during BOUND or RELEASED
static int frenew(struct client_state_t cs[static 1], bool is_bound)
{
if (is_bound) {
log_line("%s: Forcing a DHCP renew...", client_config.interface);
start_dhcp_listen(cs);
int r = send_renew(cs);
if (r < 0) {
log_warning("%s: Failed to send a renew request packet.",
client_config.interface);
return -1;
}
} else { // RELEASED
if (reinit_selecting(cs, 0) < 0)
return -1;
}
return 0;
}
// If we have a lease, check to see if our gateway is still valid via ARP.
// If it fails, state -> SELECTING.
static int ifup_action(struct client_state_t cs[static 1])
{
if (cs->routerAddr && cs->serverAddr) {
int r = arp_gw_check(cs);
if (r >= 0) {
log_line("%s: Interface is back. Revalidating lease...",
client_config.interface);
return IFUP_REVALIDATE;
} else {
log_warning("%s: arp_gw_check could not make arp socket.",
client_config.interface);
return IFUP_FAIL;
}
}
log_line("%s: Interface is back. Searching for new lease...",
client_config.interface);
return IFUP_NEWLEASE;
}
#define BAD_STATE() suicide("%s(%d): bad state", __func__, __LINE__)
// XXX: Should be re-entrant so as to handle multiple servers.
int dhcp_handle(struct client_state_t cs[static 1], long long nowts,
int sev_dhcp, struct dhcpmsg dhcp_packet[static 1],
uint8_t dhcp_msgtype, uint32_t dhcp_srcaddr, int sev_arp,
bool force_fingerprint, bool dhcp_timeout, bool arp_timeout)
{
scrBegin;
reinit:
// We're in the SELECTING state here.
for (;;) {
int ret = COR_SUCCESS;
if (sev_dhcp) {
int r = selecting_packet(cs, dhcp_packet, dhcp_msgtype,
dhcp_srcaddr);
if (r == ANP_SUCCESS) {
// Send a request packet to the answering DHCP server.
sev_dhcp = false;
goto skip_to_requesting;
}
}
if (dhcp_timeout) {
int r = selecting_timeout(cs, nowts);
if (r == SEL_SUCCESS) {
} else if (r == SEL_FAIL) {
ret = COR_ERROR;
} else BAD_STATE();
}
scrReturn(ret);
}
scrReturn(COR_SUCCESS);
// We're in the REQUESTING state here.
for (;;) {
int ret;
skip_to_requesting:
ret = COR_SUCCESS;
if (sev_dhcp) {
int r = an_packet(cs, dhcp_packet, dhcp_msgtype, dhcp_srcaddr,
true);
if (r == ANP_IGNORE) {
} else if (r == ANP_FAIL) {
ret = COR_ERROR;
scrReturn(ret);
continue;
} else if (r == ANP_REJECTED) {
sev_dhcp = false;
goto reinit;
} else if (r == ANP_CHECK_IP) {
if (arp_check(cs, dhcp_packet) < 0) {
log_warning("%s: Failed to make arp socket. Searching for new lease...",
client_config.interface);
if (reinit_selecting(cs, 3000) < 0) {
scrReturn(COR_ERROR);
goto reinit;
}
sev_dhcp = false;
goto reinit;
}
break;
} else BAD_STATE();
}
if (dhcp_timeout) {
// Send a request packet to the answering DHCP server.
int r = requesting_timeout(cs, nowts);
if (r == REQ_SUCCESS) {
} else if (r == REQ_TIMEOUT) {
// We timed out. Send another packet.
sev_dhcp = false;
goto reinit;
} else if (r == REQ_FAIL) {
// Failed to send packet. Sleep and retry.
ret = COR_ERROR;
} else BAD_STATE();
}
scrReturn(ret);
}
scrReturn(COR_SUCCESS);
// We're checking to see if there's a conflict for our IP. Technically,
// this is still in REQUESTING.
for (;;) {
int ret;
ret = COR_SUCCESS;
if (sev_dhcp) {
// XXX: Maybe I can think of something to do here. Would
// be more relevant if we tracked multiple dhcp servers.
}
if (sev_arp) {
int r = arp_do_collision_check(cs);
if (r == ARPR_OK) {
} else if (r == ARPR_CONFLICT) {
// XXX: If we tracked multiple DHCP servers, then we
// could fall back on another one.
if (reinit_selecting(cs, 0) < 0) {
ret = COR_ERROR;
scrReturn(ret);
}
sev_dhcp = false;
goto reinit;
} else if (r == ARPR_FAIL) {
ret = COR_ERROR;
scrReturn(ret);
continue;
} else BAD_STATE();
}
if (arp_timeout) {
int r = arp_collision_timeout(cs, nowts);
if (r == ARPR_FREE) {
break;
} else if (r == ARPR_OK) {
} else if (r == ARPR_FAIL) {
ret = COR_ERROR;
scrReturn(ret);
continue;
} else BAD_STATE();
}
if (dhcp_timeout) {
// Send a request packet to the answering DHCP server.
int r = requesting_timeout(cs, nowts);
if (r == REQ_SUCCESS) {
} else if (r == REQ_TIMEOUT) {
// We timed out. Send another packet.
sev_dhcp = false;
goto reinit;
} else if (r == REQ_FAIL) {
// Failed to send packet. Sleep and retry.
ret = COR_ERROR;
} else BAD_STATE();
}
scrReturn(ret);
}
scrReturn(COR_SUCCESS);
// We're in the BOUND, RENEWING, or REBINDING states here.
for (;;) {
int ret = COR_SUCCESS;
if (sev_dhcp && is_renewing(cs, nowts)) {
int r = an_packet(cs, dhcp_packet, dhcp_msgtype, dhcp_srcaddr,
false);
if (r == ANP_SUCCESS || r == ANP_IGNORE) {
} else if (r == ANP_FAIL) {
ret = COR_ERROR;
scrReturn(ret);
continue;
} else if (r == ANP_REJECTED) {
sev_dhcp = false;
goto reinit;
} else if (r == ANP_CHECK_IP) {
if (arp_check(cs, dhcp_packet) < 0) {
log_warning("%s: Failed to make arp socket. Searching for new lease...",
client_config.interface);
if (reinit_selecting(cs, 3000) < 0) {
ret = COR_ERROR;
scrReturn(ret);
}
sev_dhcp = false;
goto reinit;
}
} else BAD_STATE();
}
if (sev_arp) {
int r;
r = arp_do_defense(cs);
if (r == ARPR_OK) {
} else if (r == ARPR_CONFLICT) {
if (reinit_selecting(cs, 0) < 0) {
ret = COR_ERROR;
scrReturn(ret);
}
sev_dhcp = false;
goto reinit;
} else if (r == ARPR_FAIL) {
ret = COR_ERROR;
scrReturn(ret);
continue;
} else BAD_STATE();
if (!cs->got_router_arp || !cs->got_server_arp) {
r = arp_do_gw_query(cs);
if (r == ARPR_OK) {
} else if (r == ARPR_FREE) {
// We got both ARP addresses.
} else if (r == ARPR_FAIL) {
ret = COR_ERROR;
scrReturn(ret);
continue;
} else BAD_STATE();
} else if (cs->check_fingerprint) {
r = arp_do_gw_check(cs);
if (r == ARPR_OK) {
} else if (r == ARPR_FREE) {
cs->check_fingerprint = false;
} else if (r == ARPR_CONFLICT) {
cs->check_fingerprint = false;
if (reinit_selecting(cs, 0) < 0) {
ret = COR_ERROR;
scrReturn(ret);
}
sev_dhcp = false;
goto reinit;
} else if (r == ARPR_FAIL) {
ret = COR_ERROR;
scrReturn(ret);
continue;
} else BAD_STATE();
}
}
if (arp_timeout) {
arp_defense_timeout(cs, nowts);
if (!cs->got_router_arp || !cs->got_server_arp) {
int r = arp_gw_query_timeout(cs, nowts);
if (r == ARPR_OK) {
} else if (r == ARPR_FAIL) {
ret = COR_ERROR;
scrReturn(ret);
continue;
} else BAD_STATE();
} else if (cs->check_fingerprint) {
int r = arp_gw_check_timeout(cs, nowts);
if (r == ARPR_OK) {
} else if (r == ARPR_CONFLICT) {
cs->check_fingerprint = false;
if (reinit_selecting(cs, 0) < 0) {
ret = COR_ERROR;
scrReturn(ret);
}
sev_dhcp = false;
goto reinit;
} else if (r == ARPR_FAIL) {
ret = COR_ERROR;
scrReturn(ret);
continue;
} else BAD_STATE();
}
}
if (force_fingerprint) {
int r = ifup_action(cs);
if (r == IFUP_REVALIDATE) {
} else if (r == IFUP_NEWLEASE) {
if (reinit_selecting(cs, 0) < 0) {
ret = COR_ERROR;
scrReturn(ret);
}
sev_dhcp = false;
goto reinit;
} else if (r == IFUP_FAIL) {
ret = COR_ERROR;
scrReturn(ret);
continue;
} else BAD_STATE();
}
if (dhcp_timeout) {
int r;
if (is_rebinding(cs, nowts)) {
r = rebinding_timeout(cs, nowts);
} else if (is_renewing(cs, nowts)) {
r = renewing_timeout(cs, nowts);
} else {
r = bound_timeout(cs, nowts);
}
if (r == BTO_WAIT) {
} else if (r == BTO_EXPIRED) {
sev_dhcp = false;
goto reinit;
} else if (r == BTO_HARDFAIL) {
ret = COR_ERROR;
} else
BAD_STATE();
}
scrReturn(ret);
}
sev_dhcp = false;
goto reinit;
scrFinish(COR_SUCCESS);
// XXX: xmit_release -> acquire_lease
// XXX: Continue to clean up the ARP code.
}