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