/*
 * 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);
	}
}
