sortix-mirror/dhclient/dhclient.c

1603 lines
46 KiB
C

/*
* Copyright (c) 2016, 2017, 2023 Jonas 'Sortie' Termansen.
* Copyright (c) 2021, 2022, 2023 Juhani 'nortti' Krekelä.
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
* dhclient.c
* Dynamic Host Configuration Protocol client.
*/
#include <sys/dnsconfig.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <assert.h>
#include <ctype.h>
#include <dirent.h>
#include <endian.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <netdb.h>
#include <net/if.h>
#include <netinet/if_ether.h>
#include <netinet/in.h>
#include <poll.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <timespec.h>
#include <unistd.h>
#define PORT_DHCP_SERVER 67
#define PORT_DHCP_CLIENT 68
struct dhcp
{
uint8_t op;
uint8_t htype;
uint8_t hlen;
uint8_t hops;
uint32_t xid;
uint16_t secs;
uint16_t flags;
uint8_t ciaddr[4];
uint8_t yiaddr[4];
uint8_t siaddr[4];
uint8_t giaddr[4];
uint8_t chaddr[16];
uint8_t sname[64];
uint8_t file[128];
uint8_t magic[4];
};
#define DHCP_OP_BOOTREQUEST 1
#define DHCP_OP_BOOTREPLY 2
#define DHCP_FLAGS_BROADCAST (1 << 15)
#define DHCP_HTYPE_ETHERNET 1
#define DHCP_HLEN_ETHERNET 6
#define DHCP_MAGIC_0 99
#define DHCP_MAGIC_1 130
#define DHCP_MAGIC_2 83
#define DHCP_MAGIC_3 99
#define OPTION_PAD 0
#define OPTION_SUBNET 1
#define OPTION_TIME_OFFSET 2
#define OPTION_ROUTERS 3
#define OPTION_DNS 6
#define OPTION_DOMAIN_NAME 12
#define OPTION_INTERFACE_MTU 26
#define OPTION_BROADCAST_ADDRESS 28
#define OPTION_NTP 42
#define OPTION_REQUESTED_IP 50
#define OPTION_LEASE_TIME 51
#define OPTION_OPTION_OVERLOAD 52
#define OPTION_DHCP_MSGTYPE 53
#define OPTION_SERVER_IDENTIFIER 54
#define OPTION_PARAMETER_REQUEST 55
#define OPTION_RENEWAL_TIME 58
#define OPTION_REBINDING_TIME 59
#define OPTION_END 255
#define DHCPDISCOVER 1
#define DHCPOFFER 2
#define DHCPREQUEST 3
#define DHCPDECLINE 4
#define DHCPACK 5
#define DHCPNAK 6
#define DHCPRELEASE 7
#define DHCPINFORM 9
struct dhcp_message
{
struct dhcp hdr;
unsigned char options[65536 - (sizeof(struct dhcp))];
};
enum option_state
{
OPTION_STATE_OPTIONS,
OPTION_STATE_FILE,
OPTION_STATE_SNAME,
OPTION_STATE_DONE,
};
struct option_iterate
{
struct dhcp* hdr;
unsigned char* options;
size_t offset;
size_t length;
enum option_state state;
bool has_sname_options;
bool has_file_options;
};
struct interface
{
char name[IF_NAMESIZE];
int if_fd;
int sock_fd;
struct ether_addr hwaddr;
unsigned int linkid;
};
struct request
{
unsigned char requests[255];
unsigned char requests_len;
uint32_t xid;
struct timespec begun;
struct timespec since_startup;
struct sockaddr_in remote;
socklen_t remote_len;
unsigned char server_identifier[4];
unsigned char yiaddr[4];
char remote_host_str[NI_MAXHOST];
char remote_serv_str[NI_MAXSERV];
char yiaddr_str[INET_ADDRSTRLEN];
};
struct lease
{
struct in_addr server;
struct in_addr address;
struct in_addr subnet;
struct in_addr router;
size_t dns_count;
unsigned char dns[DNSCONFIG_MAX_SERVERS][4];
uint32_t lease_time;
struct timespec t1;
struct timespec t2;
struct timespec expiration;
bool leased;
};
enum config_method { AUTO, MANUAL, NONE };
struct config_dns_servers
{
enum config_method method;
struct dnsconfig dnsconfig;
};
struct config_dns
{
struct config_dns_servers servers;
};
struct config_ether_address
{
enum config_method method;
struct ether_addr addr;
};
struct config_ether
{
struct config_ether_address address;
};
struct config_inet_address
{
enum config_method method;
struct in_addr addr;
};
struct config_inet
{
struct config_inet_address address;
struct config_inet_address router;
struct config_inet_address subnet;
};
struct config
{
struct config_dns dns;
struct config_ether ether;
struct config_inet inet;
};
struct config_file
{
const char* path;
FILE* fp;
bool shared;
char* line;
size_t line_size;
off_t line_number;
char* token;
char* token_start;
char* token_saved;
};
static bool dns_servers_parse(void* ptr, const char* value)
{
struct config_dns_servers* config = ptr;
if ( !strcmp(value, "none") )
config->method = NONE;
else if ( !strcmp(value, "auto") )
config->method = AUTO;
else
{
config->method = MANUAL;
config->dnsconfig.servers_count = 0;
while ( value[0] )
{
if ( value[0] == ',' )
{
value++;
continue;
}
char addr[INET6_ADDRSTRLEN];
size_t length = strcspn(value, ",");
if ( sizeof(addr) <= length )
return false;
memcpy(addr, value, length);
addr[length] = '\0';
value += length;
struct dnsconfig_server server = {0};
if ( inet_pton(AF_INET, addr, &server.addr.in) )
{
server.family = AF_INET;
server.addrsize = sizeof(server.addr.in);
}
else if ( inet_pton(AF_INET6, addr, &server.addr.in6) )
{
server.family = AF_INET6;
server.addrsize = sizeof(server.addr.in6);
}
else
return false;
size_t index = config->dnsconfig.servers_count++;
if ( DNSCONFIG_MAX_SERVERS < config->dnsconfig.servers_count )
return false;
config->dnsconfig.servers[index] = server;
if ( value[0] )
{
if ( value[0] != ',' )
return false;
value++;
}
}
}
return true;
}
static bool mac_parse(struct ether_addr* addr, const char* string)
{
for ( size_t i = 0; i < 6; i++ )
{
int upper;
if ( '0' <= string[i*3 + 0] && string[i*3 + 0] <= '9' )
upper = string[i*3 + 0] - '0';
else if ( 'a' <= string[i*3 + 0] && string[i*3 + 0] <= 'f' )
upper = string[i*3 + 0] - 'a' + 10;
else if ( 'A' <= string[i*3 + 0] && string[i*3 + 0] <= 'F' )
upper = string[i*3 + 0] - 'A' + 10;
else
return false;
int lower;
if ( '0' <= string[i*3 + 1] && string[i*3 + 1] <= '9' )
lower = string[i*3 + 1] - '0';
else if ( 'a' <= string[i*3 + 1] && string[i*3 + 1] <= 'f' )
lower = string[i*3 + 1] - 'a' + 10;
else if ( 'A' <= string[i*3 + 1] && string[i*3 + 1] <= 'F' )
lower = string[i*3 + 1] - 'A' + 10;
else
return false;
if ( string[i*3 + 2] != (i + 1 != 6 ? ':' : '\0') )
return false;
addr->ether_addr_octet[i] = upper << 4 | lower;
}
return true;
}
static bool ether_address_parse(void* ptr, const char* value)
{
struct config_ether_address* config = ptr;
if ( !strcmp(value, "auto") )
config->method = AUTO;
else if ( !strcmp(value, "none") )
config->method = NONE;
else
{
if ( !mac_parse(&config->addr, value) )
return false;
config->method = MANUAL;
}
return true;
}
static bool inet_address_parse(void* ptr, const char* value)
{
struct config_inet_address* config = ptr;
if ( !strcmp(value, "auto") )
config->method = AUTO;
else if ( !strcmp(value, "none") )
config->method = NONE;
else
{
if ( inet_pton(AF_INET, value, &config->addr) != 1 )
return false;
config->method = MANUAL;
}
return true;
}
#define ARRAY_LENGTH(array) (sizeof(array) / sizeof((array)[0]))
struct configuration
{
const char* name;
size_t offset;
bool (*parse)(void*, const char*);
};
#define CONFIGOFFSET(protocol, parameter) \
(offsetof(struct config, protocol) + \
offsetof(struct config_##protocol, parameter))
struct configuration dns_configurations[] =
{
{ "servers", CONFIGOFFSET(dns, servers), dns_servers_parse},
};
struct configuration ether_configurations[] =
{
{ "address", CONFIGOFFSET(ether, address), ether_address_parse},
};
struct configuration inet_configurations[] =
{
{ "address", CONFIGOFFSET(inet, address), inet_address_parse },
{ "router", CONFIGOFFSET(inet, router), inet_address_parse },
{ "subnet", CONFIGOFFSET(inet, subnet), inet_address_parse },
};
struct protocol
{
const char* name;
struct configuration* configurations;
size_t configurations_count;
};
struct protocol protocols[] =
{
{ "dns", dns_configurations, ARRAY_LENGTH(dns_configurations) },
{ "ether", ether_configurations, ARRAY_LENGTH(ether_configurations) },
{ "inet", inet_configurations, ARRAY_LENGTH(inet_configurations) },
};
static const struct protocol* protocol_lookup(const char* name)
{
for ( size_t i = 0; i < ARRAY_LENGTH(protocols); i++ )
if ( !strcmp(protocols[i].name, name) )
return &protocols[i];
return NULL;
}
static const struct configuration* configuration_lookup(
const struct protocol* protocol,
const char* name)
{
for ( size_t i = 0; i < protocol->configurations_count; i++ )
{
if ( !strcmp(protocol->configurations[i].name, name) )
return &protocol->configurations[i];
}
return NULL;
}
static bool config_file_read_line(struct config_file* config_file)
{
errno = 0;
ssize_t length = getline(&config_file->line, &config_file->line_size,
config_file->fp);
if ( length < 0 )
{
if ( errno )
err(1, "%s", config_file->path);
free(config_file->line);
return false;
}
config_file->line_number++;
// Remove leading whitespace.
char* line = config_file->line;
size_t start = 0;
while ( isspace((unsigned char) line[start]) )
start++;
length -= start;
memmove(line, line + start, length);
line[length] = '\0';
// Remove comments.
length = strcspn(line, "#");
line[length] = '\0';
// Remove trailing whitespace.
while ( length && isspace((unsigned char) line[length - 1]) )
length--;
line[length] = '\0';
return true;
}
static char* config_file_read_token(struct config_file* config_file)
{
while ( true )
{
if ( !config_file->line )
{
if ( !config_file_read_line(config_file) )
return NULL;
config_file->token_start = config_file->line;
}
if ( (config_file->token = strtok_r(config_file->token_start,
" \t\n\v\f\r",
&config_file->token_saved)) )
{
config_file->token_start = NULL;
return config_file->token;
}
config_file->token_start = NULL;
config_file->token_saved = NULL;
free(config_file->line);
config_file->line = NULL;
}
}
static char* config_file_read_parameter(struct config_file* config_file,
const char* option)
{
char* option_copy = strdup(option);
if ( !option_copy )
err(1, "malloc");
char* token = config_file_read_token(config_file);
if ( !token )
errx(1, "%s:%ji: error: %s expects a parameter", config_file->path,
(intmax_t) config_file->line_number, option_copy);
free(option_copy);
return token;
}
static bool match_interface(const struct interface* interface,
const char* specifier,
struct config_file* config_file)
{
if ( !strchr(specifier, ':') )
return strcmp(specifier, interface->name);
else if ( !strncmp(specifier, "etherhw:", strlen("etherhw:")) )
{
struct ether_addr ether_hwaddr;
const char *addr_string = specifier + strlen("etherhw:");
if ( !mac_parse(&ether_hwaddr, addr_string) )
errx(1, "%s:%ji: Invalid ethernet address: %s",
config_file->path, (intmax_t) config_file->line_number,
addr_string);
return !memcmp(&ether_hwaddr, &interface->hwaddr, sizeof(ether_hwaddr));
}
else if ( !strncmp(specifier, "id:", strlen("id:")) )
{
const char* id_string = specifier + strlen("id:");
char* end;
errno = 0;
unsigned long ulong = strtoul(id_string, &end, 10);
if ( errno || !*id_string || *end || ulong > UINT_MAX )
errx(1, "%s:%ji: Invalid interface id: %s",
config_file->path, (intmax_t) config_file->line_number,
id_string);
return ulong == interface->linkid;
}
errx(1, "%s:%ji: Invalid interface specifier: %s", config_file->path,
(intmax_t) config_file->line_number, specifier);
}
static void config_file_load(const struct interface* interface,
struct config* config,
struct config_file* config_file)
{
bool relevant = true;
config_file->line_number = 0;
const struct protocol* protocol = NULL;
const struct protocol* found_protocol = NULL;
const struct configuration* configuration = NULL;
char* option;
while ( (option = config_file_read_token(config_file)) )
{
if ( !strcmp(option, "if") )
{
if ( !config_file->shared )
errx(1, "%s:%ji: `if` not valid in interface-specific config",
config_file->path, (intmax_t) config_file->line_number);
char* value = config_file_read_parameter(config_file, option);
relevant = match_interface(interface, value, config_file) ||
!interface->name[0] /* testing */;
}
else if ( (found_protocol = protocol_lookup(option)) )
protocol = found_protocol;
else if ( protocol &&
(!strcmp(option, "none") || !strcmp(option, "auto")) )
{
for ( size_t i = 0; i < protocol->configurations_count; i++ )
if ( relevant && (configuration = protocol->configurations+i) )
configuration->parse((char*) config + configuration->offset,
option);
}
else if ( protocol &&
(configuration = configuration_lookup(protocol, option)) )
{
const char* value = config_file_read_parameter(config_file, option);
if ( relevant &&
!configuration->parse((char*) config + configuration->offset,
value) )
errx(1, "%s:%ji: Invalid configuration value: %s %s: %s",
config_file->path, (intmax_t) config_file->line_number,
protocol->name, configuration->name, value);
}
else if ( protocol )
errx(1, "%s:%ji: Unknown %s configuration or protocol: %s",
config_file->path, (intmax_t) config_file->line_number,
protocol->name, option);
else
errx(1, "%s:%ji: Unknown protocol: %s", config_file->path,
(intmax_t) config_file->line_number, option);
}
}
static bool config_file_load_path(const struct interface* interface,
struct config* config,
const char* path,
bool shared)
{
struct config_file config_file = {0};
config_file.path = path;
config_file.shared = shared;
if ( !(config_file.fp = fopen(path, "r")) )
{
if ( errno == ENOENT )
return false;
err(1, "%s", path);
}
config_file_load(interface, config, &config_file);
fclose(config_file.fp);
return true;
}
static void load_config(const struct interface* interface,
struct config* config,
const char* override_path)
{
memset(config, 0, sizeof(struct config));
if ( override_path )
{
if ( !config_file_load_path(interface, config, override_path, true) )
err(1, "%s", override_path);
return;
}
char* paths[3];
if ( asprintf(&paths[0], "/etc/dhclient.%02x:%02x:%02x:%02x:%02x:%02x.conf",
interface->hwaddr.ether_addr_octet[0],
interface->hwaddr.ether_addr_octet[1],
interface->hwaddr.ether_addr_octet[2],
interface->hwaddr.ether_addr_octet[3],
interface->hwaddr.ether_addr_octet[4],
interface->hwaddr.ether_addr_octet[5]) < 0 )
err(1, "malloc");
if ( asprintf(&paths[1], "/etc/dhclient.%s.conf", interface->name) < 0 )
err(1, "malloc");
if ( asprintf(&paths[2], "/etc/dhclient.conf") < 0 )
err(1, "malloc");
bool loaded = false;
for ( size_t i = 0; !loaded && i < ARRAY_LENGTH(paths); i++ )
{
bool shared = i == ARRAY_LENGTH(paths) - 1;
loaded = config_file_load_path(interface, config, paths[i], shared);
}
for ( size_t i = 0; i < ARRAY_LENGTH(paths); i++ )
free(paths[i]);
}
static void option_iterate_begin(struct option_iterate* iter,
struct dhcp* hdr,
unsigned char* options,
size_t length)
{
memset(iter, 0, sizeof(*iter));
iter->hdr = hdr;
iter->options = options;
iter->length = length;
}
static void option_iterate_begin_msg(struct option_iterate* iter,
struct dhcp_message* msg,
size_t length)
{
size_t offset = offsetof(struct dhcp_message, options);
assert(offset <= length);
option_iterate_begin(iter, &msg->hdr, msg->options, length - offset);
}
static bool option_iterate_array(struct option_iterate* iter,
unsigned char* options,
size_t length,
unsigned char* out_option,
unsigned char* out_optlen,
unsigned char** out_data)
{
while ( iter->offset < length )
{
unsigned char option = options[iter->offset++];
if ( option == OPTION_PAD )
continue;
if ( option == OPTION_END )
break;
if ( iter->offset == length )
return false;
unsigned char optlen = options[iter->offset++];
if ( length - iter->offset < optlen )
return false;
unsigned char* data = options + iter->offset;
*out_option = option;
*out_optlen = optlen;
*out_data = data;
iter->offset += optlen;
if ( option == OPTION_OPTION_OVERLOAD )
{
if ( optlen != 1 )
return false;
if ( iter->state == OPTION_STATE_OPTIONS )
{
if ( data[0] & 1 << 0 )
iter->has_sname_options = true;
if ( data[0] & 1 << 1 )
iter->has_file_options = true;
}
continue;
}
return true;
}
return false;
}
static bool option_iterate(struct option_iterate* iter,
unsigned char* out_option,
unsigned char* out_optlen,
unsigned char** out_data)
{
if ( iter->state == OPTION_STATE_OPTIONS )
{
if ( option_iterate_array(iter, iter->options, iter->length,
out_option, out_optlen, out_data) )
return true;
iter->state = OPTION_STATE_SNAME;
iter->offset = 0;
}
if ( iter->state == OPTION_STATE_SNAME )
{
if ( iter->has_sname_options &&
option_iterate_array(iter, iter->hdr->sname,
sizeof(iter->hdr->sname), out_option,
out_optlen, out_data) )
return true;
iter->state = OPTION_STATE_FILE;
iter->offset = 0;
}
if ( iter->state == OPTION_STATE_FILE )
{
if ( iter->has_file_options &&
option_iterate_array(iter, iter->hdr->file,
sizeof(iter->hdr->file), out_option,
out_optlen, out_data) )
return true;
iter->state = OPTION_STATE_DONE;
iter->offset = 0;
}
return false;
}
static bool option_search(struct option_iterate* input_iter,
unsigned char search_option,
unsigned char* out_optlen,
unsigned char** out_data)
{
struct option_iterate iter = *input_iter;
bool result = false;
unsigned char option;
unsigned char optlen;
unsigned char* data;
while ( option_iterate(&iter, &option, &optlen, &data) )
{
if ( option == search_option )
{
result = true;
*out_optlen = optlen;
*out_data = data;
break;
}
}
return result;
}
static size_t add_option_byte(unsigned char* options,
size_t optsmax,
size_t offset,
unsigned char byte)
{
if ( optsmax <= offset )
errx(1, "too many dhcp options");
options[offset++] = byte;
return offset;
}
static size_t add_option(unsigned char* options,
size_t optsmax,
size_t offset,
unsigned char option,
unsigned char optlen,
const unsigned char* data)
{
offset = add_option_byte(options, optsmax, offset, option);
offset = add_option_byte(options, optsmax, offset, optlen);
for ( size_t i = 0; i < optlen; i++ )
offset = add_option_byte(options, optsmax, offset, data[i]);
return offset;
}
static bool send_dhcpdiscover(const struct interface* interface,
const struct request* request,
struct sockaddr_in dest)
{
struct dhcp_message msg = {0};
msg.hdr.op = DHCP_OP_BOOTREQUEST;
msg.hdr.htype = DHCP_HTYPE_ETHERNET;
msg.hdr.hlen = DHCP_HLEN_ETHERNET;
msg.hdr.xid = htobe32(request->xid);
msg.hdr.secs = htobe16((uint16_t) request->since_startup.tv_sec);
msg.hdr.flags = htobe16(DHCP_FLAGS_BROADCAST);
memset(msg.hdr.chaddr, 0, sizeof(msg.hdr.chaddr));
memcpy(msg.hdr.chaddr, &interface->hwaddr, sizeof(interface->hwaddr));
msg.hdr.magic[0] = DHCP_MAGIC_0;
msg.hdr.magic[1] = DHCP_MAGIC_1;
msg.hdr.magic[2] = DHCP_MAGIC_2;
msg.hdr.magic[3] = DHCP_MAGIC_3;
const size_t optsmax = sizeof(msg.options);
size_t optsoff = 0;
const unsigned char msgtype = DHCPDISCOVER;
optsoff = add_option(msg.options, optsmax, optsoff, OPTION_DHCP_MSGTYPE, 1,
&msgtype);
if ( request->requests_len )
optsoff = add_option(msg.options, optsmax, optsoff,
OPTION_PARAMETER_REQUEST, request->requests_len,
request->requests);
optsoff = add_option_byte(msg.options, optsmax, optsoff, OPTION_END);
const size_t msgsize = sizeof(msg.hdr) + optsoff;
if ( sendto(interface->sock_fd, &msg, msgsize, 0,
(const struct sockaddr*) &dest, sizeof(dest)) < 0)
{
warn("send");
// Drop packets and retry on transient errors and otherwise consider the
// send attempt permanently failed for now.
return errno == EAGAIN || errno == EWOULDBLOCK ||
errno == ENOMEM || errno == ENOBUFS;
}
return true;
}
static bool send_dhcprequest(const struct interface* interface,
const struct request* request,
struct sockaddr_in dest,
struct in_addr client_address)
{
struct dhcp_message msg = {0};
msg.hdr.op = DHCP_OP_BOOTREQUEST;
msg.hdr.htype = DHCP_HTYPE_ETHERNET;
msg.hdr.hlen = DHCP_HLEN_ETHERNET;
msg.hdr.xid = htobe32(request->xid);
msg.hdr.secs = htobe16((uint16_t) request->since_startup.tv_sec);
msg.hdr.flags = client_address.s_addr ? 0 : htobe16(DHCP_FLAGS_BROADCAST);
memcpy(msg.hdr.ciaddr, &client_address, sizeof(client_address));
memset(msg.hdr.chaddr, 0, sizeof(msg.hdr.chaddr));
memcpy(msg.hdr.chaddr, &interface->hwaddr, sizeof(interface->hwaddr));
msg.hdr.magic[0] = DHCP_MAGIC_0;
msg.hdr.magic[1] = DHCP_MAGIC_1;
msg.hdr.magic[2] = DHCP_MAGIC_2;
msg.hdr.magic[3] = DHCP_MAGIC_3;
const size_t optsmax = sizeof(msg.options);
size_t optsoff = 0;
const unsigned char msgtype = DHCPREQUEST;
optsoff = add_option(msg.options, optsmax, optsoff, OPTION_DHCP_MSGTYPE, 1,
&msgtype);
if ( request->requests_len )
optsoff = add_option(msg.options, optsmax, optsoff,
OPTION_PARAMETER_REQUEST, request->requests_len,
request->requests);
if ( !client_address.s_addr )
{
optsoff = add_option(msg.options, optsmax, optsoff,
OPTION_SERVER_IDENTIFIER,
sizeof(request->server_identifier),
request->server_identifier);
optsoff = add_option(msg.options, optsmax, optsoff, OPTION_REQUESTED_IP,
sizeof(request->yiaddr),
request->yiaddr);
}
optsoff = add_option_byte(msg.options, optsmax, optsoff, OPTION_END);
const size_t msgsize = sizeof(msg.hdr) + optsoff;
if ( sendto(interface->sock_fd, &msg, msgsize, 0,
(const struct sockaddr*) &dest, sizeof(dest)) < 0)
{
warn("send");
// Drop packets and retry on transient errors and otherwise consider the
// send attempt permanently failed for now.
return errno == EAGAIN || errno == EWOULDBLOCK ||
errno == ENOMEM || errno == ENOBUFS;
}
return true;
}
static ssize_t receive_dhcp_message(const struct interface* interface,
struct dhcp_message* msg,
struct timespec* left,
struct sockaddr_in* remote,
socklen_t* remote_len)
{
struct pollfd pfd = { .fd = interface->sock_fd, .events = POLLIN };
int num_events = ppoll(&pfd, 1, left, NULL);
if ( num_events < 0 )
err(1, "ppoll");
if ( num_events == 0 )
return -1;
*remote_len = sizeof(remote);
ssize_t amount = recvfrom(interface->sock_fd, msg, sizeof(*msg), 0,
(struct sockaddr*) remote, remote_len);
if ( amount < 0 )
{
warn("recv");
return -1;
}
return amount;
}
static bool check_dchp_message(const struct interface* interface,
const struct request* request,
struct dhcp_message* msg,
size_t amount)
{
if ( (size_t) amount < sizeof(msg->hdr) )
return false;
if ( msg->hdr.op != DHCP_OP_BOOTREPLY )
return false;
if ( msg->hdr.htype != DHCP_HTYPE_ETHERNET ||
msg->hdr.hlen != DHCP_HLEN_ETHERNET )
return false;
unsigned char chaddr[16];
memset(chaddr, 0, sizeof(chaddr));
memcpy(chaddr, &interface->hwaddr, sizeof(interface->hwaddr));
if ( memcmp(msg->hdr.chaddr, chaddr, sizeof(msg->hdr.chaddr)) != 0 )
return false;
if ( msg->hdr.xid != htobe32(request->xid) )
return false;
if ( msg->hdr.magic[0] != DHCP_MAGIC_0 ||
msg->hdr.magic[1] != DHCP_MAGIC_1 ||
msg->hdr.magic[2] != DHCP_MAGIC_2 ||
msg->hdr.magic[3] != DHCP_MAGIC_3 )
return false;
return true;
}
static bool parse_dhcpoffer(const struct interface* interface,
struct request* request,
struct dhcp_message* msg,
size_t amount)
{
if ( !check_dchp_message(interface, request, msg, amount) )
return false;
struct option_iterate iter;
unsigned char optlen;
unsigned char* optdata;
option_iterate_begin_msg(&iter, msg, amount);
if ( !option_search(&iter, OPTION_DHCP_MSGTYPE, &optlen, &optdata) ||
optlen != 1 || optdata[0] != DHCPOFFER )
{
fprintf(stderr, "error: not DHCPOFFER\n");
return false;
}
if ( !option_search(&iter, OPTION_SERVER_IDENTIFIER, &optlen, &optdata) ||
optlen != sizeof(request->server_identifier) )
return false;
memcpy(request->server_identifier, optdata,
sizeof(request->server_identifier));
memcpy(request->yiaddr, msg->hdr.yiaddr, sizeof(request->yiaddr));
return true;
}
static bool parse_dhcpack(const struct interface* interface,
const struct config* config,
struct request* request,
struct lease* lease,
struct dhcp_message* msg,
size_t amount)
{
if ( !check_dchp_message(interface, request, msg, amount) )
return false;
struct option_iterate iter;
unsigned char optlen;
unsigned char* optdata;
option_iterate_begin_msg(&iter, msg, amount);
if ( !option_search(&iter, OPTION_DHCP_MSGTYPE, &optlen, &optdata) ||
optlen != 1 || optdata[0] != DHCPACK )
{
fprintf(stderr, "error: not DHCPACK\n");
return false;
}
if ( !option_search(&iter, OPTION_SERVER_IDENTIFIER, &optlen, &optdata) ||
optlen != sizeof(request->server_identifier) )
{
fprintf(stderr, "error: DHCPACK missing server identifier\n");
return false;
}
if ( memcmp(request->yiaddr, msg->hdr.yiaddr, sizeof(request->yiaddr)) )
{
fprintf(stderr, "error: Served bait-and-switched the address\n");
return false;
}
if ( config->inet.subnet.method == AUTO )
{
if ( !option_search(&iter, OPTION_SUBNET, &optlen, &optdata) ||
optlen != sizeof(lease->subnet) )
{
fprintf(stderr, "error: DHCPACK missing subnet mask\n");
return false;
}
memcpy(&lease->subnet, optdata, sizeof(lease->subnet));
}
if ( config->inet.router.method == AUTO )
{
if ( !option_search(&iter, OPTION_ROUTERS, &optlen, &optdata) ||
optlen < sizeof(lease->router) )
{
fprintf(stderr, "error: DHCPACK missing router information\n");
return false;
}
memcpy(&lease->router, optdata, sizeof(lease->router));
}
if ( !option_search(&iter, OPTION_LEASE_TIME, &optlen, &optdata) ||
optlen != 4 )
{
fprintf(stderr, "error: DHCPACK missing lease time\n");
return false;
}
lease->lease_time = (uint32_t) optdata[0] << 24 |
(uint32_t) optdata[1] << 16 |
(uint32_t) optdata[2] << 8 |
(uint32_t) optdata[3] << 0;
if ( !lease->lease_time )
{
fprintf(stderr, "error: DHCPACK has zero lease time\n");
return false;
}
memcpy(request->server_identifier, optdata,
sizeof(request->server_identifier));
memcpy(&lease->address, msg->hdr.yiaddr, sizeof(lease->address));
if ( config->dns.servers.method == AUTO )
{
if ( option_search(&iter, OPTION_DNS, &optlen, &optdata) )
{
size_t offset = 0;
for ( lease->dns_count = 0;
lease->dns_count < DNSCONFIG_MAX_SERVERS &&
4 <= optlen - offset;
lease->dns_count++ )
{
lease->dns[lease->dns_count][0] = optdata[offset++];
lease->dns[lease->dns_count][1] = optdata[offset++];
lease->dns[lease->dns_count][2] = optdata[offset++];
lease->dns[lease->dns_count][3] = optdata[offset++];
}
}
}
return true;
}
static bool find_dhcp_server(const struct interface* interface,
struct request* request)
{
struct sockaddr_in dest = {0};
dest.sin_family = AF_INET;
dest.sin_port = htobe16(PORT_DHCP_SERVER);
dest.sin_addr.s_addr = htobe32(INADDR_BROADCAST);
unsigned int retransmissions = 0;
struct timespec last_sent = timespec_make(-1, 0);
struct timespec timeout = timespec_make(0, 0);
while ( true )
{
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
struct timespec since_sent = timespec_sub(now, last_sent);
if ( timespec_ge(since_sent, timeout) )
{
if ( retransmissions == 0 )
fprintf(stderr, "Broadcasting DHCPDISCOVER\n");
else
fprintf(stderr, "Broadcasting DHCPDISCOVER (attempt %i)\n",
retransmissions + 1);
if ( !send_dhcpdiscover(interface, request, dest) )
return false;
last_sent = now;
timeout = timespec_make(1 << retransmissions,
arc4random_uniform(1000000000));
if ( retransmissions < 6 )
retransmissions++;
else
{
fprintf(stderr, "error: DHCPDISCOVER timed out\n");
return false;
}
}
struct timespec left =
timespec_sub(timespec_add(last_sent, timeout), now);
struct dhcp_message msg = {0};
ssize_t amount = receive_dhcp_message(interface, &msg, &left,
&request->remote,
&request->remote_len);
if ( amount < 0 )
continue;
if ( !parse_dhcpoffer(interface, request, &msg, amount) )
continue;
getnameinfo((const struct sockaddr*) &request->remote,
request->remote_len,
request->remote_host_str, sizeof(request->remote_host_str),
request->remote_serv_str, sizeof(request->remote_serv_str),
NI_NUMERICHOST | NI_NUMERICSERV);
inet_ntop(AF_INET, request->yiaddr, request->yiaddr_str,
INET_ADDRSTRLEN);
fprintf(stderr, "DHCPOFFER of %s from %s:%s\n", request->yiaddr_str,
request->remote_host_str, request->remote_serv_str);
return true;
}
}
static bool acquire_lease(const struct interface* interface,
const struct config* config,
struct request* request,
struct lease* lease)
{
// Don't unicast during the REBINDING state.
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
bool unicast = lease->leased && timespec_lt(now, lease->t2);
struct sockaddr_in dest = {0};
dest.sin_family = AF_INET;
dest.sin_port = htobe16(PORT_DHCP_SERVER);
dest.sin_addr.s_addr = htobe32(INADDR_BROADCAST);
if ( lease->leased )
{
memcpy(request->yiaddr, &lease->address, 4);
request->remote.sin_family = AF_INET;
request->remote.sin_addr = lease->server;
request->remote.sin_port = htobe16(PORT_DHCP_SERVER);
request->remote_len = sizeof(request->remote);
}
if ( unicast )
dest.sin_addr.s_addr = lease->server.s_addr;
inet_ntop(AF_INET, request->yiaddr, request->yiaddr_str, INET_ADDRSTRLEN);
getnameinfo((const struct sockaddr*) &request->remote,
sizeof(request->remote),
request->remote_host_str, sizeof(request->remote_host_str),
request->remote_serv_str, sizeof(request->remote_serv_str),
NI_NUMERICHOST | NI_NUMERICSERV);
fprintf(stderr, "%s %s from %s:%s\n",
lease->leased ? "Renewing" : "Requesting", request->yiaddr_str,
request->remote_host_str, request->remote_serv_str);
unsigned int retransmissions = 0;
struct timespec last_sent = timespec_make(-1, 0);
struct timespec timeout = timespec_make(0, 0);
while ( true )
{
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
struct timespec since_sent = timespec_sub(now, last_sent);
if ( timespec_le(timeout, since_sent) )
{
const char* action = unicast ? "Sending" : "Broadcasting";
if ( retransmissions == 0 )
fprintf(stderr, "%s DHCPREQUEST", action);
else
fprintf(stderr, "%s DHCPREQUEST (attempt %i)",
action, retransmissions + 1);
if ( unicast )
fprintf(stderr, " to %s:%s\n",
request->remote_host_str, request->remote_serv_str);
else
fputc('\n', stderr);
if ( !send_dhcprequest(interface, request, dest, lease->address) )
return false;
last_sent = now;
timeout = timespec_make(1 << retransmissions,
arc4random_uniform(1000000000));
if ( retransmissions < 6 )
retransmissions++;
else
{
fprintf(stderr, "error: DHCPREQUEST timed out\n");
return false;
}
}
struct timespec left =
timespec_sub(timespec_add(last_sent, timeout), now);
struct dhcp_message msg = {0};
struct sockaddr_in peer;
socklen_t peer_len;
ssize_t amount = receive_dhcp_message(interface, &msg, &left, &peer,
&peer_len);
if ( amount < 0 )
continue;
if ( peer_len != request->remote_len ||
memcmp(&peer, &request->remote, peer_len) != 0 )
continue;
// TODO: Handle DHCPNACK gracefully, unassign the allocated address.
if ( !parse_dhcpack(interface, config, request, lease, &msg, amount) )
continue;
fprintf(stderr, "DHCPACK of %s from %s:%s\n", request->yiaddr_str,
request->remote_host_str, request->remote_serv_str);
memcpy(&lease->server, request->server_identifier,
sizeof(lease->server));
lease->expiration = timespec_add(request->begun,
timespec_make(lease->lease_time, 0));
// The lease isn't expired in the main loop during T2 renewal, which may
// take 2^6 (64) seconds and T2 has 15% of the lease time to renew, so
// that means it needs at least 64 / 0.15 = 427 seconds (7 min) to avoid
// using the lease after it has expired. Round up to a nice 10 minutes.
if ( 10 * 60 <= lease->lease_time )
{
struct timespec d1 = timespec_make(lease->lease_time * 0.5,
arc4random_uniform(1000000000));
struct timespec d2 = timespec_make(lease->lease_time * 0.85,
arc4random_uniform(1000000000));
lease->t1 = timespec_add(request->begun, d1);
lease->t2 = timespec_add(request->begun, d2);
}
else
{
fprintf(stderr, "warning: Lease time of %u seconds is too short "
"for renewal to work properly", lease->lease_time);
lease->t1 = lease->expiration;
lease->t2 = lease->expiration;
}
lease->leased = true;
return true;
}
}
static void configure_interface(const struct interface* interface,
const struct config* config,
const struct lease* lease)
{
if ( config->inet.address.method != NONE ||
config->inet.router.method != NONE ||
config->inet.subnet.method != NONE )
{
struct if_config_inet inet_cfg = {0};
if ( ioctl(interface->if_fd, NIOC_GETCONFIG_INET, &inet_cfg) < 0 )
err(1, "%s: ioctl: NIOC_GETCONFIG_INET", interface->name);
if ( config->inet.address.method == AUTO )
inet_cfg.address = lease->address;
else if ( config->inet.address.method == MANUAL )
inet_cfg.address = config->inet.address.addr;
if ( config->inet.router.method == AUTO )
inet_cfg.router = lease->router;
else if ( config->inet.router.method == MANUAL )
inet_cfg.router = config->inet.router.addr;
if ( config->inet.subnet.method == AUTO )
inet_cfg.subnet = lease->subnet;
else if ( config->inet.subnet.method == MANUAL )
inet_cfg.subnet = config->inet.subnet.addr;
if ( ioctl(interface->if_fd, NIOC_SETCONFIG_INET, &inet_cfg) < 0 )
err(1, "%s: ioctl: NIOC_SETCONFIG_INET", interface->name);
fprintf(stderr, "Configured network interface %s\n", interface->name);
}
if ( config->dns.servers.method != NONE )
{
struct dnsconfig dnsconfig = {0};
if ( config->dns.servers.method == AUTO )
{
dnsconfig.servers_count = lease->dns_count;
for ( size_t i = 0; i < lease->dns_count; i++ )
{
dnsconfig.servers[i].family = AF_INET;
dnsconfig.servers[i].addrsize = 4;
memcpy(&dnsconfig.servers[i].addr, lease->dns[i], 4);
}
}
else if ( config->dns.servers.method == MANUAL )
dnsconfig = config->dns.servers.dnsconfig;
if ( setdnsconfig(&dnsconfig) < 0 )
err(1, "setdnsconfig");
fprintf(stderr, "Configured DNS\n");
}
}
static void activate_lease(const struct interface* interface,
const struct config* config,
const struct lease* lease)
{
char address_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &lease->address, address_str, sizeof(address_str));
fprintf(stderr, "Leased %s for %u seconds\n",
address_str, lease->lease_time);
if ( config->inet.router.method == AUTO )
{
char router_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &lease->router, router_str, sizeof(router_str));
fprintf(stderr, "Router is %s\n", router_str);
}
if ( config->inet.subnet.method == AUTO )
{
char subnet_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &lease->subnet, subnet_str, sizeof(subnet_str));
fprintf(stderr, "Subnet is %s\n", subnet_str);
}
if ( config->dns.servers.method == AUTO )
{
if ( lease->dns_count == 0 )
fprintf(stderr, "No DNS servers were offered\n");
else for ( size_t i = 0; i < lease->dns_count; i++ )
{
char dns_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, lease->dns[i], dns_str, sizeof(dns_str));
fprintf(stderr, "DNS server %zu is %s\n", i + 1, dns_str);
}
}
configure_interface(interface, config, lease);
}
static void deactivate_lease(const struct interface* interface,
const struct config* config,
struct lease* lease)
{
char address_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &lease->address, address_str, sizeof(address_str));
fprintf(stderr, "Lease of %s has expired after %u seconds\n",
address_str, lease->lease_time);
struct if_config_inet inet_cfg = {0};
if ( ioctl(interface->if_fd, NIOC_GETCONFIG_INET, &inet_cfg) < 0 )
err(1, "%s: ioctl: NIOC_GETCONFIG_INET", interface->name);
if ( config->inet.address.method == AUTO )
inet_cfg.address.s_addr = htobe32(INADDR_ANY);
if ( config->inet.router.method == AUTO )
inet_cfg.router.s_addr = htobe32(INADDR_ANY);
if ( config->inet.subnet.method == AUTO )
inet_cfg.subnet.s_addr = htobe32(INADDR_ANY);
if ( ioctl(interface->if_fd, NIOC_SETCONFIG_INET, &inet_cfg) < 0 )
err(1, "%s: ioctl: NIOC_SETCONFIG_INET", interface->name);
fprintf(stderr, "Unconfigured network interface %s\n", interface->name);
lease->expiration = timespec_nul();
lease->address.s_addr = htobe32(INADDR_ANY);
lease->server.s_addr = htobe32(INADDR_ANY);
lease->leased = false;
}
static void ready(void)
{
const char* readyfd_env = getenv("READYFD");
if ( !readyfd_env )
return;
int readyfd = atoi(readyfd_env);
char c = '\n';
write(readyfd, &c, 1);
close(readyfd);
unsetenv("READYFD");
}
static void wait_for_link(struct interface* interface, int* timeout_ptr)
{
fprintf(stderr, "Waiting for interface %s to come up\n",
interface->name);
while ( true )
{
struct pollfd pfd =
{
.fd = interface->if_fd,
.events = POLLOUT
};
int num_events = poll(&pfd, 1, *timeout_ptr);
if ( num_events < 0 )
err(1, "poll");
else if ( num_events == 1 )
break;
// Signal readiness if waiting for the link to go up times out.
if ( 0 <= *timeout_ptr )
{
fprintf(stderr, "Link has not come up yet on %s\n",
interface->name);
ready();
*timeout_ptr = -1;
}
}
fprintf(stderr, "Interface %s is up\n", interface->name);
}
int main(int argc, char* argv[])
{
struct interface interface = {0};
const char* file = NULL;
bool test = false;
int opt;
while ( (opt = getopt(argc, argv, "f:t")) != -1 )
{
switch ( opt )
{
case 'f': file = optarg; break;
case 't': test = true; break;
default: return 1;
}
}
int args_min = test ? 0 : 1;
int args_max = 1;
if ( argc - optind < args_min || args_max < argc - optind )
{
printf("Usage: %s <interface>\n", argv[0]);
return 1;
}
if ( 1 <= argc - optind )
{
const char* path = argv[optind];
int dev_fd = open("/dev", O_RDONLY | O_DIRECTORY);
if ( dev_fd < 0 )
err(1, "/dev");
interface.if_fd = openat(dev_fd, path, test ? O_RDONLY : O_RDWR);
if ( interface.if_fd < 0 )
err(1, "%s", path);
close(dev_fd);
int type = ioctl(interface.if_fd, IOCGETTYPE);
if ( type < 0 )
err(1, "%s: ioctl: IOCGETTYPE", path);
if ( IOC_TYPE(type) != IOC_TYPE_NETWORK_INTERFACE )
errx(1, "%s: Not a network interface", path);
struct if_info info;
if ( ioctl(interface.if_fd, NIOC_GETINFO, &info) < 0 )
err(1, "%s: ioctl: NIOC_GETINFO", path);
if ( info.type == IF_TYPE_LOOPBACK )
errx(0, "%s: Loopback interface doesn't need to be configured", path);
if ( info.type != IF_TYPE_ETHERNET )
errx(1, "%s: ioctl: NIOC_GETINFO: Unknown device type", path);
if ( info.addrlen != 6 )
errx(1, "%s: ioctl: NIOC_GETINFO: Invalid address length", path);
memcpy(interface.name, info.name, IF_NAMESIZE);
memcpy(&interface.hwaddr, info.addr, 6);
interface.linkid = info.linkid;
}
struct config config;
load_config(&interface, &config, file);
if ( test )
return 0;
interface.sock_fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if ( interface.sock_fd < 0 )
err(1, "socket");
if ( setsockopt(interface.sock_fd, SOL_SOCKET, SO_BINDTOINDEX,
&interface.linkid, sizeof(interface.linkid)) < 0 )
err(1, "setsockopt: SO_BINDTOINDEX");
int enable = 1;
if ( setsockopt(interface.sock_fd, SOL_SOCKET, SO_BROADCAST, &enable,
sizeof(enable)) < 0 )
err(1, "setsockopt: SO_BROADCAST");
struct sockaddr_in local = {0};
local.sin_family = AF_INET;
local.sin_port = htobe16(PORT_DHCP_CLIENT);
local.sin_addr.s_addr = htobe32(INADDR_ANY);
if ( bind(interface.sock_fd, (const struct sockaddr*) &local,
sizeof(local)) < 0 )
{
if ( errno == EADDRINUSE )
errx(0, "%s: Interface is already managed: bind: 0.0.0.0:%u",
interface.name, PORT_DHCP_CLIENT);
err(1, "%s: bind: 0.0.0.0:%u", interface.name, PORT_DHCP_CLIENT);
}
if ( config.ether.address.method == AUTO ||
config.ether.address.method == MANUAL )
{
struct if_config_ether ether_cfg = {0};
if ( ioctl(interface.if_fd, NIOC_GETCONFIG_ETHER, &ether_cfg) < 0 )
err(1, "%s: ioctl: NIOC_GETCONFIG_ETHER", interface.name);
if ( config.ether.address.method == AUTO )
ether_cfg.address = interface.hwaddr;
else if ( config.ether.address.method == MANUAL )
ether_cfg.address = config.ether.address.addr;
if ( ioctl(interface.if_fd, NIOC_SETCONFIG_ETHER, &ether_cfg) < 0 )
err(1, "%s: ioctl: NIOC_SETCONFIG_ETHER", interface.name);
fprintf(stderr, "Configured ethernet on interface %s\n",
interface.name);
}
bool dhcp_needed = config.inet.address.method == AUTO ||
config.inet.router.method == AUTO ||
config.inet.subnet.method == AUTO ||
config.dns.servers.method == AUTO;
// TODO: Implement DHCPINFORM mode.
if ( dhcp_needed && config.inet.address.method != AUTO )
errx(1,
"%s: IP address must be configured automatically if using DHCP",
interface.name);
if ( !dhcp_needed )
{
configure_interface(&interface, &config, NULL);
return 0;
}
// TODO: Allow the link up timeout to be configurable.
int link_up_timeout = 10 * 1000; // Documented in dhclient(8).
struct timespec startup;
clock_gettime(CLOCK_MONOTONIC, &startup);
bool first = true;
bool link_up = false;
bool success = false;
struct lease lease = {0};
while ( true )
{
if ( !first )
ready();
if ( errno == ENETDOWN )
link_up = false;
if ( !first && !success )
{
fprintf(stderr, "Negotiation failed, waiting before restarting\n");
struct timespec delay =
timespec_make(1, arc4random_uniform(1000000000));
nanosleep(&delay, NULL);
}
first = false;
success = false;
if ( !link_up )
{
wait_for_link(&interface, &link_up_timeout);
link_up = true;
}
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
if ( lease.leased && timespec_le(lease.expiration, now) )
deactivate_lease(&interface, &config, &lease);
struct request request = {0};
if ( config.inet.router.method == AUTO )
request.requests[request.requests_len++] = OPTION_ROUTERS;
if ( config.inet.subnet.method == AUTO )
request.requests[request.requests_len++] = OPTION_SUBNET;
if ( config.dns.servers.method == AUTO )
request.requests[request.requests_len++] = OPTION_DNS;
request.xid = arc4random();
request.begun = now;
request.since_startup = timespec_sub(now, startup);
if ( !lease.leased && !find_dhcp_server(&interface, &request) )
continue;
if ( !lease.leased || timespec_le(lease.t1, now) )
{
if ( acquire_lease(&interface, &config, &request, &lease) )
{
activate_lease(&interface, &config, &lease);
ready();
}
if ( !lease.leased )
continue;
}
success = true;
struct timespec wakeup;
if ( timespec_lt(now, lease.t1) )
wakeup = lease.t1;
else if ( timespec_lt(now, lease.t2) )
wakeup = lease.t2;
else
wakeup = lease.expiration;
// TODO: Use poll to wake on incoming datagrams which are discarded.
// Otherwise they'll be received with errors on renewal and
// rejected and legimate packets might be dropped until the
// receive queue drains.
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &wakeup, NULL);
}
}