summaryrefslogtreecommitdiff
path: root/libdhcp/dhcp_client.c
diff options
context:
space:
mode:
Diffstat (limited to 'libdhcp/dhcp_client.c')
-rw-r--r--libdhcp/dhcp_client.c934
1 files changed, 934 insertions, 0 deletions
diff --git a/libdhcp/dhcp_client.c b/libdhcp/dhcp_client.c
new file mode 100644
index 0000000..8ec3647
--- /dev/null
+++ b/libdhcp/dhcp_client.c
@@ -0,0 +1,934 @@
+/* libdhcp/dhcp_client.c - A DHCP client
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * 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 <ORGANIZATION> 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
+ */
+
+/* Implemented:
+ * - DHCPv4: https://datatracker.ietf.org/doc/html/rfc2131
+ * - DHCPv4 long options: https://datatracker.ietf.org/doc/html/rfc3396
+ * - DHCPv4 client identifiers in replies: https://datatracker.ietf.org/doc/html/rfc6842
+ *
+ * TODO:
+ * - AutoIPv4: https://datatracker.ietf.org/doc/html/rfc3927
+ * - mDNS responder: https://datatracker.ietf.org/doc/html/rfc6762
+ *
+ * Not implemented:
+ * - DHCPv4 node-specific client identifiers: https://datatracker.ietf.org/doc/html/rfc4361
+ * - DNAv4: https://datatracker.ietf.org/doc/html/rfc4436
+ * - LLMNR responder: https://datatracker.ietf.org/doc/html/rfc4795
+ *
+ * Not implemented, because the W5500 doesn't support IPv6:
+ * - SLAAC: https://datatracker.ietf.org/doc/html/rfc2462
+ * - DNAv6: https://datatracker.ietf.org/doc/html/rfc6059
+ * - DHCPv6: https://datatracker.ietf.org/doc/html/rfc8415
+ */
+
+#include <string.h> /* for strlen(), memcpy(), memset() */
+
+#include <libmisc/rand.h>
+#include <libhw/generic/alarmclock.h>
+
+#define LOG_NAME DHCP
+#include <libmisc/log.h>
+
+#include <libdhcp/client.h>
+
+#include "dhcp_common.h"
+
+/* Config *********************************************************************/
+
+#include "config.h"
+
+#ifndef CONFIG_DHCP_DEBUG
+ #error config.h must define CONFIG_DHCP_DEBUG
+#endif
+#ifndef CONFIG_DHCP_SELECTING_NS
+ #error config.h must define CONFIG_DHCP_SELECTING_NS
+#endif
+#ifndef CONFIG_DHCP_CAN_RECV_UNICAST_IP_WITHOUT_IP
+ #error config.h must define CONFIG_DHCP_CAN_RECV_UNICAST_IP_WITHOUT_IP
+#endif
+
+/* Implementation *************************************************************/
+
+enum requirement {
+ MUST,
+ MUST_NOT,
+ SHOULD,
+ SHOULD_NOT,
+ MAY,
+
+ _SHOULD_NOT_HAPPEN,
+};
+
+struct dhcp_client {
+ /* Static. */
+ lo_interface net_iface iface;
+ lo_interface net_packet_conn sock;
+ struct net_eth_addr self_eth_addr;
+ char *self_hostname;
+ size_t self_id_len;
+ void *self_id_dat;
+
+ /* 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;
+ uint16_t last_discover_secs;
+ /* 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;
+
+};
+
+static const char *state_strs[] = {
+ [STATE_INIT] = "INIT",
+ [STATE_SELECTING] = "SELECTING",
+ [STATE_REQUESTING] = "REQUESTING",
+ [STATE_BOUND] = "BOUND",
+ [STATE_RENEWING] = "RENEWING",
+ [STATE_REBINDING] = "REBINDING",
+
+ [STATE_INIT_REBOOT] = "INIT_REBOOT",
+ [STATE_REBOOTING] = "REBOOTING",
+};
+
+/**
+ * For convenience in switch blocks, a list of the states that, when
+ * msgtyp==DHCP_MSGTYP_REQUEST, dhcp_client_send() has assert()ed the state is
+ * not.
+ */
+#define IMPOSSIBLE_REQUEST_STATES \
+ STATE_INIT: \
+ case STATE_REQUESTING: \
+ case STATE_REBINDING: \
+ case STATE_REBOOTING
+
+static inline enum requirement dhcp_table5(typeof((struct dhcp_client){}.state) state,
+ uint8_t msgtyp,
+ uint8_t opt) {
+ /* Encode "Table 5: Fields and options used by DHCP clients"
+ * from
+ * https://datatracker.ietf.org/doc/html/rfc2131#page-38. */
+#define INC_ADDR ({ \
+ enum requirement req; \
+ switch (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 (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; \
+ })
+ struct { enum requirement cols[5]; } row;
+#define ROW(...) row = (typeof(row)){{ __VA_ARGS__ }}
+ switch (opt) {
+ /* Option DISCOVER INFORM REQUEST DECLINE RELEASE */
+ /* ------ ---------- ---------- ---------- -------- -------- */
+ case DHCP_OPT_ADDRESS_REQUEST: ROW(MAY, MUST_NOT, INC_ADDR, MUST, MUST_NOT); break;
+ case DHCP_OPT_ADDRESS_TIME: ROW(MAY, MUST_NOT, MAY, MUST_NOT, MUST_NOT); break;
+ case DHCP_OPT_OVERLOAD: ROW(MAY, MAY, MAY, MAY, MAY ); break;
+ case DHCP_OPT_DHCP_MSG_TYPE: ROW(MUST, MUST, MUST, MUST, MUST ); break;
+ case DHCP_OPT_CLIENT_ID: ROW(MAY, MAY, MAY, MAY, MAY ); break;
+ case DHCP_OPT_CLASS_ID: ROW(MAY, MAY, MAY, MUST_NOT, MUST_NOT); break;
+ case DHCP_OPT_DHCP_SERVER_ID: ROW(MUST_NOT, MUST_NOT, INC_SERVER, MUST, MUST ); break;
+ case DHCP_OPT_PARAMETER_LIST: ROW(MAY, MAY, MAY, MUST_NOT, MUST_NOT); break;
+ case DHCP_OPT_DHCP_MAX_MSG_SIZE: ROW(MAY, MAY, MAY, MUST_NOT, MUST_NOT); break;
+ case DHCP_OPT_DHCP_MESSAGE: ROW(SHOULD_NOT, SHOULD_NOT, SHOULD_NOT, SHOULD, SHOULD ); break;
+ default: ROW(MAY, MAY, MAY, MUST_NOT, MUST_NOT);
+ }
+#undef ROW
+#undef INC_SERVER
+#undef INC_ADDR
+ switch (msgtyp) {
+ case DHCP_MSGTYP_DISCOVER: return row.cols[0];
+ case DHCP_MSGTYP_INFORM: return row.cols[1];
+ case DHCP_MSGTYP_REQUEST: return row.cols[2];
+ case DHCP_MSGTYP_DECLINE: return row.cols[3];
+ case DHCP_MSGTYP_RELEASE: return row.cols[4];
+ default: return _SHOULD_NOT_HAPPEN;
+ }
+}
+
+/**
+ * @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, const char *errstr, struct dhcp_msg *scratch_msg) {
+ /**********************************************************************\
+ * 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 */
+
+ /**********************************************************************\
+ * 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 *
+ \**********************************************************************/
+
+ *scratch_msg = (struct dhcp_msg){0};
+ size_t optlen = 0;
+
+ /* Base structure.
+ * https://datatracker.ietf.org/doc/html/rfc2131#page-37 */
+ scratch_msg->op = DHCP_OP_BOOTREQUEST;
+ scratch_msg->htype = DHCP_HTYPE_ETHERNET;
+ scratch_msg->hlen = sizeof(client->self_eth_addr);
+ scratch_msg->hops = 0; /* relays increment this when they forward it along */
+ scratch_msg->xid = uint32be_marshal(client->xid);
+ scratch_msg->secs = uint16be_marshal( ({
+ uint16_t secs;
+ switch (msgtyp) {
+ case DHCP_MSGTYP_DISCOVER: case DHCP_MSGTYP_INFORM:
+ case DHCP_MSGTYP_REQUEST:
+ secs = (LO_CALL(bootclock, get_time_ns) - client->time_ns_init)/NS_PER_S;
+ if (!secs)
+ /* systemd's sd-dhcp-client.c asserts that some
+ * servers are broken and require .secs to be
+ * non-zero, even though RFC 2131 explicitly
+ * says that zero is valid. */
+ secs = 1;
+ if (msgtyp == DHCP_MSGTYP_REQUEST &&
+ client->state == STATE_SELECTING)
+ /* "The DHCPREQUEST message MUST use the same
+ * value in the DHCP message header's 'secs'
+ * field ... as the original DHCPDISCOVER
+ * message" -- RFC 2131
+ */
+ secs = client->last_discover_secs;
+ if (msgtyp == DHCP_MSGTYP_DISCOVER)
+ /* Record the value to make the above
+ * possible. */
+ client->last_discover_secs = secs;
+ break;
+ case DHCP_MSGTYP_DECLINE: case DHCP_MSGTYP_RELEASE:
+ secs = 0;
+ break;
+ default:
+ assert_notreached("invalid message type for client to send");
+ }
+ secs;
+ }) );
+ switch (msgtyp) {
+ case DHCP_MSGTYP_DISCOVER: case DHCP_MSGTYP_INFORM: case DHCP_MSGTYP_REQUEST:
+ scratch_msg->flags = uint16be_marshal(server_broadcasts ? DHCP_FLAG_BROADCAST : 0);
+ break;
+ case DHCP_MSGTYP_DECLINE: case DHCP_MSGTYP_RELEASE:
+ scratch_msg->flags = uint16be_marshal(0);
+ break;
+ }
+ switch (msgtyp) {
+ case DHCP_MSGTYP_DISCOVER: scratch_msg->ciaddr = net_ip4_addr_zero; break;
+ case DHCP_MSGTYP_INFORM: scratch_msg->ciaddr = client->lease_client_addr; break;
+ case DHCP_MSGTYP_REQUEST: switch (client->state) {
+ case STATE_SELECTING: case STATE_INIT_REBOOT:
+ scratch_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. */
+ scratch_msg->ciaddr = client->lease_client_addr; break;
+ case IMPOSSIBLE_REQUEST_STATES:
+ assert_notreached("invalid client state for sending DHCPREQUEST");
+ } break;
+ case DHCP_MSGTYP_DECLINE: scratch_msg->ciaddr = net_ip4_addr_zero; break;
+ case DHCP_MSGTYP_RELEASE: scratch_msg->ciaddr = client->lease_client_addr; break;
+ }
+ scratch_msg->yiaddr = net_ip4_addr_zero; /* only set by servers */
+ scratch_msg->siaddr = net_ip4_addr_zero; /* only set by servers */
+ scratch_msg->giaddr = net_ip4_addr_zero; /* only set by relays */
+ memcpy(scratch_msg->chaddr, client->self_eth_addr.octets, sizeof(client->self_eth_addr));
+ /* scratch_msg->sname = "options, if indicated in 'sname/file' option'; otherwise unused"; */
+ /* scratch_msg->file = "options, if indicated in 'sname/file' option'; otherwise unused"; */
+
+ /* Magic cookie.
+ * https://datatracker.ietf.org/doc/html/rfc2131#section-4.1 */
+ scratch_msg->options[optlen++] = dhcp_magic_cookie[0];
+ scratch_msg->options[optlen++] = dhcp_magic_cookie[1];
+ scratch_msg->options[optlen++] = dhcp_magic_cookie[2];
+ scratch_msg->options[optlen++] = dhcp_magic_cookie[3];
+
+ /* Options. */
+ for (uint8_t opt = 1; opt < 255; opt++) {
+ enum requirement req = dhcp_table5(client->state, msgtyp, opt);
+ switch (req) {
+ case MUST_NOT:
+ /* Do nothing. */
+ break;
+ case MUST:
+ case SHOULD:
+ case SHOULD_NOT:
+ case MAY:
+ struct { size_t len; const void *ptr; } val;
+ uint8_t _val_prl[] = {
+ DHCP_OPT_SUBNET_MASK,
+ DHCP_OPT_ROUTER,
+ DHCP_OPT_RENEWAL_TIME,
+ DHCP_OPT_REBINDING_TIME,
+ };
+ uint16be_t _val16 ;
+#define V_RAW(len, ptr) val = (typeof(val)){ len, ptr }
+#define V_OBJ(x) V_RAW(sizeof(x), &x)
+#define V_STR(x) V_RAW(x ? strlen(x) : 0, x)
+#define V_NONE() V_RAW(0, NULL)
+ switch (opt) {
+ /* For clarity, list all options mentioned in "Table 5:
+ * Fields and options used by DHCP clients" from
+ * https://datatracker.ietf.org/doc/html/rfc2131#page-38,
+ * even if we don't have a value for them. */
+ case DHCP_OPT_ADDRESS_REQUEST: V_OBJ(client->lease_client_addr); break;
+ case DHCP_OPT_ADDRESS_TIME: V_NONE(); break;
+ case DHCP_OPT_OVERLOAD: V_NONE(); break;
+ case DHCP_OPT_DHCP_MSG_TYPE: V_OBJ(msgtyp); break;
+ case DHCP_OPT_CLIENT_ID: V_RAW(client->self_id_len, client->self_id_dat); break;
+ case DHCP_OPT_CLASS_ID: V_NONE(); break;
+ case DHCP_OPT_DHCP_SERVER_ID: V_OBJ(client->lease_server_id); break;
+ case DHCP_OPT_PARAMETER_LIST: V_OBJ(_val_prl); break;
+ case DHCP_OPT_DHCP_MAX_MSG_SIZE:
+ if (CONFIG_DHCP_OPT_SIZE <= DHCP_MSG_MIN_MAX_OPT_SIZE)
+ V_NONE();
+ else {
+ _val16 = uint16be_marshal(20 /* IP header */ +
+ 8 /* UDP header */ +
+ sizeof(*scratch_msg));
+ V_OBJ(_val16);
+ }
+ break;
+ case DHCP_OPT_DHCP_MESSAGE: V_STR(errstr); break;
+ /* "Site-specific" and "All others". */
+ case DHCP_OPT_HOSTNAME: V_STR(client->self_hostname); break;
+ default: V_NONE();
+ };
+#undef V_NONE
+#undef V_STR
+#undef V_OBJ
+#undef V_RAW
+ if (req == MUST)
+ assert(val.len);
+ if (val.len) {
+ assert(val.len <= UINT16_MAX);
+ for (size_t done = 0; done < val.len;) {
+ uint8_t len = val.len - done > UINT8_MAX
+ ? UINT8_MAX
+ : val.len - done;
+ scratch_msg->options[optlen++] = opt;
+ scratch_msg->options[optlen++] = len;
+ memcpy(&scratch_msg->options[optlen], &((char*)val.ptr)[done], len);
+ optlen += len;
+ done += len;
+ }
+ }
+ break;
+ case _SHOULD_NOT_HAPPEN:
+ assert_notreached("bad table");
+ }
+ }
+ scratch_msg->options[optlen++] = DHCP_OPT_END;
+ assert(optlen <= CONFIG_DHCP_OPT_SIZE);
+
+ /**********************************************************************\
+ * Send *
+ \**********************************************************************/
+ debugf("state %s: sending DHCP %s", state_strs[client->state], dhcp_msgtyp_str(msgtyp));
+ ssize_t r = LO_CALL(client->sock, sendto, scratch_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 off;
+ uint16_t len;
+ } options[0xff];
+ uint8_t option_dat[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->option_dat[ret->options[opt_typ].off+ret->options[opt_typ].len], &dat[pos], opt_len);
+ ret->options[opt_typ].len += opt_len;
+ }
+ }
+}
+
+static inline enum requirement dhcp_table3(uint8_t req_msgtyp, uint8_t resp_msgtyp, uint8_t resp_opt) {
+ /* Encode "Table 3: Fields and options used by DHCP servers"
+ * from https://datatracker.ietf.org/doc/html/rfc2131#page-29,
+ * with modifications from
+ * https://datatracker.ietf.org/doc/html/rfc6842#page-4. */
+ struct { enum requirement cols[3]; } row;
+#define ROW(...) row = (typeof(row)){{ __VA_ARGS__ }}
+ switch (resp_opt) {
+ /* Option DHCPOFFER DHCPACK DHCPNAK */
+ /* -------------------------- -------- ---------- -------- */
+ case DHCP_OPT_ADDRESS_REQUEST: ROW(MUST_NOT, MUST_NOT, MUST_NOT); break;
+ case DHCP_OPT_ADDRESS_TIME: ROW(MUST, req_msgtyp == DHCP_MSGTYP_REQUEST ? MUST : MUST_NOT,
+ MUST_NOT); break;
+ case DHCP_OPT_OVERLOAD: ROW(MAY, MAY, MUST_NOT); break;
+ case DHCP_OPT_DHCP_MSG_TYPE: ROW(MUST, MUST, MUST ); break;
+ case DHCP_OPT_PARAMETER_LIST: ROW(MUST_NOT, MUST_NOT, MUST_NOT); break;
+ case DHCP_OPT_DHCP_MESSAGE: ROW(SHOULD, SHOULD, SHOULD ); break;
+ case DHCP_OPT_CLIENT_ID: ROW(MAY, MAY, MAY ); /* MUST_NOTs changed to MAY, per RFC 6842 */ break;
+ case DHCP_OPT_CLASS_ID: ROW(MAY, MAY, MAY ); break;
+ case DHCP_OPT_DHCP_SERVER_ID: ROW(MUST, MUST, MUST ); break;
+ case DHCP_OPT_DHCP_MAX_MSG_SIZE: ROW(MUST_NOT, MUST_NOT, MUST_NOT); break;
+ default: ROW(MAY, MAY, MUST_NOT);
+ }
+#undef ROW
+ switch (resp_msgtyp) {
+ case DHCP_MSGTYP_OFFER: return row.cols[0];
+ case DHCP_MSGTYP_ACK: return row.cols[1];
+ case DHCP_MSGTYP_NAK: return row.cols[2];
+ default: return _SHOULD_NOT_HAPPEN;
+ }
+}
+
+/**
+ * @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 = LO_CALL(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].off = 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. */
+ if (!ret->options[DHCP_OPT_DHCP_MSG_TYPE].len)
+ goto ignore;
+ for (uint8_t opt = 1; opt < 255; opt++) {
+ enum requirement req = dhcp_table3(client->last_sent_msgtyp,
+ ret->option_dat[ret->options[DHCP_OPT_DHCP_MSG_TYPE].off],
+ opt);
+ 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:
+ goto ignore;
+ }
+ }
+ /* Validate values of options. */
+ if (ret->options[DHCP_OPT_CLASS_ID].len) {
+ /* https://datatracker.ietf.org/doc/html/rfc6842#page-4 */
+ if (ret->options[DHCP_OPT_CLASS_ID].len != client->self_id_len ||
+ memcmp(&ret->option_dat[ret->options[DHCP_OPT_CLASS_ID].off], client->self_id_dat, client->self_id_len))
+ /* ignore messages that aren't to us */
+ goto ignore;
+ }
+
+ return 0;
+}
+
+/** @return true if there's a conflict, false if the addr appears to be unused */
+static bool dhcp_check_conflict(lo_interface net_packet_conn sock, struct net_ip4_addr addr) {
+ assert(!LO_IS_NULL(sock));
+ ssize_t v = LO_CALL(sock, sendto, "CHECK_IP_CONFLICT", 17, addr, 5000);
+ debugf("check_ip_conflict => %zd", v);
+ return v != -NET_EARP_TIMEOUT;
+}
+
+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->option_dat[msg->options[DHCP_OPT_SUBNET_MASK].off])
+ : net_ip4_addr_zero;
+ client->lease_auxdata.gateway_addr = msg->options[DHCP_OPT_ROUTER].len
+ ? *((struct net_ip4_addr *)&msg->option_dat[msg->options[DHCP_OPT_SUBNET_MASK].off])
+ : net_ip4_addr_zero;
+ client->lease_server_id =
+ *((struct net_ip4_addr *)&msg->option_dat[msg->options[DHCP_OPT_DHCP_SERVER_ID].off]);
+
+ uint64_t dur_ns_end =
+ ((uint64_t)uint32be_decode(&msg->option_dat[msg->options[DHCP_OPT_ADDRESS_TIME].off]))*NS_PER_S;
+ uint64_t dur_ns_t1 = msg->options[DHCP_OPT_RENEWAL_TIME].len
+ ? ((uint64_t)uint32be_decode(&msg->option_dat[msg->options[DHCP_OPT_RENEWAL_TIME].off]))*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->option_dat[msg->options[DHCP_OPT_RENEWAL_TIME].off]))*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) {
+ infof("applying configuration to "PRI_net_eth_addr, ARG_net_eth_addr(client->self_eth_addr));
+ infof(":: addr = "PRI_net_ip4_addr, ARG_net_ip4_addr(client->lease_client_addr));
+ infof(":: gateway_addr = "PRI_net_ip4_addr, ARG_net_ip4_addr(client->lease_auxdata.gateway_addr));
+ infof(":: subnet_mask = "PRI_net_ip4_addr, ARG_net_ip4_addr(client->lease_auxdata.subnet_mask));
+ LO_CALL(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 void dhcp_client_setstate(struct dhcp_client *client,
+ typeof((struct dhcp_client){}.state) newstate,
+ uint8_t send_msgtyp, const char *errstr, struct dhcp_recv_msg *scratch_msg) {
+ if (send_msgtyp)
+ (void)dhcp_client_send(client, send_msgtyp, errstr, &scratch_msg->raw);
+ client->state = newstate;
+}
+
+[[noreturn]] static void dhcp_client_run(struct dhcp_client *client, struct dhcp_recv_msg *scratch_msg) {
+ assert(client);
+
+ ssize_t r;
+
+ /* State transition diagram: https://datatracker.ietf.org/doc/html/rfc2131#page-35 */
+ for (;;) {
+ debugf("loop: state=%s", state_strs[client->state]);
+ switch (client->state) {
+ case STATE_INIT:
+ client->xid = rand_uint63n(UINT32_MAX);
+ client->time_ns_init = LO_CALL(bootclock, get_time_ns);
+ dhcp_client_setstate(client, STATE_SELECTING, DHCP_MSGTYP_DISCOVER, NULL, scratch_msg);
+ break;
+ case STATE_SELECTING:
+ LO_CALL(client->sock, set_recv_deadline, client->time_ns_init+CONFIG_DHCP_SELECTING_NS);
+ switch ((r = dhcp_client_recv(client, scratch_msg))) {
+ case 0:
+ switch (scratch_msg->option_dat[scratch_msg->options[DHCP_OPT_DHCP_MSG_TYPE].off]) {
+ case DHCP_MSGTYP_OFFER:
+ /* Accept the first offer. */
+ dhcp_client_take_lease(client, scratch_msg, false);
+ dhcp_client_setstate(client, STATE_REQUESTING, DHCP_MSGTYP_REQUEST, NULL, scratch_msg);
+ break;
+ default:
+ /* ignore */
+ }
+ break;
+ case -NET_ERECV_TIMEOUT:
+ dhcp_client_setstate(client, STATE_INIT, 0, NULL, scratch_msg);
+ break;
+ default:
+ assert(r < 0);
+ debugf("error: recvfrom: %d", r);
+ }
+ break;
+ case STATE_REQUESTING:
+ LO_CALL(client->sock, set_recv_deadline, 0);
+ switch ((r = dhcp_client_recv(client, scratch_msg))) {
+ case 0:
+ switch (scratch_msg->option_dat[scratch_msg->options[DHCP_OPT_DHCP_MSG_TYPE].off]) {
+ case DHCP_MSGTYP_NAK:
+ dhcp_client_setstate(client, STATE_INIT, 0, NULL, scratch_msg);
+ break;
+ case DHCP_MSGTYP_ACK:
+ if (dhcp_check_conflict(client->sock, client->lease_client_addr)) {
+ debugf("IP "PRI_net_ip4_addr" is already in use",
+ ARG_net_ip4_addr(client->lease_client_addr));
+ dhcp_client_setstate(client, STATE_INIT, DHCP_MSGTYP_DECLINE, "IP is already in use", scratch_msg);
+ } else {
+ dhcp_client_take_lease(client, scratch_msg, true);
+ dhcp_client_setstate(client, STATE_BOUND, 0, NULL, scratch_msg);
+ }
+ break;
+ default:
+ /* ignore */
+ }
+ break;
+ default:
+ assert(r < 0);
+ debugf("error: recvfrom: %d", r);
+ }
+ break;
+ case STATE_BOUND:
+ LO_CALL(client->sock, set_recv_deadline, client->lease_time_ns_t1);
+ switch ((r = dhcp_client_recv(client, scratch_msg))) {
+ case 0:
+ /* discard */
+ break;
+ case -NET_ERECV_TIMEOUT:
+ dhcp_client_setstate(client, STATE_RENEWING, DHCP_MSGTYP_REQUEST, NULL, scratch_msg);
+ break;
+ default:
+ assert(r < 0);
+ debugf("error: recvfrom: %d", r);
+ }
+ break;
+ case STATE_RENEWING:
+ client->xid = rand_uint63n(UINT32_MAX);
+ client->time_ns_init = LO_CALL(bootclock, get_time_ns);
+
+ LO_CALL(client->sock, set_recv_deadline, client->lease_time_ns_t2);
+ switch ((r = dhcp_client_recv(client, scratch_msg))) {
+ case 0:
+ switch (scratch_msg->option_dat[scratch_msg->options[DHCP_OPT_DHCP_MSG_TYPE].off]) {
+ case DHCP_MSGTYP_NAK:
+ LO_CALL(client->iface, ifdown);
+ dhcp_client_setstate(client, STATE_INIT, 0, NULL, scratch_msg);
+ break;
+ case DHCP_MSGTYP_ACK:
+ dhcp_client_take_lease(client, scratch_msg, true);
+ dhcp_client_setstate(client, STATE_BOUND, 0, NULL, scratch_msg);
+ break;
+ default:
+ /* ignore */
+ }
+ break;
+ case -NET_ERECV_TIMEOUT:
+ client->lease_server_id = net_ip4_addr_zero;
+ dhcp_client_setstate(client, STATE_REBINDING, DHCP_MSGTYP_REQUEST, NULL, scratch_msg);
+ break;
+ default:
+ assert(r < 0);
+ debugf("error: recvfrom: %d", r);
+ }
+ break;
+ case STATE_REBINDING:
+ LO_CALL(client->sock, set_recv_deadline, client->lease_time_ns_end);
+ switch ((r = dhcp_client_recv(client, scratch_msg))) {
+ case 0:
+ switch (scratch_msg->option_dat[scratch_msg->options[DHCP_OPT_DHCP_MSG_TYPE].off]) {
+ case DHCP_MSGTYP_NAK:
+ LO_CALL(client->iface, ifdown);
+ dhcp_client_setstate(client, STATE_BOUND, 0, NULL, scratch_msg);
+ break;
+ case DHCP_MSGTYP_ACK:
+ dhcp_client_take_lease(client, scratch_msg, true);
+ dhcp_client_setstate(client, STATE_BOUND, 0, NULL, scratch_msg);
+ break;
+ default:
+ /* ignore */
+ }
+ break;
+ case -NET_ERECV_TIMEOUT:
+ LO_CALL(client->iface, ifdown);
+ dhcp_client_setstate(client, STATE_BOUND, 0, NULL, scratch_msg);
+ 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");
+ }
+ }
+}
+
+[[noreturn]] void dhcp_client_main(lo_interface net_iface iface,
+ char *self_hostname) {
+ assert(!LO_IS_NULL(iface));
+
+ /* Even though a client ID is optional and not meaningful for
+ * us (the best we can do is to duplicate .chaddr), systemd's
+ * sd-dhcp-client.c asserts that some servers are broken and
+ * require it to be set. */
+ struct {uint8_t typ; struct net_eth_addr dat;} client_id = {
+ DHCP_HTYPE_ETHERNET,
+ LO_CALL(iface, hwaddr),
+ };
+
+ struct dhcp_client client = {
+ /* Static. */
+ .iface = iface,
+ .sock = LO_CALL(iface, udp_conn, DHCP_PORT_CLIENT),
+ .self_eth_addr = LO_CALL(iface, hwaddr),
+ .self_hostname = self_hostname,
+ .self_id_len = sizeof(client_id),
+ .self_id_dat = &client_id,
+
+ /* Mutable. */
+ .state = STATE_INIT,
+ };
+ assert(!LO_IS_NULL(client.sock));
+
+ struct dhcp_recv_msg scratch;
+
+ dhcp_client_run(&client, &scratch);
+}