From 4379ca962a492ad887f0494aee71286f37aa56ca Mon Sep 17 00:00:00 2001 From: Jonas 'Sortie' Termansen Date: Fri, 17 Feb 2023 23:31:57 +0100 Subject: [PATCH] Add dhclient(8). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Juhani Krekelä --- Makefile | 1 + build-aux/iso-grub-cfg.sh | 26 + dhclient/.gitignore | 1 + dhclient/Makefile | 32 + dhclient/dhclient.8 | 165 +++ dhclient/dhclient.c | 1602 +++++++++++++++++++++ dhclient/dhclient.conf.5 | 183 +++ dnsconfig/dnsconfig.8 | 4 + ifconfig/ifconfig.8 | 15 + libc/sys/dnsconfig/getdnsconfig.2 | 1 + share/init/dhclient | 2 + share/init/network | 1 + share/man/man4/if.4 | 1 + share/man/man5/init.5 | 25 + share/man/man7/installation.7 | 26 +- share/man/man7/release-iso-bootconfig.7 | 5 + share/man/man7/release-iso-modification.7 | 13 +- share/man/man7/upgrade.7 | 3 +- tix/tix-iso-bootconfig | 4 + tix/tix-iso-bootconfig.8 | 28 + 20 files changed, 2135 insertions(+), 3 deletions(-) create mode 100644 dhclient/.gitignore create mode 100644 dhclient/Makefile create mode 100644 dhclient/dhclient.8 create mode 100644 dhclient/dhclient.c create mode 100644 dhclient/dhclient.conf.5 create mode 100644 share/init/dhclient diff --git a/Makefile b/Makefile index 9056f0db..7e2e96fa 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ libmount \ bench \ carray \ checksum \ +dhclient \ disked \ dnsconfig \ editor \ diff --git a/build-aux/iso-grub-cfg.sh b/build-aux/iso-grub-cfg.sh index ec08d41e..fd274223 100755 --- a/build-aux/iso-grub-cfg.sh +++ b/build-aux/iso-grub-cfg.sh @@ -120,6 +120,12 @@ ports=$(ls repository | sed -E 's/\.tix\.tar\.xz$//') mkdir -p boot/grub +mkdir -p boot/grub/init + +echo "furthermore" > boot/grub/init/furthermore +# TODO: Possibly use a 'try' feature to not warn in case it was already unset. +echo "unset require dhclient" > boot/grub/init/network-no-dhclient + exec > boot/grub/grub.cfg for hook in \ @@ -186,6 +192,7 @@ else fi set enable_src=true set enable_network_drivers= +set enable_dhclient=true export version export machine @@ -196,6 +203,7 @@ export default export no_random_seed export enable_src export enable_network_drivers +export enable_dhclient EOF if [ -n "$ports" ]; then @@ -285,6 +293,12 @@ cat << EOF multiboot /$kernel \$no_random_seed \$enable_network_drivers "\$@" echo done hook_kernel_post + if ! \$enable_dhclient; then + echo -n "Disabling dhclient ... " + module /boot/grub/init/furthermore --create-to /etc/init/network + module /boot/grub/init/network-no-dhclient --append-to /etc/init/network + echo done + fi if [ \$no_random_seed != --no-random-seed ]; then echo -n "Loading /boot/random.seed (256) ... " module /boot/random.seed --random-seed @@ -432,6 +446,18 @@ else } fi +if \$enable_dhclient; then + menuentry "Disable DHCP client" { + enable_dhclient=false + configfile /boot/grub/advanced.cfg + } +else + menuentry "Enable DHCP client" { + enable_dhclient=true + configfile /boot/grub/advanced.cfg + } +fi + menuentry "Select binary packages..." { configfile /boot/grub/tix.cfg } diff --git a/dhclient/.gitignore b/dhclient/.gitignore new file mode 100644 index 00000000..5d468f9b --- /dev/null +++ b/dhclient/.gitignore @@ -0,0 +1 @@ +dhclient diff --git a/dhclient/Makefile b/dhclient/Makefile new file mode 100644 index 00000000..52fc25cb --- /dev/null +++ b/dhclient/Makefile @@ -0,0 +1,32 @@ +SOFTWARE_MEANT_FOR_SORTIX=1 +include ../build-aux/platform.mak +include ../build-aux/compiler.mak +include ../build-aux/version.mak +include ../build-aux/dirs.mak + +OPTLEVEL?=$(DEFAULT_OPTLEVEL) +CFLAGS?=$(OPTLEVEL) + +CFLAGS += -Wall -Wextra + +BINARIES = dhclient +MANPAGES5 = dhclient.conf.5 +MANPAGES8 = dhclient.8 + +all: $(BINARIES) + +.PHONY: all install clean + +install: all + mkdir -p $(DESTDIR)$(SBINDIR) + install $(BINARIES) $(DESTDIR)$(SBINDIR) + mkdir -p $(DESTDIR)$(MANDIR)/man5 + install $(MANPAGES5) $(DESTDIR)$(MANDIR)/man5 + mkdir -p $(DESTDIR)$(MANDIR)/man8 + install $(MANPAGES8) $(DESTDIR)$(MANDIR)/man8 + +%: %.c + $(CC) -std=gnu11 $(CFLAGS) $(CPPFLAGS) $< -o $@ + +clean: + rm -f $(BINARIES) diff --git a/dhclient/dhclient.8 b/dhclient/dhclient.8 new file mode 100644 index 00000000..728e7680 --- /dev/null +++ b/dhclient/dhclient.8 @@ -0,0 +1,165 @@ +.Dd January 15, 2023 +.Dt DHCLIENT 8 +.Os +.Sh NAME +.Nm dhclient +.Nd dynamic host configuration protocol client +.Sh SYNOPSIS +.Nm +.Op Fl f Ar config +.Op Fl t +.Ar interface +.Sh DESCRIPTION +.Nm +dynamically configures the specified network +.Ar interface +.Xr ( if 4 ) +and the Domain Name System (DNS) server list +via the Dynamic Host Configuration Protocol (DHCP) with settings obtained from +the local network's DHCP server. +.Pp +.Nm +is configured in +.Xr dhclient.conf 5 . +Each network setting can be configured to be automatically obtained from the +network (default), set manually, or not set at all. +If the interface is fully manually configured, +.Nm +sets the configuration and exits immediately. +.Pp +.Nm +broadcasts a request and selects a DHCP server to obtain the network +configuration from. +.Nm +leases an internet address as needed and remains running to occasionally renew +the release. +.Pp +The options are as follows: +.Bl -tag -width "12345678" +.It Fl f Ar config +Load the configuration from the +.Ar config +file instead as a shared configuration file instead of searching for it. +.It Fl t +Test the configuration is valid without configuring the network interface. +The +.Ar interface +can optionally be specified to search for its configuration file. +Exit 0 if the configuration is valid and non-zero otherwise. +.El +.Pp +Port 68 on the network interface is bound to send and receive DHCP messages. +.Nm +exits 0 immediately if the port is already bound to avoid interfering with other +.Nm +processes. +.Pp +.Nm +supports the Ethernet link layer +.Xr ether 4 +with the Internet Protocol network layer +.Xr ip 4 . +The loopback interface +.Xr lo 4 +does not need to be configured and +.Nm +immediately exits successfully. +.Pp +Network interfaces can be manually configured using +.Xr ifconfig 8 +and the DNS servers using +.Xr dnsconfig 8 . +.Nm +will occasionally reset the configuration when the internet address lease is +renewed unless disabled. +.Pp +.Nm +is typically automatically started as the +.Sy dhclient +daemon, which is depended on by the +.Sy network +daemon per the +.Xr init 5 +configuration. +.Sh IMPLEMENTATION NOTES +.Nm +signals readiness when it has attempted to configure the network interface and +continues retrying afterwards. +The first attempt fails if the network interface's link doesn't come up within +10 seconds. +Packets are transmitted 6 times with an exponential backoff before giving up +and the configuration restarts. +.Sh FILES +.Bl -tag -width "/etc/dhclient.${mac}.conf" -compact +.It Pa /etc/dhclient.${mac}.conf +The preferred +.Xr dhclient.conf 5 +file for the interface with the hardware address +.Ar mac . +.It Pa /etc/dhclient.${if}.conf +The second priority +.Xr dhclient.conf 5 +file for the interface with the device name +.Ar if . +.It Pa /etc/dhclient.conf +The lowest priority +.Xr dhclient.conf 5 +file shared across all network interfaces. +.El +.Sh ASYNCHRONOUS EVENTS +.Bl -tag -width "SIGUSR1" +.It Dv SIGTERM +Request daemon termination. +.El +.Sh EXIT STATUS +.Nm +runs as a +.Xr daemon 7 +until stopped by +.Dv SIGTERM . +.Nm +signals readiness on the +.Ev READYFD +file descriptor when the interface is configured, when waiting for the link to +come up times out, or when the DHCP server doesn't respond in time or +configuration fails. +.Nm +will exit 0 if the network interface doesn't need to be configured or is fully +manually configured and will exit non-zero on any fatal startup error. +.Sh EXAMPLES +See +.Xr dhclient.conf 5 +for example configurations. +.Pp +See +.Xr init 5 +for an example disabling the +.Sy dhclient +daemon by removing the +.Sy network +daemon's dependency on it. +.Sh SEE ALSO +.Xr ether 4 , +.Xr if 4 , +.Xr inet 4 , +.Xr ip 4 , +.Xr dnsconfig 8 , +.Xr ifconfig 8 , +.Xr init 8 +.Sh STANDARDS +.Rs +.%A R. Droms +.%D March 1997 +.%R RFC 2131 +.%T Dynamic Host Configuration Protocol +.%Q Network Working Group +.Re +.Pp +.Rs +.%A S. Alexander +.%A R. Droms +.%D March 1997 +.%R RFC 2132 +.%T DHCP Options and BOOTP Vendor Extensions +.%Q Network Working Group +.Re diff --git a/dhclient/dhclient.c b/dhclient/dhclient.c new file mode 100644 index 00000000..6f54b6c4 --- /dev/null +++ b/dhclient/dhclient.c @@ -0,0 +1,1602 @@ +/* + * 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 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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(ðer_hwaddr, addr_string) ) + errx(1, "%s:%ji: Invalid ethernet address: %s", + config_file->path, (intmax_t) config_file->line_number, + addr_string); + return !memcmp(ðer_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 \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, ðer_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, ðer_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); + } +} diff --git a/dhclient/dhclient.conf.5 b/dhclient/dhclient.conf.5 new file mode 100644 index 00000000..4cee4c52 --- /dev/null +++ b/dhclient/dhclient.conf.5 @@ -0,0 +1,183 @@ +.Dd January 16, 2023 +.Dt DHCLIENT.CONF 5 +.Os +.Sh NAME +.Nm dhclient.conf +.Nd dhcp client configuration +.Sh SYNOPSIS +.Nm /etc/dhclient.${mac}.conf +.Nm /etc/dhclient.${if}.conf +.Nm /etc/dhclient.conf +.Sh DESCRIPTION +.Xr dhclient 8 +configures the network interface according to its +.Nm +configuration file, searching for the first file path to exist: +.Pp +.Bl -bullet -compact +.It +.Pa /etc/dhclient.${mac}.conf +- where +.Ar mac +is the network interface's hardware address. +(Example: +.Pa dhclient.00:00:5e:00:53:00.conf ) +.It +.Pa /etc/dhclient.${if}.conf +- where +.Ar if +is the network interface's name. +(Example: +.Pa dhclient.if0.conf ) +.It +.Pa /etc/dhclient.conf +- the shared configuration file. +.El +.Pp +Network interface names are not guaranteed to be stable and it's recommended to +use the hardware address in the file name or in the +.Sy if +statement in the shared file. +.Sh FORMAT +.Nm +contains a series of whitespace delimited tokens akin to an +.Xr ifconfig 8 +invocation. +The +.Sq # +character begins a comment and the rest of the line is ignored. +If a token names a protocol, then that protocol is selected. +If a token names a configuration inside the current protocol or a top level +statement, then that configuration is set to the value of the subsequent +token. +.Pp +Configurations can be generally be set to either a manual value, or +.Sy auto +to obtain the value from the DHCP server, or +.Sy none +to disable setting the configuration and retain the currently set value. +If +.Sy auto +or +.Sy none +follows a protocol name, then all the configurations inside the protocol is set +to that value. +.Pp +The top level statements are: +.Bl -tag -width "12345678" +.It Sy if Oo Ar name "|" Sy etherhw: Ns Ar mac "|" Sy id: Ns Ar linkid Oc +The following configurations only applies to the network interface if it +matches by its +.Ar name , +its hardware address +.Ar mac , +or its unstable index number +.Ar linkid . +The +.Sy if +statement can only be used in the shared +.Pa /etc/dhclient.conf +file and allows defining a section for each network interface. +Configurations are applied to all network interfaces until the +.Sy if +statement appears. +.El +.Pp +The +.Sy inet +protocol contains the following configurations: +.Bl -tag -width "12345678" +.It Sy address Oo Ar ip "|" Sy auto "|" Sy none Oc +The local address in +.Xr inet 4 +address notation. +(Default: +.Sy auto ) +.It Sy router Oo Ar ip "|" Sy auto "|" Sy none Oc +The default route in +.Xr inet 4 +address notation. +(Default: +.Sy auto ) +.It Sy subnet Oo Ar ip "|" Sy auto "|" Sy none Oc +The subnet mask in +.Xr inet 4 +address notation. +(Default: +.Sy auto ) +.El +.Pp +The +.Sy dns +protocol contains the following configurations: +.Bl -tag -width "12345678" +.It Sy servers Oo Ar ip1,ip2,... "|" Sy auto "|" Sy none Oc +The comma separated list of DNS servers in +.Xr inet 4 +address notation. +A singular comma means the empty list. +(Default: +.Sy auto ) +.El +.Pp +The +.Sy ether +protocol contains the following configurations: +.Bl -tag -width "12345678" +.It Sy address Oo Ar mac "|" Sy auto "|" Sy none Oc +The local address in +.Xr ether 4 +address notation. +.Sy auto +sets it to the hardware address. +(Default: +.Sy auto ) +.El +.Sh EXAMPLES +.Ss Manually configuring an interface +To fully manually configure the network interface +.Pa if0 , +even if the network interface's device name changes over time, first first look +up its ethernet hardware address: +.Bd -literal -offset indent +$ ifconfig -l if0 ether hwaddress +00:00:5e:00:53:00 +.Ed +.Pp +Then write an +.Pa /etc/dhclient.conf +section using the +.Sy if +statement: +.Bd -literal -offset indent +if etherhw:00:00:5e:00:53:00 +inet address 192.0.2.4 router 192.0.2.1 subnet 255.255.255.0 +dns servers 192.0.2.2 192.0.2.3 +.Ed +.Ss Obtaining the DNS servers on only one interface +The DNS server list is global and a system with multiple network interfaces can +obtain the list on only the preferred interface by disabling DNS configuration +and enabling it on the desired interface: +.Bd -literal -offset indent +dns none +if etherhw:00:00:5e:00:53:00 +dns auto +.Ed +.Ss Disabling dhclient +.Xr dhclient 8 +can be disabled on an interface by disabling configuration of the inet and dns +protocols: +.Bd -literal -offset indent +inet none +dns none +.Ed +.Sh SEE ALSO +.Xr ether 4 , +.Xr if 4 , +.Xr inet 4 , +.Xr dhclient 8 , +.Xr dnsconfig 8 , +.Xr ifconfig 8 +.Sh CAVEATS +The list of DNS servers is global and should only be obtained on one network +interface to avoid interference. diff --git a/dnsconfig/dnsconfig.8 b/dnsconfig/dnsconfig.8 index f6557dd4..c0dc18a5 100644 --- a/dnsconfig/dnsconfig.8 +++ b/dnsconfig/dnsconfig.8 @@ -77,6 +77,7 @@ Delete a resolver: .Xr getdnsconfig 2 , .Xr setdnsconfig 2 , .Xr inet 4 , +.Xr dhclient 8 , .Xr ifconfig 8 .Sh HISTORY .Nm @@ -86,3 +87,6 @@ The kernel DNS resolver list is global state. Changes made with .Nm may be overwritten by other programs. +In particular +.Xr dhclient 8 +sets the resolver list automatically unless otherwise configured. diff --git a/ifconfig/ifconfig.8 b/ifconfig/ifconfig.8 index dae58e0e..6b58bd9a 100644 --- a/ifconfig/ifconfig.8 +++ b/ifconfig/ifconfig.8 @@ -215,4 +215,19 @@ if0 .Xr if 4 , .Xr inet 4 , .Xr lo 4 , +.Xr dhclient 8 , .Xr dnsconfig 8 +.Sh CAVEATS +.Xr ether 4 +network interfaces with the +.Xr inet 4 +protocol are commonly automatically configured by +.Xr dhclient 8 . +Disable +.Xr dhclient 8 +before manually configuring +.Xr inet 4 +on such interfaces to avoid the manual configuration being overwritten, or +configure +.Xr dhclient 8 +to set your desired network interface configuration in the first place. diff --git a/libc/sys/dnsconfig/getdnsconfig.2 b/libc/sys/dnsconfig/getdnsconfig.2 index 2b3196de..b50d7c03 100644 --- a/libc/sys/dnsconfig/getdnsconfig.2 +++ b/libc/sys/dnsconfig/getdnsconfig.2 @@ -120,6 +120,7 @@ does not match the size of the address corresponding to .Sh SEE ALSO .Xr inet 4 , .Xr inet6 4 , +.Xr dhclient 8 , .Xr dnsconfig 8 .Sh HISTORY The diff --git a/share/init/dhclient b/share/init/dhclient new file mode 100644 index 00000000..16bfa2d2 --- /dev/null +++ b/share/init/dhclient @@ -0,0 +1,2 @@ +per if +exec dhclient diff --git a/share/init/network b/share/init/network index e69de29b..792ae583 100644 --- a/share/init/network +++ b/share/init/network @@ -0,0 +1 @@ +require dhclient diff --git a/share/man/man4/if.4 b/share/man/man4/if.4 index 960b1a7f..ec521291 100644 --- a/share/man/man4/if.4 +++ b/share/man/man4/if.4 @@ -338,6 +338,7 @@ temporarily fail with .Xr ip 4 , .Xr lo 4 , .Xr kernel 7 , +.Xr dhclient 8 , .Xr ifconfig 8 .Sh STANDARDS .St -p1003.1-2008 diff --git a/share/man/man5/init.5 b/share/man/man5/init.5 index c0be9f0f..c3046b22 100644 --- a/share/man/man5/init.5 +++ b/share/man/man5/init.5 @@ -153,6 +153,11 @@ It depends on the and .Sy time daemons. +.It Sy dhclient +Daemon that starts +.Xr dhclient 8 +on each network interface and becomes ready when each network interface has +been attempted configured. .It Sy local Virtual daemon that starts daemons pertinent to the local system. The system provides a default implementation that does nothing. @@ -163,6 +168,9 @@ system. .It Sy network Virtual daemon that becomes ready when an attempt has been made to establish network connectivity. +It depends on the +.Sy dhclient +deamon. Daemons can depend on this daemon if they need the network to have been established before they start. .It Sy time @@ -525,6 +533,23 @@ The .Sy optional flag should only be omitted if a local daemon is critical and the boot should fail if the daemon fails. +.Ss Disable network auto-configuration (DHCP) +The +.Sy network +daemon depends by default on +.Sy dhclient , +which does DHCP configuration of the network. +This dependency can be removed by creating +.Pa /etc/init/network +with the following contents: +.Bd -literal +furthermore +unset require dhclient +.Ed +.Pp +This example extends the existing configuration in +.Pa /share/init/network +by removing a dependency. .Ss Creating a new daemon The .Sy exampled diff --git a/share/man/man7/installation.7 b/share/man/man7/installation.7 index d897eceb..4f9b82a6 100644 --- a/share/man/man7/installation.7 +++ b/share/man/man7/installation.7 @@ -82,7 +82,8 @@ per the instructions in The release modification procedure lets you customize aspects such as the default bootloader menu option and timeout, the default hostname, the default keyboard layout, the default graphics resolution, adding files of your choice to -the live environment, control which drivers are loaded by default, and so on. +the live environment, control which drivers are loaded by default, control which +live environment daemons are started by default, and so on. .Pp Warning: The live environment does not come with any random entropy and entropy gathering is not yet implemented. @@ -143,6 +144,29 @@ Only the selected ports are loaded into the live environment and installed onto the new installation. If upgrading an existing installation, then any ports not loaded will be removed from the installation being upgraded. +.Pp +Ports can additionally be loaded as binary packages in the +.Pa /repository +directory by navigating to the advanced menu and then the select binary packages +submenu and then selecting which ports. +.Pp +The network drivers can be disabled by navigating to the advanced menu and +selecting +.Sy Disable network drivers . +It can be useful to disable the network drivers if it's undesirable to put the +system on the network for security reasons. +You can disable network drivers by default by editing the bootloader +configuration as described below after completing the installation. +.Pp +By default +.Xr dhclient 8 +will automatically configure +.Xr ether 4 +network interfaces with DHCP and bring up network connectivity. +The DHCP client can be disabled by navigating to the advanced menu and selecting +.Sy Disable DHCP client , +which is useful if you want to manually configure the network or not expose the +system until you are ready. .Ss Installer This guide assumes you selected the operating system installation option in the bootloader. diff --git a/share/man/man7/release-iso-bootconfig.7 b/share/man/man7/release-iso-bootconfig.7 index d592d2ea..a44078f5 100644 --- a/share/man/man7/release-iso-bootconfig.7 +++ b/share/man/man7/release-iso-bootconfig.7 @@ -262,6 +262,11 @@ If the selected menu option itself is a submenu, it can be appended with a .Sy '>' and another selection to pick a default menu option in that submenu, and so on. (Default: 0) +.It Sy enable_dhclient +Whether to start the +.Xr dhclient 8 +daemon. +(Default: true) .It Sy enable_network_drivers An additional .Xr kernel 7 diff --git a/share/man/man7/release-iso-modification.7 b/share/man/man7/release-iso-modification.7 index 13b389a4..06ed566b 100644 --- a/share/man/man7/release-iso-modification.7 +++ b/share/man/man7/release-iso-modification.7 @@ -16,7 +16,8 @@ configuration as described in section 5 of the manual. The release modification procedure lets you customize aspects such as the default bootloader menu option and timeout, the default hostname, the default keyboard layout, the default graphics resolution, adding files of your choice to -the live environment, control which drivers are loaded by default, and so on. +the live environment, control which drivers are loaded by default, control which +live environment daemons are started by default, and so on. .Ss Prerequisites .Bl -bullet -compact .It @@ -397,6 +398,16 @@ security reasons or to work around driver issues: tix-iso-bootconfig --disable-network-drivers bootconfig tix-iso-add sortix.iso bootconfig .Ed +.Ss Disable DHCP Auto-Configuration By Default +To customize a release so +.Xr dhclient 8 +doesn't automatically configure network interfaces using DHCP, useful if one +wants to manually configure network interfaces with +.Xr ifconfig 8 . +.Bd -literal +tix-iso-bootconfig --disable-dhclient bootconfig +tix-iso-add sortix.iso bootconfig +.Ed .Sh SEE ALSO .Xr xorriso 1 , .Xr development 7 , diff --git a/share/man/man7/upgrade.7 b/share/man/man7/upgrade.7 index ed28b54e..f9181efd 100644 --- a/share/man/man7/upgrade.7 +++ b/share/man/man7/upgrade.7 @@ -29,7 +29,8 @@ per the instructions in The release modification procedure lets you customize aspects such as the default bootloader menu option and timeout, the default hostname, the default keyboard layout, the default graphics resolution, adding files of your choice to -the live environment, control which drivers are loaded by default, and so on. +the live environment, control which drivers are loaded by default, control which +live environment daemons are started by default, and so on. .Pp Warning: The live environment does not come with any random entropy and entropy gathering is not yet implemented. diff --git a/tix/tix-iso-bootconfig b/tix/tix-iso-bootconfig index 53e2eb25..71f0be9b 100755 --- a/tix/tix-iso-bootconfig +++ b/tix/tix-iso-bootconfig @@ -22,6 +22,7 @@ append_title="modified by $(id -un)@$(hostname)" default= directory= enable_append_title=true +enable_dhclient= enable_network_drivers= enable_src= init_target= @@ -52,9 +53,11 @@ for argument do --default=*) default=$parameter ;; --default) previous_option=default ;; --disable-append-title) enable_append_title=false ;; + --disable-dhclient) enable_dhclient=false ;; --disable-network-drivers) enable_network_drivers=false ;; --disable-src) enable_src=false ;; --enable-append-title) enable_append_title=true ;; + --enable-dhclient) enable_dhclient=true ;; --enable-network-drivers) enable_network_drivers=true ;; --enable-src) enable_src=true ;; --init-target=*) init_target=$parameter ;; @@ -138,6 +141,7 @@ mkdir -p -- "$directory/boot/grub" if [ -n "$timeout" ]; then printf 'timeout="%s"\n' "$timeout" fi + print_enable_default_bool "$enable_dhclient" dhclient dhclient print_enable_default "$enable_network_drivers" network_drivers network-drivers print_enable_default_bool "$enable_src" src src if $enable_append_title; then diff --git a/tix/tix-iso-bootconfig.8 b/tix/tix-iso-bootconfig.8 index 40dc4a0f..b40184d6 100644 --- a/tix/tix-iso-bootconfig.8 +++ b/tix/tix-iso-bootconfig.8 @@ -9,9 +9,11 @@ .Op Fl \-append-title Ns = Ns Ar text .Op Fl \-default Ns = Ns Ar default-boot-menu-option .Op Fl \-disable-append-title +.Op Fl \-disable-dhclient .Op Fl \-disable-network-drivers .Op Fl \-disable-src .Op Fl \-enable-append-title +.Op Fl \-enable-dhclient .Op Fl \-enable-network-drivers .Op Fl \-enable-src .Op Fl \-init-target Ns = Ns Ar target @@ -91,6 +93,14 @@ GRUB variable. Don't append anything to the bootloader menu title by appending to the .Sy base_menu_title GRUB variable. +.It Fl \-disable-dhclient +Disable automatic DHCP configuration by setting the +.Sy enable_dhclient +GRUB variable to +.Sy false , +causing the bootloader to load additional configuration that turns off the +.Xr dhclient 8 +daemon on boot. .It Fl \-disable-network-drivers Disable network drivers by setting the .Sy enable_network_drivers @@ -114,6 +124,14 @@ to the bootloader menu title by appending to the GRUB variable. This option is on by default and can be disabled with .Fl \-disable-append-title . +.It Fl \-enable-dhclient +Enable automatic DHCP configuration by setting the +.Sy enable_dhclient +GRUB variable to +.Sy true , +selecting the default behavior of starting the +.Xr dhclient 8 +daemon. .It Fl \-enable-network-drivers Enable network drivers by setting the .Sy enable_network_drivers @@ -271,6 +289,16 @@ security reasons or to work around driver issues: tix-iso-bootconfig --disable-network-drivers bootconfig tix-iso-add sortix.iso bootconfig .Ed +.Ss Disable DHCP Auto-Configuration By Default +To customize a release so +.Xr dhclient 8 +doesn't automatically configure network interfaces using DHCP, useful if one +wants to manually configure network interfaces with +.Xr ifconfig 8 . +.Bd -literal +tix-iso-bootconfig --disable-dhclient bootconfig +tix-iso-add sortix.iso bootconfig +.Ed .Sh SEE ALSO .Xr xorriso 1 , .Xr kernel 7 ,