/* libdhcp/dhcp_client.c - A DHCP client * * Copyright (C) 2024 Luke T. Shumaker * SPDX-License-Identifier: AGPL-3.0-or-later * * ----------------------------------------------------------------------------- * https://github.com/Wiznet/ioLibrary_Driver/blob/b981401e7f3d07015619adf44c13998e13e777f9/Internet/DHCP/dhcp.c * * Copyright (c) 2013, WIZnet Co., LTD. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. * * SPDX-License-Identifier: BSD-3-Clause * * ----------------------------------------------------------------------------- * https://github.com/Wiznet/ioLibrary_Driver/blob/b981401e7f3d07015619adf44c13998e13e777f9/license.txt * * Copyright (c) 2014 WIZnet Co.,Ltd. * Copyright (c) WIZnet ioLibrary Project. * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * * SPDX-License-Identifier: MIT */ /* IPv4: * - DHCPv4: https://datatracker.ietf.org/doc/html/rfc2131 * - Auto-IP: https://datatracker.ietf.org/doc/html/rfc3927 * - DNAv4: https://datatracker.ietf.org/doc/html/rfc4436 * * This implmementation does NOT support RFC 4361 and thus cannot * support RFC 6842. * * - DHCPv4 node-specific client identifiers: https://datatracker.ietf.org/doc/html/rfc4361 * - DHCPv4 client identifiers in replies: https://datatracker.ietf.org/doc/html/rfc6842 * * This implementation does NOT support split options (originally RFC * 2131, but clarified in RFC 3396). * * - DHCPv4 long options: https://datatracker.ietf.org/doc/html/rfc3396 * * IPv6: * - SLAAC: https://datatracker.ietf.org/doc/html/rfc2462 * - DNAv6: https://datatracker.ietf.org/doc/html/rfc6059 * * Common: * - NetBIOS name resolution: https://datatracker.ietf.org/doc/html/rfc1001 * https://datatracker.ietf.org/doc/html/rfc1002 * - LLMNR: https://datatracker.ietf.org/doc/html/rfc4795 * - mDNS: https://datatracker.ietf.org/doc/html/rfc6762 */ #include /* for strlen(), memcpy(), memset() */ #include /* for random() */ #include #include #include #include "dhcp_common.h" /* Config *********************************************************************/ #include "config.h" #ifndef CONFIG_DHCP_DEBUG #define CONFIG_DHCP_DEBUG 1 #endif #ifndef CONFIG_DHCP_SELECTING_NS #define CONFIG_DHCP_SELECTING_NS (5*NS_PER_S) #endif #ifndef CONFIG_DHCP_CAN_RECV_UNICAST_IP_WITHOUT_IP #define CONFIG_DHCP_CAN_RECV_UNICAST_IP_WITHOUT_IP false #endif /* Implementation *************************************************************/ #if CONFIG_DHCP_DEBUG #include #define debugf(fmt, ...) printf(fmt "\n" __VA_OPT__(,) __VA_ARGS__) #else #define debugf(fmt, ...) ((void)0) #endif enum requirement { MUST, MUST_NOT, SHOULD, SHOULD_NOT, MAY, _SHOULD_NOT_HAPPEN, }; struct dhcp_client { /* Static. */ implements_net_iface *iface; implements_net_packet_conn *sock; struct net_eth_addr self_eth_addr; char self_hostname[63]; /* Mutable. */ enum { STATE_INIT, STATE_SELECTING, STATE_REQUESTING, STATE_BOUND, STATE_RENEWING, STATE_REBINDING, STATE_INIT_REBOOT, STATE_REBOOTING, } state; uint8_t last_sent_msgtyp; /* Lifetime of xid: * 1. initial : [INIT]->DHCPDISCOVER->DHCPOFFER->DHCPREQUEST-+->DHCPNAK-------------->[INIT] * |->DHCPACK->DHCPDECLINE->[INIT] * `->DHCPACK-------------->[BOUND] * 2. renew/rebind : [RENEWING/REBINDING]->DHCPREQUEST-+->DHCPNAK->[INIT] * `->DHCPACK->[BOUND] */ uint32_t xid; uint64_t time_ns_init; struct net_ip4_addr lease_client_addr; /* .yiaddr */ struct net_ip4_addr lease_server_id; /* .options[DHCP_OPT_DHCP_SERVER_ID] */ uint64_t lease_time_ns_t1; /* .options[DHCP_OPT_RENEWAL_TIME] + time_ns_init */ uint64_t lease_time_ns_t2; /* .options[DHCP_OPT_REBINDING_TIME] + time_ns_init */ uint64_t lease_time_ns_end; /* .options[DHCP_OPT_ADDRESS_TIME] + time_ns_init */ struct { struct net_ip4_addr subnet_mask; /* .options[DHCP_OPT_SUBNET_MASK] */ struct net_ip4_addr gateway_addr; /* .options[DHCP_OPT_ROUTER] */ } lease_auxdata; }; /** * @param client->state * @param client->self_eth_addr * @param client->xid * @param client->time_ns_init * @param client->lease_client_addr (sometimes) * @param client->lease_server_id (sometimes) * @param client->sock * * @return client->last_sent_msgtyp * @return whether there was an error */ static bool dhcp_client_send(struct dhcp_client *client, uint8_t msgtyp, char *errstr) { /**********************************************************************\ * Preconditions * \**********************************************************************/ assert(client); assert(msgtyp == DHCP_MSGTYP_DISCOVER || msgtyp == DHCP_MSGTYP_INFORM || msgtyp == DHCP_MSGTYP_REQUEST || msgtyp == DHCP_MSGTYP_DECLINE || msgtyp == DHCP_MSGTYP_RELEASE ); if (msgtyp == DHCP_MSGTYP_REQUEST) assert(client->state == STATE_SELECTING || /* initial selection */ client->state == STATE_INIT_REBOOT || /* variant initial selection */ client->state == STATE_BOUND || /* T1 expired, start renew */ client->state == STATE_RENEWING ); /* T2 expired, start rebind */ #define IMPOSSIBLE_REQUEST_STATES \ STATE_INIT: \ case STATE_REQUESTING: \ case STATE_REBINDING: \ case STATE_REBOOTING /**********************************************************************\ * Setup * \**********************************************************************/ bool server_broadcasts, client_broadcasts; server_broadcasts = !CONFIG_DHCP_CAN_RECV_UNICAST_IP_WITHOUT_IP; if (msgtyp == DHCP_MSGTYP_REQUEST && (client->state == STATE_BOUND || client->state == STATE_RENEWING)) server_broadcasts = false; /* https://datatracker.ietf.org/doc/html/rfc2131#section-4.4.4 */ switch (msgtyp) { case DHCP_MSGTYP_DISCOVER: client_broadcasts = true; break; case DHCP_MSGTYP_INFORM: client_broadcasts = true; break; /* may unicast if it knows the server */ case DHCP_MSGTYP_REQUEST: client_broadcasts = client->state != STATE_BOUND; break; case DHCP_MSGTYP_DECLINE: client_broadcasts = true; break; case DHCP_MSGTYP_RELEASE: client_broadcasts = false; break; default: assert_notreached("invalid message type for client to send"); } /**********************************************************************\ * Build the message * \**********************************************************************/ struct dhcp_msg msg = {0}; size_t optlen = 0; /* Base structure. * https://datatracker.ietf.org/doc/html/rfc2131#page-37 */ msg.op = DHCP_OP_BOOTREQUEST; msg.htype = DHCP_HTYPE_ETHERNET; msg.hlen = sizeof(client->self_eth_addr); msg.hops = 0; /* relays increment this when they forward it along */ msg.xid = uint32be_marshal(client->xid); msg.secs = uint16be_marshal((VCALL(bootclock, get_time_ns) - client->time_ns_init)/NS_PER_S); switch (msgtyp) { case DHCP_MSGTYP_DISCOVER: case DHCP_MSGTYP_INFORM: case DHCP_MSGTYP_REQUEST: msg.flags = uint16be_marshal(server_broadcasts ? DHCP_FLAG_BROADCAST : 0); break; case DHCP_MSGTYP_DECLINE: case DHCP_MSGTYP_RELEASE: msg.flags = uint16be_marshal(0); break; } switch (msgtyp) { case DHCP_MSGTYP_DISCOVER: msg.ciaddr = net_ip4_addr_zero; break; case DHCP_MSGTYP_INFORM: msg.ciaddr = client->lease_client_addr; break; case DHCP_MSGTYP_REQUEST: switch (client->state) { case STATE_SELECTING: case STATE_INIT_REBOOT: msg.ciaddr = net_ip4_addr_zero; break; case STATE_BOUND: case STATE_RENEWING: /* case STATE_REBINDING: */ /* RFC 2131 includes "REBINDING" here, but a * DHCPREQUEST is never sent in the REBINDING * state. */ msg.ciaddr = client->lease_client_addr; break; case IMPOSSIBLE_REQUEST_STATES: assert_notreached("invalid client state for sending DHCPREQUEST"); default: assert_notreached("invalid client state"); } break; case DHCP_MSGTYP_DECLINE: msg.ciaddr = net_ip4_addr_zero; break; case DHCP_MSGTYP_RELEASE: msg.ciaddr = client->lease_client_addr; break; } msg.yiaddr = net_ip4_addr_zero; /* only set by servers */ msg.siaddr = net_ip4_addr_zero; /* only set by servers */ msg.giaddr = net_ip4_addr_zero; /* only set by relays */ memcpy(msg.chaddr, client->self_eth_addr.octets, sizeof(client->self_eth_addr)); /* msg.sname = "options, if indicated in 'sname/file' option'; otherwise unused"; */ /* msg.file = "options, if indicated in 'sname/file' option'; otherwise unused"; */ /* Magic cookie. * https://datatracker.ietf.org/doc/html/rfc2131#section-4.1 */ msg.options[optlen++] = dhcp_magic_cookie[0]; msg.options[optlen++] = dhcp_magic_cookie[1]; msg.options[optlen++] = dhcp_magic_cookie[2]; msg.options[optlen++] = dhcp_magic_cookie[3]; /* Options. */ static uint8_t parameter_request_list[] = { DHCP_OPT_SUBNET_MASK, DHCP_OPT_ROUTER, DHCP_OPT_RENEWAL_TIME, DHCP_OPT_REBINDING_TIME, }; const struct { bool have_cols; enum requirement cols[5]; size_t val_len; void *val_ptr; } options[0x100] = { #define V(x) sizeof(x), &(x) #define NONE 0, NULL /* Encode the table from * https://datatracker.ietf.org/doc/html/rfc2131#page-38 */ #define INC_ADDR ({ \ enum requirement req; \ switch (client->state) { \ case STATE_SELECTING: case STATE_INIT_REBOOT: req = MUST; break; \ case STATE_BOUND: case STATE_RENEWING: req = MUST_NOT; break; \ case IMPOSSIBLE_REQUEST_STATES: default: req = _SHOULD_NOT_HAPPEN; \ } \ req; \ }) #define INC_SERVER ({ \ enum requirement req; \ switch (client->state) { \ case STATE_SELECTING: req = MUST; break; \ case STATE_INIT_REBOOT: case STATE_BOUND: case STATE_RENEWING: req = MUST_NOT; break; \ case IMPOSSIBLE_REQUEST_STATES: default: req = _SHOULD_NOT_HAPPEN; \ } \ req; \ }) /* Option DISCOVER INFORM REQUEST DECLINE RELEASE */ /* ------ ---------- ---------- ---------- -------- -------- */ [DHCP_OPT_ADDRESS_REQUEST] = { 1, { MAY, MUST_NOT, INC_ADDR, MUST, MUST_NOT }, V(client->lease_client_addr) }, [DHCP_OPT_ADDRESS_TIME] = { 1, { MAY, MUST_NOT, MAY, MUST_NOT, MUST_NOT }, NONE }, [DHCP_OPT_OVERLOAD] = { 1, { MAY, MAY, MAY, MAY, MAY }, NONE }, [DHCP_OPT_DHCP_MSG_TYPE] = { 1, { MUST, MUST, MUST, MUST, MUST }, V(msgtyp) }, [DHCP_OPT_CLIENT_ID] = { 1, { MAY, MAY, MAY, MAY, MAY }, NONE }, [DHCP_OPT_CLASS_ID] = { 1, { MAY, MAY, MAY, MUST_NOT, MUST_NOT }, NONE }, [DHCP_OPT_DHCP_SERVER_ID] = { 1, { MUST_NOT, MUST_NOT, INC_SERVER, MUST, MUST }, V(client->lease_server_id) }, [DHCP_OPT_PARAMETER_LIST] = { 1, { MAY, MAY, MAY, MUST_NOT, MUST_NOT }, V(parameter_request_list) }, [DHCP_OPT_DHCP_MAX_MSG_SIZE] = { 1, { MAY, MAY, MAY, MUST_NOT, MUST_NOT }, CONFIG_DHCP_OPT_SIZE > DHCP_MSG_MIN_MAX_OPT_SIZE ? 2 : 0, CONFIG_DHCP_OPT_SIZE > DHCP_MSG_MIN_MAX_OPT_SIZE ? &(struct{uint16be_t;}){uint16be_marshal(/*IP*/20+/*UDP*/8+sizeof(msg))} : NULL }, [DHCP_OPT_DHCP_MESSAGE] = { 1, { SHOULD_NOT, SHOULD_NOT, SHOULD_NOT, SHOULD, SHOULD }, errstr ? strlen(errstr) : 0, errstr }, [0]/* All others */ = { 0, { MAY, MAY, MAY, MUST_NOT, MUST_NOT }, NONE }, [DHCP_OPT_HOSTNAME] = { 0, {0}, strnlen(client->self_hostname, sizeof(client->self_hostname)), client->self_hostname }, }; int col; switch (msgtyp) { case DHCP_MSGTYP_DISCOVER: col = 0; break; case DHCP_MSGTYP_INFORM: col = 1; break; case DHCP_MSGTYP_REQUEST: col = 2; break; case DHCP_MSGTYP_DECLINE: col = 3; break; case DHCP_MSGTYP_RELEASE: col = 4; break; default: assert_notreached("invalid message type for client to send"); } for (uint8_t opt = 1; opt < 255; opt++) { enum requirement req = options[opt].have_cols ? options[opt].cols[col] : options[0].cols[col]; switch (req) { case MUST_NOT: /* Do nothing. */ break; case MUST: assert(options[opt].val_len); /* fall-through */ case SHOULD: case SHOULD_NOT: case MAY: if (options[opt].val_len) { assert(options[opt].val_len <= UINT8_MAX); msg.options[optlen++] = opt; msg.options[optlen++] = options[opt].val_len; memcpy(&msg.options[optlen], options[opt].val_ptr, options[opt].val_len); optlen += options[opt].val_len; } break; case _SHOULD_NOT_HAPPEN: assert_notreached("bad table"); } } msg.options[optlen++] = DHCP_OPT_END; assert(optlen <= CONFIG_DHCP_OPT_SIZE); /**********************************************************************\ * Send * \**********************************************************************/ ssize_t r = VCALL(client->sock, sendto, &msg, DHCP_MSG_BASE_SIZE + optlen, client_broadcasts ? net_ip4_addr_broadcast : client->lease_server_id, DHCP_PORT_SERVER); if (r < 0) { debugf("error: sendto: %zd", r); return true; } client->last_sent_msgtyp = msgtyp; return false; } struct dhcp_recv_msg { struct dhcp_msg raw; struct { uint16_t len; uint8_t *dat; } options[0x100]; uint8_t _option_data[sizeof((struct dhcp_msg){}.options)+ sizeof((struct dhcp_msg){}.file)+ sizeof((struct dhcp_msg){}.sname)]; }; /** @return whether there is an error */ static inline bool _dhcp_client_recv_measure_opts(struct dhcp_recv_msg *ret, uint8_t *optoverload, uint8_t *dat, size_t len, bool require_pad) { for (size_t pos = 0, opt_len; pos < len; pos += opt_len) { uint8_t opt_typ = dat[pos++]; switch (opt_typ) { case DHCP_OPT_END: if (require_pad) while (pos < len) if (dat[pos++] != DHCP_OPT_PAD) return true; return false; case DHCP_OPT_PAD: opt_len = 0; break; default: if (pos == len) return true; opt_len = dat[pos++]; if (pos+opt_len > len) return true; ret->options[opt_typ].len += opt_len; if (opt_typ == DHCP_OPT_OVERLOAD && opt_len == 1) *optoverload = *optoverload | dat[pos]; } } return true; } static inline void _dhcp_client_recv_consolidate_opts(struct dhcp_recv_msg *ret, uint8_t *dat, size_t len) { for (size_t pos = 0, opt_len; pos < len; pos += opt_len) { uint8_t opt_typ = dat[pos++]; switch (opt_typ) { case DHCP_OPT_END: return; case DHCP_OPT_PAD: opt_len = 0; break; default: opt_len = dat[pos++]; memcpy(&ret->options[opt_typ].dat[ret->options[opt_typ].len], &dat[pos], opt_len); ret->options[opt_typ].len += opt_len; } } } /** * @param client->sock * @param client->self_eth_addr * @param client->xid * @param client->lease_server_id * * @return * - <0: -errno * - 0: success * - >0: should not happen */ static ssize_t dhcp_client_recv(struct dhcp_client *client, struct dhcp_recv_msg *ret) { struct net_ip4_addr srv_addr; uint16_t srv_port; ssize_t msg_len; assert(client); ignore: msg_len = VCALL(client->sock, recvfrom, &ret->raw, sizeof(ret->raw), &srv_addr, &srv_port); if (msg_len < 0) /* msg_len is -errno */ return msg_len; /* Validate L3: IP */ /* Don't validate that srv_addr matches client->server_id * because there may be a relay between us and the server, and * don't bother to track the relay IP either, because it may * change. */ /* Validate L4: UDP. */ if (srv_port != DHCP_PORT_SERVER) goto ignore; /* Validate L5: DHCP. */ if ((size_t)msg_len < DHCP_MSG_BASE_SIZE + sizeof(dhcp_magic_cookie)) /* ignore impossibly short message */ goto ignore; if ((size_t)msg_len > sizeof(ret->raw)) /* ignore message that is larger than the specified * DHCP_OPT_DHCP_MAX_MSG_SIZE */ goto ignore; if (ret->raw.op != DHCP_OP_BOOTREPLY) /* ignore non-replies */ goto ignore; if (memcmp(ret->raw.chaddr, client->self_eth_addr.octets, sizeof(client->self_eth_addr))) /* ignore messages that aren't to us */ goto ignore; if (uint32be_unmarshal(ret->raw.xid) != client->xid) /* ignore messages that aren't in response to what we've said */ goto ignore; if (memcmp(client->lease_server_id.octets, net_ip4_addr_zero.octets, 4)) if (memcmp(srv_addr.octets, client->lease_server_id.octets, 4)) /* ignore messages from the wrong server */ goto ignore; if (memcmp(ret->raw.options, dhcp_magic_cookie, sizeof(dhcp_magic_cookie))) /* ignore messages without options */ goto ignore; /* Consolidate split options into contiguous buffers. * * RFC 2131 and RFC 2132 specify this behavior, but are a bit * ambiguous about it; RFC 3396 provides clarification. * * "The aggregate option buffer is made up of the optional * parameters field, the file field, and the sname field, in * that order." -- RFC 3396 */ uint8_t optoverload = 0; memset(&ret->options, 0, sizeof(ret->options)); /* Size the buffers. */ if (_dhcp_client_recv_measure_opts(ret, &optoverload, &ret->raw.options[sizeof(dhcp_magic_cookie)], msg_len - (DHCP_MSG_BASE_SIZE + sizeof(dhcp_magic_cookie)), false)) goto ignore; if (optoverload & 1u) if (_dhcp_client_recv_measure_opts(ret, &optoverload, ret->raw.file, sizeof(ret->raw.file), true)) goto ignore; if (optoverload & 2u) if (_dhcp_client_recv_measure_opts(ret, &optoverload, ret->raw.sname, sizeof(ret->raw.sname), true)) goto ignore; /* Validate sizes, allocate buffers. */ for (uint8_t opt = 1, allocated = 0; opt < 255; opt++) { if (!ret->options[opt].len) continue; if (!dhcp_opt_length_is_valid(opt, ret->options[opt].len)) goto ignore; ret->options[opt].dat = &ret->_option_data[allocated]; allocated += ret->options[opt].len; ret->options[opt].len = 0; } /* Fill the buffers. */ _dhcp_client_recv_consolidate_opts(ret, &ret->raw.options[sizeof(dhcp_magic_cookie)], msg_len - (DHCP_MSG_BASE_SIZE + sizeof(dhcp_magic_cookie))); if (optoverload & 1u) _dhcp_client_recv_consolidate_opts(ret, ret->raw.file, sizeof(ret->raw.file)); if (optoverload & 2u) _dhcp_client_recv_consolidate_opts(ret, ret->raw.sname, sizeof(ret->raw.sname)); /* Validate presence of options. */ const struct { bool have_cols; enum requirement cols[3]; } option_req[0x100] = { /* Enocde the table from * https://datatracker.ietf.org/doc/html/rfc2131#page-29 */ #define REQ_TIME client->last_sent_msgtyp == DHCP_MSGTYP_REQUEST ? MUST : MUST_NOT /* Option DHCPOFFER DHCPACK DHCPNAK */ /* ------ -------- ---------- -------- */ [DHCP_OPT_ADDRESS_REQUEST] = { 1, { MUST_NOT, MUST_NOT, MUST_NOT } }, [DHCP_OPT_ADDRESS_TIME] = { 1, { MUST, REQ_TIME, MUST_NOT } }, [DHCP_OPT_OVERLOAD] = { 1, { MAY, MAY, MUST_NOT } }, [DHCP_OPT_DHCP_MSG_TYPE] = { 1, { MUST, MUST, MUST } }, [DHCP_OPT_PARAMETER_LIST] = { 1, { MUST_NOT, MUST_NOT, MUST_NOT } }, [DHCP_OPT_DHCP_MESSAGE] = { 1, { SHOULD, SHOULD, SHOULD } }, [DHCP_OPT_CLIENT_ID] = { 1, { MUST_NOT, MUST_NOT, MAY } }, [DHCP_OPT_CLASS_ID] = { 1, { MAY, MAY, MAY } }, [DHCP_OPT_DHCP_SERVER_ID] = { 1, { MUST, MUST, MUST } }, [DHCP_OPT_DHCP_MAX_MSG_SIZE] = { 1, { MUST_NOT, MUST_NOT, MUST_NOT } }, [0]/* All others */ = { 0, { MAY, MAY, MUST_NOT } }, }; if (!ret->options[DHCP_OPT_DHCP_MSG_TYPE].len) goto ignore; int col; switch (ret->options[DHCP_OPT_DHCP_MSG_TYPE].dat[0]) { case DHCP_MSGTYP_OFFER: col = 0; break; case DHCP_MSGTYP_ACK: col = 1; break; case DHCP_MSGTYP_NAK: col = 2; break; default: goto ignore; } for (uint8_t opt = 1; opt < 255; opt++) { enum requirement req = option_req[opt].have_cols ? option_req[opt].cols[col] : option_req[0].cols[col]; switch (req) { case MUST: if (!ret->options[opt].len) goto ignore; break; case MUST_NOT: if (ret->options[opt].len) goto ignore; break; case SHOULD: case SHOULD_NOT: case MAY: /* Do nothing. */ break; case _SHOULD_NOT_HAPPEN: assert_notreached("bad table"); } } return 0; } /** @return true if there's a conflict, false if the addr appears to be unused */ static bool dhcp_check_conflict(implements_net_packet_conn *sock, struct net_ip4_addr addr) { assert(sock); return VCALL(sock, sendto, "CHECK_IP_CONFLICT", 17, addr, 5000) != NET_ETIMEDOUT; } static void dhcp_client_take_lease(struct dhcp_client *client, struct dhcp_recv_msg *msg, bool ifup) { assert(client); assert(msg); client->lease_client_addr = msg->raw.yiaddr; client->lease_auxdata.subnet_mask = msg->options[DHCP_OPT_SUBNET_MASK].len ? *((struct net_ip4_addr *)msg->options[DHCP_OPT_SUBNET_MASK].dat) : net_ip4_addr_zero; client->lease_auxdata.gateway_addr = msg->options[DHCP_OPT_ROUTER].len ? *((struct net_ip4_addr *)msg->options[DHCP_OPT_SUBNET_MASK].dat) : net_ip4_addr_zero; client->lease_server_id = *((struct net_ip4_addr *)msg->options[DHCP_OPT_DHCP_SERVER_ID].dat); uint64_t dur_ns_end = ((uint64_t)uint32be_decode(msg->options[DHCP_OPT_ADDRESS_TIME].dat))*NS_PER_S; uint64_t dur_ns_t1 = msg->options[DHCP_OPT_RENEWAL_TIME].len ? ((uint64_t)uint32be_decode(msg->options[DHCP_OPT_RENEWAL_TIME].dat))*NS_PER_S : (dur_ns_end == DHCP_INFINITY * NS_PER_S) ? DHCP_INFINITY * NS_PER_S : dur_ns_end/2; uint64_t dur_ns_t2 = msg->options[DHCP_OPT_REBINDING_TIME].len ? ((uint64_t)uint32be_decode(msg->options[DHCP_OPT_RENEWAL_TIME].dat))*NS_PER_S : (dur_ns_end == DHCP_INFINITY * NS_PER_S) ? DHCP_INFINITY * NS_PER_S : (dur_ns_end*7)/8; /* 0.875 = 7/8 */ client->lease_time_ns_t1 = (dur_ns_t1 == DHCP_INFINITY * NS_PER_S) ? 0 : client->time_ns_init + dur_ns_t1; client->lease_time_ns_t2 = (dur_ns_t2 == DHCP_INFINITY * NS_PER_S) ? 0 : client->time_ns_init + dur_ns_t2; client->lease_time_ns_end = (dur_ns_end == DHCP_INFINITY * NS_PER_S) ? 0 : client->time_ns_init + dur_ns_end; if (ifup) VCALL(client->iface, ifup, (struct net_iface_config){ .addr = client->lease_client_addr, .gateway_addr = client->lease_auxdata.gateway_addr, .subnet_mask = client->lease_auxdata.subnet_mask, }); } } static __attribute__((noreturn)) void dhcp_client_run(struct dhcp_client *client) { assert(client); /* State transition diagram: https://datatracker.ietf.org/doc/html/rfc2131#page-35 */ for (;;) { switch (client->state) { case STATE_INIT: { client->xid = (uint32_t)random(); /* random() returns 31 random bits; good enough */ client->time_ns_init = VCALL(bootclock, get_time_ns); if (dhcp_client_send(client, DHCP_MSGTYP_DISCOVER, NULL)) break; client->state = STATE_SELECTING; break; } case STATE_SELECTING: { VCALL(client->sock, set_read_deadline, client->time_ns_init+CONFIG_DHCP_SELECTING_NS); struct dhcp_recv_msg msg; ssize_t r = dhcp_client_recv(client, &msg); switch (r) { case 0: switch (msg.options[DHCP_OPT_DHCP_MSG_TYPE].dat[0]) { case DHCP_MSGTYP_OFFER: /* Accept the first offer. */ dhcp_client_take_lease(client, &msg, false); if (dhcp_client_send(client, DHCP_MSGTYP_REQUEST, NULL)) break; client->state = STATE_REQUESTING; break; default: /* ignore */ } break; case -NET_ETIMEDOUT: client->state = STATE_INIT; break; default: assert(r < 0); debugf("error: recvfrom: %d", r); } break; } case STATE_REQUESTING: { VCALL(client->sock, set_read_deadline, 0); struct dhcp_recv_msg msg; ssize_t r = dhcp_client_recv(client, &msg); switch (r) { case 0: switch (msg.options[DHCP_OPT_DHCP_MSG_TYPE].dat[0]) { case DHCP_MSGTYP_NAK: client->state = STATE_INIT; break; case DHCP_MSGTYP_ACK: if (dhcp_check_conflict(client->sock, client->lease_client_addr)) { (void)dhcp_client_send(client, DHCP_MSGTYP_DECLINE, "IP is already in use"); client->state = STATE_INIT; } else { dhcp_client_take_lease(client, &msg, true); } default: /* ignore */ } break; default: assert(r < 0); debugf("error: recvfrom: %d", r); } break; } case STATE_BOUND: { VCALL(client->sock, set_read_deadline, client->lease_time_ns_t1); struct dhcp_recv_msg msg; ssize_t r = dhcp_client_recv(client, &msg); switch (r) { case 0: /* discard */ break; case -NET_ETIMEDOUT: if (dhcp_client_send(client, DHCP_MSGTYP_REQUEST, NULL)) break; client->state = STATE_RENEWING; break; default: assert(r < 0); debugf("error: recvfrom: %d", r); } break; } case STATE_RENEWING: { VCALL(client->sock, set_read_deadline, client->lease_time_ns_t2); struct dhcp_recv_msg msg; ssize_t r = dhcp_client_recv(client, &msg); switch (r) { case 0: switch (msg.options[DHCP_OPT_DHCP_MSG_TYPE].dat[0]) { case DHCP_MSGTYP_NAK: VCALL(client->iface, ifdown); client->state = STATE_INIT; break; case DHCP_MSGTYP_ACK: dhcp_client_take_lease(client, &msg, true); default: /* ignore */ } break; case -NET_ETIMEDOUT: if (dhcp_client_send(client, DHCP_MSGTYP_REQUEST, NULL)) break; client->state = STATE_REBINDING; break; default: assert(r < 0); debugf("error: recvfrom: %d", r); } break; } case STATE_REBINDING: { VCALL(client->sock, set_read_deadline, client->lease_time_ns_end); struct dhcp_recv_msg msg; ssize_t r = dhcp_client_recv(client, &msg); switch (r) { case 0: switch (msg.options[DHCP_OPT_DHCP_MSG_TYPE].dat[0]) { case DHCP_MSGTYP_NAK: VCALL(client->iface, ifdown); client->state = STATE_INIT; break; case DHCP_MSGTYP_ACK: dhcp_client_take_lease(client, &msg, true); default: /* ignore */ } break; case -NET_ETIMEDOUT: VCALL(client->iface, ifdown); client->state = STATE_INIT; break; default: assert(r < 0); debugf("error: recvfrom: %d", r); } break; } case STATE_INIT_REBOOT: case STATE_REBOOTING: assert_notreached("initialization with known network address is not supported"); default: assert_notreached("invalid client state"); } } } __attribute__((noreturn)) void dhcp_client_main(implements_net_iface *iface, char *self_hostname) { assert(iface); struct dhcp_client client = { /* Static. */ .iface = iface, .sock = VCALL(iface, udp_conn, DHCP_PORT_CLIENT), .self_eth_addr = VCALL(iface, hwaddr), /* Lease. */ .state = STATE_INIT, }; if (self_hostname) strncpy(client.self_hostname, self_hostname, sizeof(client.self_hostname)); assert(client.sock); dhcp_client_run(&client); }