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