summary refs log tree commit diff
path: root/shared/n-dhcp4/src
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2019-07-31 10:51:42 +0200
committerMichael Biebl <biebl@debian.org>2019-07-31 10:51:42 +0200
commit2e5fa45ddfbb5cffa1e78221f1cea706e2f298af (patch)
tree86f69d36c56de3074280456eddc854a780b8e04b /shared/n-dhcp4/src
parent85563b7fc7ec2cd21e38debb9b28db342e2e8e7c (diff)
New upstream version 1.19.90 upstream/1.19.90
Diffstat (limited to 'shared/n-dhcp4/src')
-rw-r--r--shared/n-dhcp4/src/n-dhcp4-c-connection.c1150
-rw-r--r--shared/n-dhcp4/src/n-dhcp4-c-lease.c356
-rw-r--r--shared/n-dhcp4/src/n-dhcp4-c-probe.c1203
-rw-r--r--shared/n-dhcp4/src/n-dhcp4-client.c839
-rw-r--r--shared/n-dhcp4/src/n-dhcp4-incoming.c431
-rw-r--r--shared/n-dhcp4/src/n-dhcp4-outgoing.c372
-rw-r--r--shared/n-dhcp4/src/n-dhcp4-private.h688
-rw-r--r--shared/n-dhcp4/src/n-dhcp4-socket.c657
-rw-r--r--shared/n-dhcp4/src/n-dhcp4.h286
-rw-r--r--shared/n-dhcp4/src/util/packet.c456
-rw-r--r--shared/n-dhcp4/src/util/packet.h61
-rw-r--r--shared/n-dhcp4/src/util/socket.c121
-rw-r--r--shared/n-dhcp4/src/util/socket.h11
13 files changed, 6631 insertions, 0 deletions
diff --git a/shared/n-dhcp4/src/n-dhcp4-c-connection.c b/shared/n-dhcp4/src/n-dhcp4-c-connection.c
new file mode 100644
index 00000000..5c50dacf
--- /dev/null
+++ b/shared/n-dhcp4/src/n-dhcp4-c-connection.c
@@ -0,0 +1,1150 @@
+/*
+ * DHCPv4 Client Connection
+ *
+ * XXX
+ */
+
+#include <assert.h>
+#include <c-stdaux.h>
+#include <errno.h>
+#include <limits.h>
+#include <sys/socket.h> /* needed by linux/netdevice.h */
+#include <linux/netdevice.h>
+#include <net/if_arp.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/epoll.h>
+#include "n-dhcp4-private.h"
+#include "util/packet.h"
+
+/**
+ * n_dhcp4_c_connection_init() - initialize client connection
+ * @connection:                 connection to operate on
+ * @client_config:              client configuration to use
+ * @probe_config:               client probe configuration to use
+ * @fd_epoll:                   epoll context to attach to, or -1
+ *
+ * This initializes a new client connection using the configuration given in
+ * @client_config and @probe_config.
+ *
+ * The client-configuration given as @client_config must survive the lifetime
+ * of @connection. It is pinned in the connection and used all over the place.
+ * The caller must guarantee that the configuration is not deallocated in the
+ * meantime. Same is true for @probe_config.
+ *
+ * The new connection automatically attaches to the epoll context given as
+ * @fd_epoll. The epoll FD is retained in the connection and the caller must
+ * guarantee that it lives as long as the connection.
+ * The caller is explicitly allowed to pass -1 as @fd_epoll, in which case the
+ * connection will initialize correctly, but will not be in a usable state.
+ * That is, any call to n_dhcp4_c_connection_listen() will fail, since it will
+ * be unable to attach to the epoll context. Such a connection can be used to
+ * get a detached object that behaves sound, but provides no runtime.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int n_dhcp4_c_connection_init(NDhcp4CConnection *connection,
+                              NDhcp4ClientConfig *client_config,
+                              NDhcp4ClientProbeConfig *probe_config,
+                              int fd_epoll) {
+        *connection = (NDhcp4CConnection)N_DHCP4_C_CONNECTION_NULL(*connection);
+        connection->client_config = client_config;
+        connection->probe_config = probe_config;
+        connection->fd_epoll = fd_epoll;
+
+        /*
+         * We explicitly allow initializing connections with an invalid
+         * epoll-fd. The resulting connection immediately transitions into the
+         * CLOSED state. This allows the caller to create dummy connections
+         * useful to provide asynchronous constructor-feedback in the API.
+         *
+         * The effect of this is as if you immediately call
+         * n_dhcp4_c_connection_close() on the new connection. However, by
+         * directly passing -1 in the constructor, you are guaranteed not even
+         * the constructor can ever mess with your epoll-set.
+         */
+        if (connection->fd_epoll < 0)
+                connection->state = N_DHCP4_C_CONNECTION_STATE_CLOSED;
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_c_connection_deinit() - deinitialize client connection
+ * @connection:                 connection to operate on
+ *
+ * This deinitializes a connection that was previously initialized via
+ * n_dhcp4_c_connection_init(). It will tear down all allocated state and
+ * release it.
+ *
+ * Once this function returns, @connection is re-initialized to
+ * N_DHCP4_C_CONNECTION_NULL. If this function is called on a deinitialized
+ * connection, it is a no-op.
+ */
+void n_dhcp4_c_connection_deinit(NDhcp4CConnection *connection) {
+        n_dhcp4_c_connection_close(connection);
+        n_dhcp4_outgoing_free(connection->request);
+        *connection = (NDhcp4CConnection)N_DHCP4_C_CONNECTION_NULL(*connection);
+}
+
+static void n_dhcp4_c_connection_outgoing_set_secs(NDhcp4Outgoing *message) {
+        uint32_t secs;
+
+        /*
+         * This function sets the `secs` field for outgoing messages. It
+         * expects the base-time and start-time to be already set by the
+         * caller.
+         * For a given outgoing message, its `secs` field describes the time
+         * (in seconds) between the start of the transaction this message is
+         * part of and the start of the operational process (also called the
+         * base time here).
+         *
+         * The operational process in the DHCP sense describes the entire
+         * process of requesting a lease and acquiring it. That is, it starts
+         * with the caller's intent to request a lease, and it ends when we
+         * got granted a lease. The act of refreshing a lease is, in itself, a
+         * new operational process. The base-time describes the start-time
+         * recorded when such a process as initiated.
+         *
+         * A transaction in the DHCP sense describes a request+reply
+         * combination, in most cases. That is, the time a request is sent is
+         * the start-time of a transaction. In the ideal case, the start-time
+         * of the first transaction in an operational process matches the
+         * base-time. However, transactions are often delayed with a randomized
+         * offset to reduce traffic during network bursts.
+         * In some cases, however, transactions are composed out of multiple
+         * requests+reply combinations. This includes, for instance, the SELECT
+         * message following an OFFER. The specification clearly says that
+         * those must be considered a single transaction and thus share the
+         * transaction start-time.
+         *
+         * The `secs` field, thus, describes how long a client has been busy
+         * requesting a lease. DHCP servers and proxies do use it to prioritize
+         * clients.
+         *
+         * Note: Some DHCP relays reject a `secs` value of 0 (which might look
+         *       like it is uninitialized). Hence, we always clamp the value to
+         *       the range `[1, INF[`.
+         */
+
+        secs = message->userdata.base_time - message->userdata.start_time;
+        secs /= 1000ULL * 1000ULL * 1000ULL; /* nsecs to secs */
+        secs = secs ?: 1; /* clamp to `[1, INF[` */
+
+        n_dhcp4_outgoing_set_secs(message, secs);
+}
+
+int n_dhcp4_c_connection_listen(NDhcp4CConnection *connection) {
+        _c_cleanup_(c_closep) int fd_packet = -1;
+        int r;
+
+        c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_INIT);
+
+        r = n_dhcp4_c_socket_packet_new(&fd_packet, connection->client_config->ifindex);
+        if (r)
+                return r;
+
+        r = epoll_ctl(connection->fd_epoll,
+                      EPOLL_CTL_ADD,
+                      fd_packet,
+                      &(struct epoll_event){
+                              .events = EPOLLIN,
+                              .data = { .u32 = N_DHCP4_CLIENT_EPOLL_IO },
+                      });
+        if (r < 0)
+                return -errno;
+
+        connection->state = N_DHCP4_C_CONNECTION_STATE_PACKET;
+        connection->fd_packet = fd_packet;
+        fd_packet = -1;
+        return 0;
+}
+
+int n_dhcp4_c_connection_connect(NDhcp4CConnection *connection,
+                                 const struct in_addr *client,
+                                 const struct in_addr *server) {
+        int r, fd_udp;
+
+        c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_PACKET);
+
+        r = n_dhcp4_c_socket_udp_new(&fd_udp,
+                                     connection->client_config->ifindex,
+                                     client,
+                                     server);
+        if (r)
+                return r;
+
+        r = epoll_ctl(connection->fd_epoll,
+                      EPOLL_CTL_ADD,
+                      fd_udp,
+                      &(struct epoll_event){
+                              .events = EPOLLIN,
+                              .data = { .u32 = N_DHCP4_CLIENT_EPOLL_IO },
+                      });
+        if (r < 0) {
+                r = -errno;
+                goto exit_fd;
+        }
+
+        r = packet_shutdown(connection->fd_packet);
+        if (r < 0)
+                goto exit_epoll;
+
+        connection->state = N_DHCP4_C_CONNECTION_STATE_DRAINING;
+        connection->fd_udp = fd_udp;
+        connection->client_ip = client->s_addr;
+        connection->server_ip = server->s_addr;
+        fd_udp = -1;
+        return 0;
+
+exit_epoll:
+        epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, fd_udp, NULL);
+exit_fd:
+        close(fd_udp);
+        return r;
+}
+
+void n_dhcp4_c_connection_close(NDhcp4CConnection *connection) {
+        if (connection->fd_udp >= 0) {
+                epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, connection->fd_udp, NULL);
+                connection->fd_udp = c_close(connection->fd_udp);
+        }
+
+        if (connection->fd_packet >= 0) {
+                epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, connection->fd_packet, NULL);
+                connection->fd_packet = c_close(connection->fd_packet);
+        }
+
+        connection->fd_epoll = -1;
+        connection->state = N_DHCP4_C_CONNECTION_STATE_CLOSED;
+}
+
+static int n_dhcp4_c_connection_verify_incoming(NDhcp4CConnection *connection,
+                                                NDhcp4Incoming *message,
+                                                uint8_t *typep) {
+        NDhcp4Header *header = n_dhcp4_incoming_get_header(message);
+        uint8_t type;
+        uint32_t request_xid;
+        uint8_t *id;
+        size_t n_id;
+        int r;
+
+        r = n_dhcp4_incoming_query_message_type(message, &type);
+        if (r) {
+                if (r == N_DHCP4_E_UNSET)
+                        return N_DHCP4_E_MALFORMED;
+                else
+                        return r;
+        }
+
+        switch (type) {
+        case N_DHCP4_MESSAGE_OFFER:
+        case N_DHCP4_MESSAGE_ACK:
+        case N_DHCP4_MESSAGE_NAK:
+                /*
+                 * Only accept replies if there is a pending request, and it
+                 * has a matching transaction id.
+                 */
+                if (!connection->request)
+                        return N_DHCP4_E_UNEXPECTED;
+
+                n_dhcp4_outgoing_get_xid(connection->request, &request_xid);
+                if (header->xid != request_xid)
+                        return N_DHCP4_E_UNEXPECTED;
+
+                break;
+        case N_DHCP4_MESSAGE_FORCERENEW:
+                /*
+                 * Force renew messages are triggered by a server, and do not
+                 * match a pending request.
+                 */
+                break;
+        default:
+                return N_DHCP4_E_UNEXPECTED;
+        }
+
+        /*
+         * In case our transport makes use of the 'chaddr' field, make sure it
+         * matches exactly our address.
+         */
+        switch (connection->client_config->transport) {
+        case N_DHCP4_TRANSPORT_ETHERNET:
+                c_assert(connection->client_config->n_mac == ETH_ALEN);
+
+                if (header->hlen != ETH_ALEN)
+                        return N_DHCP4_E_UNEXPECTED;
+                if (memcmp(header->chaddr, connection->client_config->mac, ETH_ALEN) != 0)
+                        return N_DHCP4_E_UNEXPECTED;
+
+                break;
+        case N_DHCP4_TRANSPORT_INFINIBAND:
+                if (header->hlen != 0)
+                        return N_DHCP4_E_UNEXPECTED;
+
+                break;
+        }
+
+        /*
+         * If a server passes us back a client ID, it must be the one we
+         * provided. We ignore any packets that have mismatching client-ids.
+         */
+        id = NULL;
+        n_id = 0;
+        r = n_dhcp4_incoming_query(message, N_DHCP4_OPTION_CLIENT_IDENTIFIER, &id, &n_id);
+        if (r) {
+                if (r != N_DHCP4_E_UNSET)
+                        return r;
+        } else {
+                if (n_id != connection->client_config->n_client_id)
+                        return N_DHCP4_E_UNEXPECTED;
+                if (memcmp(id, connection->client_config->client_id, n_id) != 0)
+                        return N_DHCP4_E_UNEXPECTED;
+        }
+
+        *typep = type;
+        return 0;
+}
+
+void n_dhcp4_c_connection_get_timeout(NDhcp4CConnection *connection,
+                                      uint64_t *timeoutp) {
+        uint64_t timeout;
+        size_t n_send;
+
+        if (!connection->request) {
+                *timeoutp = 0;
+                return;
+        }
+
+        switch (connection->request->userdata.type) {
+        case N_DHCP4_C_MESSAGE_DISCOVER:
+        case N_DHCP4_C_MESSAGE_SELECT:
+        case N_DHCP4_C_MESSAGE_REBOOT:
+        case N_DHCP4_C_MESSAGE_INFORM:
+                /*
+                 * Resend with an exponential backoff and a one second random
+                 * slack, from a minimum of two seconds to a maximum of sixty
+                 * four.
+                 *
+                 * Note that the RFC says to start at four rather than two
+                 * seconds, and use [-1,1] slack, rather than [0,1].
+                 */
+                n_send = connection->request->userdata.n_send;
+                if (n_send >= 6)
+                        n_send = 6;
+
+                timeout = connection->request->userdata.send_time + ((1ULL << n_send) * 1000000000ULL) + connection->request->userdata.send_jitter;
+
+                break;
+        case N_DHCP4_C_MESSAGE_REBIND:
+        case N_DHCP4_C_MESSAGE_RENEW:
+                /*
+                 * Resend every sixty seconds with a one second random slack.
+                 *
+                 * Note that the RFC says to do this at most once, but we do
+                 * it until we are cancelled.
+                 */
+                timeout = connection->request->userdata.send_time + (60ULL * 1000000000ULL) + connection->request->userdata.send_jitter;
+
+                break;
+        case N_DHCP4_C_MESSAGE_DECLINE:
+        case N_DHCP4_C_MESSAGE_RELEASE:
+                /* XXX make sure these message types are never pinned? */
+                timeout = 0;
+                break;
+        default:
+                c_assert(0);
+        }
+
+        *timeoutp = timeout;
+}
+
+static int n_dhcp4_c_connection_packet_broadcast(NDhcp4CConnection *connection,
+                                                 NDhcp4Outgoing *message) {
+        int r;
+
+        c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_PACKET);
+
+        r = n_dhcp4_c_socket_packet_send(connection->fd_packet,
+                                         connection->client_config->ifindex,
+                                         connection->client_config->broadcast_mac,
+                                         connection->client_config->n_broadcast_mac,
+                                         message);
+        if (r)
+                return r;
+
+        return 0;
+}
+
+static int n_dhcp4_c_connection_udp_broadcast(NDhcp4CConnection *connection,
+                                              NDhcp4Outgoing *message) {
+        int r;
+
+        c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_DRAINING ||
+               connection->state == N_DHCP4_C_CONNECTION_STATE_UDP);
+
+        r = n_dhcp4_c_socket_udp_broadcast(connection->fd_udp, message);
+        if (r)
+                return r;
+
+        return 0;
+}
+
+static int n_dhcp4_c_connection_udp_send(NDhcp4CConnection *connection,
+                                         NDhcp4Outgoing *message) {
+        int r;
+
+        c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_DRAINING ||
+               connection->state == N_DHCP4_C_CONNECTION_STATE_UDP);
+
+        r = n_dhcp4_c_socket_udp_send(connection->fd_udp, message);
+        if (r)
+                return r;
+
+        return 0;
+}
+
+static void n_dhcp4_c_connection_init_header(NDhcp4CConnection *connection,
+                                             NDhcp4Header *header) {
+        bool broadcast = connection->client_config->request_broadcast;
+
+        header->op = N_DHCP4_OP_BOOTREQUEST;
+
+        switch (connection->client_config->transport) {
+        case N_DHCP4_TRANSPORT_ETHERNET:
+                c_assert(connection->client_config->n_mac == ETH_ALEN);
+
+                header->htype = ARPHRD_ETHER;
+                header->hlen = ETH_ALEN;
+                memcpy(header->chaddr, connection->client_config->mac, ETH_ALEN);
+                break;
+        case N_DHCP4_TRANSPORT_INFINIBAND:
+                header->htype = ARPHRD_INFINIBAND;
+                header->hlen = 0;
+
+                /* infiniband mandates to request broadcasts */
+                broadcast = true;
+                break;
+        default:
+                abort();
+                break;
+        }
+
+        if (connection->client_ip != INADDR_ANY) {
+                header->ciaddr = connection->client_ip;
+        } else {
+                /*
+                 * When the IP stack has not been configured, we may
+                 * not be able to receive unicast packets, depending
+                 * on the hardware. If that is the case we must request
+                 * replies from the server to be broadcast.
+                 *
+                 * Once the IP stack has been configured, receiving
+                 * unicast packets is never a problem, so the broadcast
+                 * flag should not be set.
+                 */
+                if (broadcast)
+                        header->flags |= N_DHCP4_MESSAGE_FLAG_BROADCAST;
+        }
+}
+
+static int n_dhcp4_c_connection_new_message(NDhcp4CConnection *connection,
+                                            NDhcp4Outgoing **messagep,
+                                            uint8_t type) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
+        NDhcp4Header *header;
+        uint8_t message_type;
+        bool via_packet_socket = false;
+        int r;
+
+        switch (type) {
+        case N_DHCP4_C_MESSAGE_DISCOVER:
+                message_type = N_DHCP4_MESSAGE_DISCOVER;
+                via_packet_socket = true;
+                break;
+        case N_DHCP4_C_MESSAGE_INFORM:
+                message_type = N_DHCP4_MESSAGE_INFORM;
+                break;
+        case N_DHCP4_C_MESSAGE_SELECT:
+                message_type = N_DHCP4_MESSAGE_REQUEST;
+                via_packet_socket = true;
+                break;
+        case N_DHCP4_C_MESSAGE_RENEW:
+                message_type = N_DHCP4_MESSAGE_REQUEST;
+                break;
+        case N_DHCP4_C_MESSAGE_REBIND:
+                message_type = N_DHCP4_MESSAGE_REQUEST;
+                break;
+        case N_DHCP4_C_MESSAGE_REBOOT:
+                message_type = N_DHCP4_MESSAGE_REQUEST;
+                via_packet_socket = true;
+                break;
+        case N_DHCP4_C_MESSAGE_RELEASE:
+                message_type = N_DHCP4_MESSAGE_RELEASE;
+                break;
+        case N_DHCP4_C_MESSAGE_DECLINE:
+                message_type = N_DHCP4_MESSAGE_DECLINE;
+                via_packet_socket = true;
+                break;
+        default:
+                abort();
+                return -ENOTRECOVERABLE;
+        }
+
+        /*
+         * We explicitly pass 0 as maximum message size, which makes
+         * NDhcp4Outgoing use the mandated default value from the spec (see its
+         * implementation). While the transport and like layers might support
+         * bigger MTUs (and we very likely know about them through
+         * n_dhcp4_client_update_mtu()), we cannot assume the target DHCP
+         * server supports parsing packets bigger than the minimum (and it is
+         * allowed to refuse bigger IP packets, even if the network supports
+         * transmission of them).
+         *
+         * We could theoretically increase this for packets other than the
+         * initial discovery. However, clients are unlikely to ever send large
+         * packets, so we just keep the same default for all outgoing packets.
+         */
+        r = n_dhcp4_outgoing_new(&message, 0, N_DHCP4_OVERLOAD_FILE | N_DHCP4_OVERLOAD_SNAME);
+        if (r)
+                return r;
+
+        header = n_dhcp4_outgoing_get_header(message);
+        n_dhcp4_c_connection_init_header(connection, header);
+
+        message->userdata.type = type;
+
+        /*
+         * Note that some implementations expect the MESSAGE_TYPE option to be
+         * the first option, and possibly even hard-code access to it. Hence,
+         * we really should make sure to pass it first as well.
+         */
+        r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_MESSAGE_TYPE, &message_type, sizeof(message_type));
+        if (r)
+                return r;
+
+        r = n_dhcp4_outgoing_append(message,
+                                    N_DHCP4_OPTION_CLIENT_IDENTIFIER,
+                                    connection->client_config->client_id,
+                                    connection->client_config->n_client_id);
+        if (r)
+                return r;
+
+        switch (message_type) {
+        case N_DHCP4_MESSAGE_DISCOVER:
+        case N_DHCP4_MESSAGE_REQUEST:
+        case N_DHCP4_MESSAGE_INFORM: {
+                uint16_t mtu;
+
+                if (connection->probe_config->n_request_parameters > 0) {
+                        r = n_dhcp4_outgoing_append(message,
+                                                    N_DHCP4_OPTION_PARAMETER_REQUEST_LIST,
+                                                    connection->probe_config->request_parameters,
+                                                    connection->probe_config->n_request_parameters);
+                        if (r)
+                                return r;
+                }
+
+                if (via_packet_socket) {
+                        /*
+                         * In case of packet sockets, we do not support
+                         * fragmentation. Hence, our maximum message size
+                         * equals the transport MTU. In case no mtu is given,
+                         * we use the minimum size mandated by the IP spec. If
+                         * we omit the field, some implementations will
+                         * interpret this to mean any packet size is supported,
+                         * which we rather not want as default behavior (we can
+                         * always support suppressing this field, if that is
+                         * what the caller wants).
+                         */
+                        mtu = htons(connection->mtu ?: N_DHCP4_NETWORK_IP_MINIMUM_MAX_SIZE);
+                        r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE, &mtu, sizeof(mtu));
+                        if (r)
+                                return r;
+                } else {
+                        /*
+                         * Once we use UDP sockets, we support fragmentation
+                         * through the kernel IP stack. This means, the biggest
+                         * message we can receive is the maximum UDP size plus
+                         * the possible IP header. This would sum up to
+                         * 2^16-1 + 20 (or even 2^16-1 + 60 if pedantic) and
+                         * thus exceed the option field. Hence, we simply set
+                         * the option to the maximum possible value.
+                         */
+                        mtu = htons(UINT16_MAX);
+                        r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE, &mtu, sizeof(mtu));
+                        if (r)
+                                return r;
+                }
+
+                break;
+        }
+        default:
+                break;
+        }
+
+        *messagep = message;
+        message = NULL;
+        return 0;
+}
+
+/*
+ *      RFC2131 3.1
+ *
+ *      The client broadcasts a DHCPDISCOVER message on its local physical
+ *      subnet.  The DHCPDISCOVER message MAY include options that suggest
+ *      values for the network address and lease duration.  BOOTP relay
+ *      agents may pass the message on to DHCP servers not on the same
+ *      physical subnet.
+ *
+ *      RFC2131 3.5
+ *
+ *      [...] in its initial DHCPDISCOVER or DHCPREQUEST message, a client
+ *      may provide the server with a list of specific parameters the
+ *      client is interested in.  If the client includes a list of
+ *      parameters in a DHCPDISCOVER message, it MUST include that list in
+ *      any subsequent DHCPREQUEST messages.
+ *
+ *      [...]
+ *
+ *      In addition, the client may suggest values for the network address
+ *      and lease time in the DHCPDISCOVER message.  The client may include
+ *      the 'requested IP address' option to suggest that a particular IP
+ *      address be assigned, and may include the 'IP address lease time'
+ *      option to suggest the lease time it would like.  Other options
+ *      representing "hints" at configuration parameters are allowed in a
+ *      DHCPDISCOVER or DHCPREQUEST message.
+ *
+ *      RFC2131 4.4.1
+ *
+ *      The client generates and records a random transaction identifier and
+ *      inserts that identifier into the 'xid' field.  The client records its
+ *      own local time for later use in computing the lease expiration.  The
+ *      client then broadcasts the DHCPDISCOVER on the local hardware
+ *      broadcast address to the 0xffffffff IP broadcast address and 'DHCP
+ *      server' UDP port.
+ *
+ *      If the 'xid' of an arriving DHCPOFFER message does not match the
+ *      'xid' of the most recent DHCPDISCOVER message, the DHCPOFFER message
+ *      must be silently discarded.  Any arriving DHCPACK messages must be
+ *      silently discarded.
+ */
+int n_dhcp4_c_connection_discover_new(NDhcp4CConnection *connection,
+                                      NDhcp4Outgoing **requestp) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_DISCOVER);
+        if (r)
+                return r;
+
+        *requestp = message;
+        message = NULL;
+        return 0;
+}
+
+/*
+ *
+ *      RFC2131 4.1.1
+ *
+ *      The DHCPREQUEST message contains the same 'xid' as the DHCPOFFER
+ *      message.
+ *
+ *      RFC2131 4.3.2
+ *
+ *      Client inserts the address of the selected server in 'server
+ *      identifier', 'ciaddr' MUST be zero, 'requested IP address' MUST be
+ *      filled in with the yiaddr value from the chosen DHCPOFFER.
+ */
+int n_dhcp4_c_connection_select_new(NDhcp4CConnection *connection,
+                                    NDhcp4Outgoing **requestp,
+                                    NDhcp4Incoming *offer) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
+        struct in_addr client;
+        struct in_addr server;
+        uint32_t xid;
+        int r;
+
+        n_dhcp4_incoming_get_yiaddr(offer, &client);
+
+        r = n_dhcp4_incoming_query_server_identifier(offer, &server);
+        if (r)
+                return r;
+
+        r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_SELECT);
+        if (r)
+                return r;
+
+        r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_REQUESTED_IP_ADDRESS, &client, sizeof(client));
+        if (r)
+                return r;
+
+        r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_SERVER_IDENTIFIER, &server, sizeof(server));
+        if (r)
+                return r;
+
+        /*
+         * SELECT continues the transaction started by DISCOVER, and as such
+         * keeps the same start time. We also have to preserve the base time
+         * of the selected lease as well as the transaction ID.
+         */
+        message->userdata.start_time = offer->userdata.start_time;
+        message->userdata.base_time = offer->userdata.base_time;
+        n_dhcp4_incoming_get_xid(offer, &xid);
+        n_dhcp4_outgoing_set_xid(message, xid);
+
+        *requestp = message;
+        message = NULL;
+        return 0;
+}
+
+/*
+ *      RFC2131 4.3.2
+ *
+ *      'server identifier' MUST NOT be filled in, 'requested IP address'
+ *      option MUST be filled in with client's notion of its previously
+ *      assigned address. 'ciaddr' MUST be zero. The client is seeking to
+ *      verify a previously allocated, cached configuration. Server SHOULD
+ *      send a DHCPNAK message to the client if the 'requested IP address'
+ *      is incorrect, or is on the wrong network.
+ */
+int n_dhcp4_c_connection_reboot_new(NDhcp4CConnection *connection,
+                                    NDhcp4Outgoing **requestp,
+                                    const struct in_addr *client) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_REBOOT);
+        if (r)
+                return r;
+
+        r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_REQUESTED_IP_ADDRESS, client, sizeof(*client));
+        if (r)
+                return r;
+
+        *requestp = message;
+        message = NULL;
+        return 0;
+}
+
+/*
+ *      RFC2131 4.3.2
+ *
+ *      'server identifier' MUST NOT be filled in, 'requested IP address'
+ *      option MUST NOT be filled in, 'ciaddr' MUST be filled in with
+ *      client's IP address. In this situation, the client is completely
+ *      configured, and is trying to extend its lease. This message will
+ *      be unicast, so no relay agents will be involved in its
+ *      transmission.  Because 'giaddr' is therefore not filled in, the
+ *      DHCP server will trust the value in 'ciaddr', and use it when
+ *      replying to the client.
+ *
+ *      A client MAY choose to renew or extend its lease prior to T1.  The
+ *      server may choose not to extend the lease (as a policy decision by
+ *      the network administrator), but should return a DHCPACK message
+ *      regardless.
+ *
+ *      RFC2131 4.4.5
+ *
+ *      At time T1 the client moves to RENEWING state and sends (via unicast)
+ *      a DHCPREQUEST message to the server to extend its lease.  The client
+ *      sets the 'ciaddr' field in the DHCPREQUEST to its current network
+ *      address. The client records the local time at which the DHCPREQUEST
+ *      message is sent for computation of the lease expiration time.  The
+ *      client MUST NOT include a 'server identifier' in the DHCPREQUEST
+ *      message.
+ */
+int n_dhcp4_c_connection_renew_new(NDhcp4CConnection *connection,
+                                   NDhcp4Outgoing **requestp) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_RENEW);
+        if (r)
+                return r;
+
+        *requestp = message;
+        message = NULL;
+        return 0;
+}
+
+/*
+ *      RFC2131 4.3.2
+ *
+ *      'server identifier' MUST NOT be filled in, 'requested IP address'
+ *      option MUST NOT be filled in, 'ciaddr' MUST be filled in with
+ *      client's IP address. In this situation, the client is completely
+ *      configured, and is trying to extend its lease. This message MUST
+ *      be broadcast to the 0xffffffff IP broadcast address.  The DHCP
+ *      server SHOULD check 'ciaddr' for correctness before replying to
+ *      the DHCPREQUEST.
+ *
+ *      RFC2131 4.4.5
+ *
+ *      If no DHCPACK arrives before time T2, the client moves to REBINDING
+ *      state and sends (via broadcast) a DHCPREQUEST message to extend its
+ *      lease.  The client sets the 'ciaddr' field in the DHCPREQUEST to its
+ *      current network address.  The client MUST NOT include a 'server
+ *      identifier' in the DHCPREQUEST message.
+ */
+int n_dhcp4_c_connection_rebind_new(NDhcp4CConnection *connection,
+                                    NDhcp4Outgoing **requestp) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_REBIND);
+        if (r)
+                return r;
+
+        *requestp = message;
+        message = NULL;
+        return 0;
+}
+
+/*
+ *      RFC2131 3.2
+ *
+ *      If the client detects that the IP address in the DHCPACK message
+ *      is already in use, the client MUST send a DHCPDECLINE message to the
+ *      server and restarts the configuration process by requesting a
+ *      new network address.
+ *
+ *      RFC2131 4.4.4
+ *
+ *      Because the client is declining the use of the IP address supplied by
+ *      the server, the client broadcasts DHCPDECLINE messages.
+ */
+int n_dhcp4_c_connection_decline_new(NDhcp4CConnection *connection,
+                                     NDhcp4Outgoing **requestp,
+                                     NDhcp4Incoming *ack,
+                                     const char *error) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
+        struct in_addr client;
+        struct in_addr server;
+        int r;
+
+        n_dhcp4_incoming_get_yiaddr(ack, &client);
+
+        r = n_dhcp4_incoming_query_server_identifier(ack, &server);
+        if (r)
+                return r;
+
+        r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_DECLINE);
+        if (r)
+                return r;
+
+        r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_REQUESTED_IP_ADDRESS, &client, sizeof(client));
+        if (r)
+                return r;
+
+        r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_SERVER_IDENTIFIER, &server, sizeof(server));
+        if (r)
+                return r;
+
+        if (error) {
+                r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_ERROR_MESSAGE, error, strlen(error) + 1);
+                if (r)
+                        return r;
+        }
+
+        *requestp = message;
+        message = NULL;
+        return 0;
+}
+
+/*
+ *      RFC2131 3.4
+ *
+ *      If a client has obtained a network address through some other means
+ *      (e.g., manual configuration), it may use a DHCPINFORM request message
+ *      to obtain other local configuration parameters.
+ *
+ *      RFC2131 4.4
+ *
+ *      The DHCPINFORM message is not shown in figure 5.  A client simply
+ *      sends the DHCPINFORM and waits for DHCPACK messages.  Once the client
+ *      has selected its parameters, it has completed the configuration
+ *      process.
+ *
+ *      RFC2131 4.4.3
+ *
+ *      The client sends a DHCPINFORM message. The client may request
+ *      specific configuration parameters by including the 'parameter request
+ *      list' option. The client generates and records a random transaction
+ *      identifier and inserts that identifier into the 'xid' field. The
+ *      client places its own network address in the 'ciaddr' field. The
+ *      client SHOULD NOT request lease time parameters.
+ *
+ *      The client then unicasts the DHCPINFORM to the DHCP server if it
+ *      knows the server's address, otherwise it broadcasts the message to
+ *      the limited (all 1s) broadcast address.  DHCPINFORM messages MUST be
+ *      directed to the 'DHCP server' UDP port.
+ */
+int n_dhcp4_c_connection_inform_new(NDhcp4CConnection *connection,
+                                    NDhcp4Outgoing **requestp) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_INFORM);
+        if (r)
+                return r;
+
+        *requestp = message;
+        message = NULL;
+        return 0;
+}
+
+/*
+ *      RFC2131 3.1
+ *
+ *      The client may choose to relinquish its lease on a network address
+ *      by sending a DHCPRELEASE message to the server.  The client
+ *      identifies the lease to be released with its 'client identifier',
+ *      or 'chaddr' and network address in the DHCPRELEASE message. If the
+ *      client used a 'client identifier' when it obtained the lease, it
+ *      MUST use the same 'client identifier' in the DHCPRELEASE message.
+ *
+ *      RFC2131 3.2
+ *
+ *      The client may choose to relinquish its lease on a network
+ *      address by sending a DHCPRELEASE message to the server.  The
+ *      client identifies the lease to be released with its
+ *      'client identifier', or 'chaddr' and network address in the
+ *      DHCPRELEASE message.
+ *
+ *      Note that in this case, where the client retains its network
+ *      address locally, the client will not normally relinquish its
+ *      lease during a graceful shutdown.  Only in the case where the
+ *      client explicitly needs to relinquish its lease, e.g., the client
+ *      is about to be moved to a different subnet, will the client send
+ *      a DHCPRELEASE message.
+ *
+ *      RFC2131 4.4.4
+ *
+ *      The client unicasts DHCPRELEASE messages to the server.
+ *
+ *      RFC2131 4.4.6
+ *
+ *      If the client no longer requires use of its assigned network address
+ *      (e.g., the client is gracefully shut down), the client sends a
+ *      DHCPRELEASE message to the server.  Note that the correct operation
+ *      of DHCP does not depend on the transmission of DHCPRELEASE messages.
+ */
+int n_dhcp4_c_connection_release_new(NDhcp4CConnection *connection,
+                                     NDhcp4Outgoing **requestp,
+                                     const char *error) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_RELEASE);
+        if (r)
+                return r;
+
+        r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_SERVER_IDENTIFIER, &connection->server_ip, sizeof(connection->server_ip));
+        if (r)
+                return r;
+
+        if (error) {
+                r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_ERROR_MESSAGE, error, strlen(error) + 1);
+                if (r)
+                        return r;
+        }
+
+        *requestp = message;
+        message = NULL;
+        return 0;
+}
+
+static int n_dhcp4_c_connection_send_request(NDhcp4CConnection *connection,
+                                             NDhcp4Outgoing *request,
+                                             uint64_t timestamp) {
+        int r;
+
+        /*
+         * Increment the base time and reset the xid field,
+         * where applicable. We never alter the header on
+         * resends of SELECT, as it must always match the
+         * OFFER message they are in reply to.
+         */
+        switch (request->userdata.type) {
+        case N_DHCP4_C_MESSAGE_DISCOVER:
+        case N_DHCP4_C_MESSAGE_INFORM:
+        case N_DHCP4_C_MESSAGE_REBOOT:
+        case N_DHCP4_C_MESSAGE_REBIND:
+        case N_DHCP4_C_MESSAGE_RENEW:
+                request->userdata.base_time = timestamp;
+                n_dhcp4_outgoing_set_xid(request, n_dhcp4_client_probe_config_get_random(connection->probe_config));
+
+                break;
+        case N_DHCP4_C_MESSAGE_SELECT:
+        case N_DHCP4_C_MESSAGE_DECLINE:
+        case N_DHCP4_C_MESSAGE_RELEASE:
+                break;
+        default:
+                c_assert(0);
+        }
+
+        request->userdata.send_time = timestamp;
+        request->userdata.send_jitter = (n_dhcp4_client_probe_config_get_random(connection->probe_config) % 1000000000ULL);
+        n_dhcp4_c_connection_outgoing_set_secs(request);
+
+        switch (request->userdata.type) {
+        case N_DHCP4_C_MESSAGE_DISCOVER:
+        case N_DHCP4_C_MESSAGE_SELECT:
+        case N_DHCP4_C_MESSAGE_REBOOT:
+        case N_DHCP4_C_MESSAGE_DECLINE:
+                r = n_dhcp4_c_connection_packet_broadcast(connection, request);
+                if (r)
+                        return r;
+                break;
+        case N_DHCP4_C_MESSAGE_INFORM:
+        case N_DHCP4_C_MESSAGE_REBIND:
+                r = n_dhcp4_c_connection_udp_broadcast(connection, request);
+                if (r)
+                        return r;
+
+                break;
+        case N_DHCP4_C_MESSAGE_RENEW:
+        case N_DHCP4_C_MESSAGE_RELEASE:
+                r = n_dhcp4_c_connection_udp_send(connection, request);
+                if (r)
+                        return r;
+
+                break;
+        default:
+                c_assert(0);
+        }
+
+        ++request->userdata.n_send;
+        return 0;
+}
+
+int n_dhcp4_c_connection_start_request(NDhcp4CConnection *connection,
+                                       NDhcp4Outgoing *request,
+                                       uint64_t timestamp) {
+        int r;
+
+        /*
+         * This function starts a request, but in the case of SELECT it
+         * continues a previous transaction, so we do not want to reset
+         * the start time. Only set the start time if it was not already
+         * set.
+         */
+        if (request->userdata.start_time == 0)
+                request->userdata.start_time = timestamp;
+
+        n_dhcp4_outgoing_free(connection->request);
+        connection->request = request;
+
+        r = n_dhcp4_c_connection_send_request(connection, request, timestamp);
+        if (r)
+                return r;
+
+        return 0;
+}
+
+int n_dhcp4_c_connection_dispatch_timer(NDhcp4CConnection *connection,
+                                        uint64_t timestamp) {
+        uint64_t timeout;
+        int r;
+
+        if (!connection->request)
+                return 0;
+
+        n_dhcp4_c_connection_get_timeout(connection, &timeout);
+
+        if (timeout > timestamp)
+                return 0;
+
+        r = n_dhcp4_c_connection_send_request(connection, connection->request, timestamp);
+        if (r)
+                return r;
+
+        return 0;
+}
+
+int n_dhcp4_c_connection_dispatch_io(NDhcp4CConnection *connection,
+                                     NDhcp4Incoming **messagep) {
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL;
+        uint8_t type;
+        int r;
+
+        switch (connection->state) {
+        case N_DHCP4_C_CONNECTION_STATE_PACKET:
+                r = n_dhcp4_c_socket_packet_recv(connection->fd_packet,
+                                                 connection->scratch_buffer,
+                                                 sizeof(connection->scratch_buffer),
+                                                 &message);
+                if (r)
+                        return r;
+
+                break;
+        case N_DHCP4_C_CONNECTION_STATE_DRAINING:
+                r = n_dhcp4_c_socket_packet_recv(connection->fd_packet,
+                                                 connection->scratch_buffer,
+                                                 sizeof(connection->scratch_buffer),
+                                                 &message);
+                if (!r)
+                        break;
+                else if (r != N_DHCP4_E_AGAIN)
+                        return r;
+
+                /*
+                 * The UDP socket is open and the packet socket has been shut down
+                 * and drained, clean up the packet socket and fall through to
+                 * dispatching the UDP socket.
+                 */
+                r = epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, connection->fd_packet, NULL);
+                c_assert(!r);
+                connection->fd_packet = c_close(connection->fd_packet);
+                connection->state = N_DHCP4_C_CONNECTION_STATE_UDP;
+
+                /* fall-through */
+        case N_DHCP4_C_CONNECTION_STATE_UDP:
+                r = n_dhcp4_c_socket_udp_recv(connection->fd_udp,
+                                              connection->scratch_buffer,
+                                              sizeof(connection->scratch_buffer),
+                                              &message);
+                if (r)
+                        return r;
+
+                break;
+        default:
+                abort();
+                return -ENOTRECOVERABLE;
+        }
+
+        r = n_dhcp4_c_connection_verify_incoming(connection, message, &type);
+        if (r)
+                return r;
+
+        switch (type) {
+        case N_DHCP4_MESSAGE_OFFER:
+        case N_DHCP4_MESSAGE_ACK:
+        case N_DHCP4_MESSAGE_NAK:
+                /*
+                 * Remember the start time of the transaction, and the base
+                 * time of any relative timestamps from the pending request.
+                 * Thes same times applies to the response, and sholud be
+                 * copied over.
+                 */
+                message->userdata.start_time = connection->request->userdata.start_time;
+                message->userdata.base_time = connection->request->userdata.base_time;
+
+                if (type != N_DHCP4_MESSAGE_OFFER) {
+                        /*
+                         * We only allow one reply to ACK or NAK, but for OFFER we must
+                         * accept several, so we do not free the pinned request.
+                         */
+                        connection->request = n_dhcp4_outgoing_free(connection->request);
+                }
+
+                break;
+        default:
+                break;
+        }
+
+        *messagep = message;
+        message = NULL;
+        return 0;
+}
diff --git a/shared/n-dhcp4/src/n-dhcp4-c-lease.c b/shared/n-dhcp4/src/n-dhcp4-c-lease.c
new file mode 100644
index 00000000..db1ffbae
--- /dev/null
+++ b/shared/n-dhcp4/src/n-dhcp4-c-lease.c
@@ -0,0 +1,356 @@
+/*
+ * DHCP4 Client Leases
+ *
+ * This implements the public API wrapping DHCP4 client leases. A lease object
+ * conists of the information given to us from the server, together with the
+ * timestamp recording the start of the validity of the lease.
+ *
+ * A probe may yield many OFFERS, each of which contains a lease object. One of
+ * these offers may be SELECTED, which implicitly rejects all the others.
+ * The server may then ACK or NAK the lease which tells us whether or not we
+ * are permitted to start using it. Once an ACK has been received, we can
+ * configure the address, and only then can we SELECT the lease. If we
+ * determine that the offered lease was not appropriate after all we
+ * may DECLINE it instead.
+ */
+
+#include <assert.h>
+#include <c-list.h>
+#include <c-stdaux.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include "n-dhcp4.h"
+#include "n-dhcp4-private.h"
+
+/*
+ * Compute the absolute timeouts from an incoming message. A message contains relative timeouts and the userdata
+ * of the incoming message is set to the offset we must apply to get the absolute values.
+ *
+ * The special value UINT64_MAX is returned to indicate no or infinite timeouts. In case the given timeouts
+ * are invalid relative to each other, we recompute T1 and/or T2 to take their default values. Later timeouts
+ * take predecende above earlier ones (T1 is adjusted if it conflicts with T2, etc).
+ */
+static int n_dhcp4_incoming_get_timeouts(NDhcp4Incoming *message, uint64_t *t1p, uint64_t *t2p, uint64_t *lifetimep) {
+        uint64_t lifetime, t2, t1;
+        uint32_t u32;
+        int r;
+
+        r = n_dhcp4_incoming_query_lifetime(message, &u32);
+        if (r == N_DHCP4_E_UNSET) {
+                lifetime = UINT64_MAX;
+        } else if (r) {
+                return r;
+        } else if (u32 == UINT32_MAX) {
+                lifetime = UINT64_MAX;
+        } else {
+                if (u32 == UINT32_MAX)
+                        lifetime = UINT64_MAX;
+                else
+                        lifetime = u32 * (1000000000ULL);
+        }
+
+        r = n_dhcp4_incoming_query_t2(message, &u32);
+        if (r == N_DHCP4_E_UNSET) {
+                if (lifetime == UINT64_MAX)
+                        t2 = UINT64_MAX;
+                else
+                        t2 = (lifetime * 7) / 8;
+        } else if (r) {
+                return r;
+        } else {
+                if (u32 == UINT32_MAX)
+                        t2 = UINT64_MAX;
+                else
+                        t2 = u32 * (1000000000ULL);
+
+                if (t2 > lifetime)
+                        t2 = (lifetime * 7) / 8;
+        }
+
+        r = n_dhcp4_incoming_query_t1(message, &u32);
+        if (r == N_DHCP4_E_UNSET) {
+                if (t2 == UINT64_MAX)
+                        t1 = UINT64_MAX;
+                else
+                        t1 = (t2 * 4) / 7;
+        } else if (r) {
+                return r;
+        } else {
+                if (u32 == UINT32_MAX)
+                        t1 = UINT64_MAX;
+                else
+                        t1 = u32 * (1000000000ULL);
+
+                if (t1 > t2)
+                        t1 = (t2 * 4) / 7;
+        }
+
+        if (lifetime != UINT64_MAX)
+                lifetime += message->userdata.base_time;
+        if (t2 != UINT64_MAX)
+                t2 += message->userdata.base_time;
+        if (t1 != UINT64_MAX)
+                t1 += message->userdata.base_time;
+
+        *lifetimep = lifetime;
+        *t2p = t2;
+        *t1p = t1;
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_lease_new() - allocate new client lease object
+ * @leasep:                     output argumnet for new client lease object
+ * @message:                    incoming message representing the lease
+ *
+ * This creates a new client lease object. Client lease objects are simple
+ * wrappers around an incoming message representing a lease.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int n_dhcp4_client_lease_new(NDhcp4ClientLease **leasep, NDhcp4Incoming *message) {
+        _c_cleanup_(n_dhcp4_client_lease_unrefp) NDhcp4ClientLease *lease = NULL;
+        int r;
+
+        c_assert(leasep);
+
+        lease = malloc(sizeof(*lease));
+        if (!lease)
+                return -ENOMEM;
+
+        *lease = (NDhcp4ClientLease)N_DHCP4_CLIENT_LEASE_NULL(*lease);
+
+        r = n_dhcp4_incoming_get_timeouts(message, &lease->t1, &lease->t2, &lease->lifetime);
+        if (r)
+                return r;
+
+        lease->message = message;
+        *leasep = lease;
+        lease = NULL;
+        return 0;
+}
+
+static void n_dhcp4_client_lease_free(NDhcp4ClientLease *lease) {
+        n_dhcp4_client_lease_unlink(lease);
+        n_dhcp4_incoming_free(lease->message);
+        free(lease);
+}
+
+/**
+ * n_dhcp4_client_lease_ref() - reference client lease
+ * @lease:                      the client lease object to reference
+ *
+ * Take a new reference to a client lease.
+ *
+ * Return: the lease.
+ */
+_c_public_ NDhcp4ClientLease *n_dhcp4_client_lease_ref(NDhcp4ClientLease *lease) {
+        if (lease)
+                ++lease->n_refs;
+        return lease;
+}
+
+/**
+ * n_dhcp4_client_lease_unref() - dereference client lease
+ * @lease:                      the client lease object to dereference
+ *
+ * Relase a reference to a client lease.
+ *
+ * Return: NULL.
+ */
+_c_public_ NDhcp4ClientLease *n_dhcp4_client_lease_unref(NDhcp4ClientLease *lease) {
+        if (lease && !--lease->n_refs)
+                n_dhcp4_client_lease_free(lease);
+        return NULL;
+}
+
+/**
+ * n_dhcp4_client_lease_link() - link lease into probe
+ * @lease:                      the lease to operate on
+ * @probe:                      the probe to link the lease into
+ *
+ * Associate a lease with a probe. The lease may not already be linked.
+ */
+void n_dhcp4_client_lease_link(NDhcp4ClientLease *lease, NDhcp4ClientProbe *probe) {
+        c_assert(!lease->probe);
+        c_assert(!c_list_is_linked(&lease->probe_link));
+
+        lease->probe = probe;
+        c_list_link_tail(&probe->lease_list, &lease->probe_link);
+}
+
+/**
+ * n_dhcp4_client_lease_unlink() - unlinke lease from its probe
+ * @lease:                      the lease to operate on
+ *
+ * Dissassociate a lease from a probe if it is associated with one. Otherwise,
+ * this is a noop.
+ */
+void n_dhcp4_client_lease_unlink(NDhcp4ClientLease *lease) {
+        lease->probe = NULL;
+        c_list_unlink(&lease->probe_link);
+}
+
+/**
+ * n_dhcp4_client_lease_get_yiaddr() - get the IP address
+ * @lease:                      the lease to operate on
+ * @yiaddr:                     return argument for the IP address
+ *
+ * Gets the IP address cotained in the lease. Or INADDR_ANY if the lease
+ * does not contain an IP address.
+ */
+_c_public_ void n_dhcp4_client_lease_get_yiaddr(NDhcp4ClientLease *lease, struct in_addr *yiaddr) {
+        NDhcp4Header *header = n_dhcp4_incoming_get_header(lease->message);
+
+        yiaddr->s_addr = header->yiaddr;
+}
+
+/**
+ * n_dhcp4_client_lease_get_lifetime() - get the lifetime
+ * @lease:                      the lease to operate on
+ * @ns_lifetimep:               return argument for the lifetime in nano seconds
+ *
+ * Gets the end of the lease's lifetime in nanoseconds according to CLOCK_BOOTTIME,
+ * or (uint64_t)-1 for permanent leases.
+ */
+_c_public_ void n_dhcp4_client_lease_get_lifetime(NDhcp4ClientLease *lease, uint64_t *ns_lifetimep) {
+        *ns_lifetimep = lease->lifetime;
+}
+
+/**
+ * n_dhcp4_client_lease_query() - query the lease for an option
+ * @lease:                      the lease to operate on
+ * @option:                     the DHCP4 option code
+ * @datap:                      return argument of the data pointer
+ * @n_datap:                    return argument of data length in bytes
+ *
+ * Query the lease for a given option. Options internal to the DHCP protocol cannot
+ * be queried, and only options that were explicitly requested can be queried.
+ *
+ * Return: 0 on success,
+ *         N_DCHP4_E_INTERNAL if an invalid option is queried,
+ *         N_DHCP4_E_UNSET if the lease did not contain the option, or
+ *         a negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_lease_query(NDhcp4ClientLease *lease, uint8_t option, uint8_t **datap, size_t *n_datap) {
+        switch (option) {
+        case N_DHCP4_OPTION_PAD:
+        case N_DHCP4_OPTION_REQUESTED_IP_ADDRESS:
+        case N_DHCP4_OPTION_IP_ADDRESS_LEASE_TIME:
+        case N_DHCP4_OPTION_OVERLOAD:
+        case N_DHCP4_OPTION_MESSAGE_TYPE:
+        case N_DHCP4_OPTION_SERVER_IDENTIFIER:
+        case N_DHCP4_OPTION_PARAMETER_REQUEST_LIST:
+        case N_DHCP4_OPTION_ERROR_MESSAGE:
+        case N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE:
+        case N_DHCP4_OPTION_RENEWAL_T1_TIME:
+        case N_DHCP4_OPTION_REBINDING_T2_TIME:
+        case N_DHCP4_OPTION_END:
+                return N_DHCP4_E_INTERNAL;
+        }
+
+        /* XXX: refuse to return options that were not requested */
+
+        return n_dhcp4_incoming_query(lease->message, option, datap, n_datap);
+}
+
+/**
+ * n_dhcp4_client_lease_select() - select an offered lease
+ * @lease:                      lease to operate on
+ *
+ * Select a lease. This must be a lease that was offered, once
+ * one of the leases that were offered in response to a probe was
+ * selected none of the others can be.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_lease_select(NDhcp4ClientLease *lease) {
+        NDhcp4ClientLease *l, *t_l;
+        NDhcp4ClientProbe *probe;
+        int r;
+
+        /* XXX error handling, this must be an OFFER */
+
+        if (!lease->probe)
+                return -ENOTRECOVERABLE;
+        if (lease->probe->current_lease)
+                return -ENOTRECOVERABLE;
+
+        r = n_dhcp4_client_probe_transition_select(lease->probe, lease->message, n_dhcp4_gettime(CLOCK_BOOTTIME));
+        if (r)
+                return r;
+
+        /*
+         * Only one of the offered leases can be selected, so flush the list.
+         * All offered lease, including this one are now dead.
+         */
+        probe = lease->probe;
+        c_list_for_each_entry_safe(l, t_l, &probe->lease_list, probe_link)
+                n_dhcp4_client_lease_unlink(l);
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_lease_accept() - accept an ack'ed lease
+ * @lease:                      lease to operate on
+ *
+ * Accept a lease. This must be a lease that was ack'ed by the
+ * server.
+ *
+ * The offered IP address must be fully configured before the lease
+ * can be accepted.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_lease_accept(NDhcp4ClientLease *lease) {
+        int r;
+
+        /* XXX error handling, this must be an ACK */
+
+        if (!lease->probe)
+                return -ENOTRECOVERABLE;
+        if (lease->probe->current_lease != lease)
+                return -ENOTRECOVERABLE;
+
+        r = n_dhcp4_client_probe_transition_accept(lease->probe, lease->message);
+        if (r)
+                return r;
+
+        n_dhcp4_client_lease_unlink(lease);
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_lease_decline() - decline an ack'ed lease
+ * @lease:                      lease to operate on
+ *
+ * Decline a lease. This must be a lease that was ack'ed by the
+ * server.
+ *
+ * The offered IP address must not be used once the lease has been
+ * decline.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_lease_decline(NDhcp4ClientLease *lease, const char *error) {
+        int r;
+
+        /* XXX: error handling, this must be an ACK */
+
+        if (!lease->probe)
+                return -ENOTRECOVERABLE;
+        if (lease->probe->current_lease != lease)
+                return -ENOTRECOVERABLE;
+
+        r = n_dhcp4_client_probe_transition_decline(lease->probe, lease->message, error, n_dhcp4_gettime(CLOCK_BOOTTIME));
+        if (r)
+                return r;
+
+        lease->probe->current_lease = n_dhcp4_client_lease_unref(lease->probe->current_lease);
+        n_dhcp4_client_lease_unlink(lease);
+
+        return 0;
+}
diff --git a/shared/n-dhcp4/src/n-dhcp4-c-probe.c b/shared/n-dhcp4/src/n-dhcp4-c-probe.c
new file mode 100644
index 00000000..308cff83
--- /dev/null
+++ b/shared/n-dhcp4/src/n-dhcp4-c-probe.c
@@ -0,0 +1,1203 @@
+/*
+ * DHCPv4 Client Probes
+ *
+ * The probe object is used to represent the lifetime of a DHCP client session.
+ * A running probe discovers DHCP servers, requests a lease, and maintains that
+ * lease.
+ */
+
+#include <assert.h>
+#include <c-list.h>
+#include <c-siphash.h>
+#include <c-stdaux.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/auxv.h>
+#include "n-dhcp4.h"
+#include "n-dhcp4-private.h"
+
+
+static int n_dhcp4_client_probe_option_new(NDhcp4ClientProbeOption **optionp,
+                                    uint8_t option,
+                                    const void *data,
+                                    uint8_t n_data) {
+        NDhcp4ClientProbeOption *op;
+
+        op = malloc(sizeof(op) + n_data);
+        if (!op)
+                return -ENOMEM;
+
+        op->option = option;
+        op->n_data = n_data;
+        memcpy(op->data, data, n_data);
+
+        *optionp = op;
+        return 0;
+}
+
+static void n_dhcp4_client_probe_option_free(NDhcp4ClientProbeOption *option) {
+        if (option)
+                free(option);
+}
+
+/**
+ * n_dhcp4_client_probe_config_new() - create new probe configuration
+ * @configp:                    output argument to store new configuration
+ *
+ * This creates a new probe configuration object. The object is a collection of
+ * parameters for probes. No data verification is done by the configuration
+ * object. Instead, when passing the configuration to the constructor of a
+ * probe, this constructor will perform parameter validation.
+ *
+ * A probe configuration is an unlinked object only used to pass information to
+ * a probe constructor. The caller fully owns the returned configuration object
+ * and is responsible to free it when no longer needed.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_probe_config_new(NDhcp4ClientProbeConfig **configp) {
+        _c_cleanup_(n_dhcp4_client_probe_config_freep) NDhcp4ClientProbeConfig *config = NULL;
+
+        config = calloc(1, sizeof(*config));
+        if (!config)
+                return -ENOMEM;
+
+        *config = (NDhcp4ClientProbeConfig)N_DHCP4_CLIENT_PROBE_CONFIG_NULL(*config);
+
+        *configp = config;
+        config = NULL;
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_probe_config_free() - destroy probe configuration
+ * @config:                     configuration to operate on, or NULL
+ *
+ * This destroys a probe configuration object and deallocates all its
+ * resources.
+ *
+ * If @config is NULL, this is a no-op.
+ *
+ * Return: NULL is returned.
+ */
+_c_public_ NDhcp4ClientProbeConfig *n_dhcp4_client_probe_config_free(NDhcp4ClientProbeConfig *config) {
+        if (!config)
+                return NULL;
+
+        for (unsigned int i = 0; i <= UINT8_MAX; ++i)
+                n_dhcp4_client_probe_option_free(config->options[i]);
+
+        free(config);
+
+        return NULL;
+}
+
+/**
+ * n_dhcp4_client_probe_config_dup() - duplicate probe configuration
+ * @config:                     configuration to operate on
+ * @dupp:                       output argument for duplicate
+ *
+ * This duplicates the probe configuration given as @config and returns it in
+ * @dupp to the caller.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int n_dhcp4_client_probe_config_dup(NDhcp4ClientProbeConfig *config,
+                                    NDhcp4ClientProbeConfig **dupp) {
+        _c_cleanup_(n_dhcp4_client_probe_config_freep) NDhcp4ClientProbeConfig *dup = NULL;
+        int r;
+
+        r = n_dhcp4_client_probe_config_new(&dup);
+        if (r)
+                return r;
+
+        dup->inform_only = config->inform_only;
+        dup->init_reboot = config->init_reboot;
+        dup->requested_ip = config->requested_ip;
+        dup->ms_start_delay = config->ms_start_delay;
+
+        for (unsigned int i = 0; i < config->n_request_parameters; ++i)
+                dup->request_parameters[dup->n_request_parameters++] = config->request_parameters[i];
+
+        for (unsigned int i = 0; i <= UINT8_MAX; ++i) {
+                if (!config->options[i])
+                        break;
+
+                r = n_dhcp4_client_probe_option_new(&dup->options[i],
+                                                    config->options[i]->option,
+                                                    config->options[i]->data,
+                                                    config->options[i]->n_data);
+                if (r)
+                        return r;
+        }
+
+        *dupp = dup;
+        dup = NULL;
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_probe_config_set_inform_only() - set inform-only property
+ * @config:                     configuration to operate on
+ * @inform_only:                value to set
+ *
+ * This sets the inform-only property of the given configuration object. This
+ * property controls whether the client probe should request a full lease, or
+ * whether it should just ask for auxiliary information without requesting an
+ * address.
+ *
+ * The default is to request a full lease and address. If inform-only is set to
+ * true, only auxiliary information will be requested.
+ *
+ * XXX: This is currently not implemented, and setting the property has no effect.
+ */
+_c_public_ void n_dhcp4_client_probe_config_set_inform_only(NDhcp4ClientProbeConfig *config, bool inform_only) {
+        config->inform_only = inform_only;
+}
+
+/**
+ * n_dhcp4_client_probe_config_set_init_reboot() - set init-reboot property
+ * @config:                     configuration to operate on
+ * @init_reboot:                value to set
+ *
+ * This sets the init-reboot property of the given configuration object. If this
+ * is enabled, a requested IP address must also be set.
+ *
+ * The default is false. If set to true, a probe will make use of the
+ * INIT-REBOOT path, as described by the DHCP specification. In most cases, you
+ * do not want this.
+ *
+ * XXX: This is currently not implemented, and setting the property has no effect.
+ *
+ * Background: The INIT-REBOOT path allows a DHCP client to skip
+ *             server-discovery when rebooting/resuming their machine. The DHCP
+ *             client simply re-requests the lease it had acquired before. This
+ *             saves one roundtrip in the success-case, since the DISCOVER step
+ *             is skipped. However, there are little to no timeouts involved,
+ *             so the roundtrip should be barely noticeable. In contrast, if
+ *             the INIT-REBOOT fails (because the lease is no longer valid, or
+ *             not valid on this network), the client has to wait for a
+ *             possible answer to the request before actually starting the DHCP
+ *             process all over. This significantly increases the time needed
+ *             to switch networks.
+ *             The INIT-REBOOT state might have been a real improvements with
+ *             the old resend-timeouts mandated by the DHCP specification.
+ *             However, on modern networks with improved timeout values we
+ *             recommend against using it.
+ */
+_c_public_ void n_dhcp4_client_probe_config_set_init_reboot(NDhcp4ClientProbeConfig *config, bool init_reboot) {
+        config->init_reboot = init_reboot;
+}
+
+/**
+ * n_dhcp4_client_probe_config_set_requested_ip() - set requested-ip property
+ * @config:                     configuration to operate on
+ * @ip:                         value to set
+ *
+ * This sets the requested-ip property of the given configuration object.
+ *
+ * The default is all 0. If set to something else, the DHCP discovery will
+ * include this IP in its requests to tell DHCP servers which address to pick.
+ * Servers are not required to honor this, nor does this have any effect on
+ * servers not serving this address.
+ *
+ * This field should always be set if the caller knows of an address that was
+ * previously acquired on this network. It serves as hint to servers and will
+ * allow them to provide the same address again.
+ */
+_c_public_ void n_dhcp4_client_probe_config_set_requested_ip(NDhcp4ClientProbeConfig *config, struct in_addr ip) {
+        config->requested_ip = ip;
+}
+
+/**
+ * n_dhcp4_client_probe_config_set_start_delay() - set start delay
+ * @config:                     configuration to operate on
+ * @msecs:                      value to set
+ *
+ * This sets the start delay property of the given configuration object.
+ *
+ * The default is 9000 ms, which is based on RFC2131. In the RFC the start
+ * delay is specified to be a random value in the range 1000 to 10.000 ms.
+ * However, there does not appear to be any particular reason to
+ * unconditionally wait at least one second, so we move the range down to
+ * start at 0 ms. The reaon for the random delay is to avoid network-wide
+ * events causing too much simultaneous network traffic. However, on modern
+ * networks, a more reasonable value may be in the 10 ms range.
+ */
+_c_public_ void n_dhcp4_client_probe_config_set_start_delay(NDhcp4ClientProbeConfig *config, uint64_t msecs) {
+        config->ms_start_delay = msecs;
+}
+
+/**
+ * n_dhpc4_client_probe_config_request_option() - append option to request from the server
+ * @config:                     configuration to operate on
+ * @option:                     option to request
+ *
+ * This adds an option to the list of options to request from the server.
+ *
+ * A server may send options that we do not requst, and it may omit options
+ * that we do request. However, to increase the likelyhood of uniform behavior
+ * between server implementations, we do not expose options that were not
+ * explicitly requested.
+ *
+ * When called multiple times, the order matters. Earlier requests are
+ * considered higher priority than later requests, in case the server must omit
+ * some, due to a lack of space. If the same option is requested more than once,
+ * only the first call has an effect.
+ */
+_c_public_ void n_dhcp4_client_probe_config_request_option(NDhcp4ClientProbeConfig *config, uint8_t option) {
+        for (unsigned int i = 0; i < config->n_request_parameters; ++i) {
+                if (config->request_parameters[i] == option)
+                        return;
+        }
+
+        c_assert(config->n_request_parameters <= UINT8_MAX);
+
+        config->request_parameters[config->n_request_parameters++] = option;
+}
+
+/**
+ * n_dhcp4_client_probe_config_append_option() - append option to outgoing messages
+ * @config:                     configuration to operate on
+ * @option:                     DHCP option number
+ * @data:                       payload
+ * @n_data:                     number of bytes in payload
+ *
+ * This sets extra options on a given configuration object.
+ *
+ * These options are appended verbatim to outgoing messages where
+ * that is supported by the specification. The same options are
+ * appended to all messages.
+ *
+ * No option may be appended more than once. Options considered internal
+ * to the DHCP protocol may not be appended.
+ *
+ * Return: 0 on success, N_DHCP4_E_DUPLICATE_OPTION if an option has already been
+ *         appended, N_DHCP4_E_INTERNAL if the option is not configurable, or
+ *         a negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_probe_config_append_option(NDhcp4ClientProbeConfig *config,
+                                                       uint8_t option,
+                                                       const void *data,
+                                                       uint8_t n_data) {
+        int r;
+
+        /* XXX: filter internal options */
+
+        for (unsigned int i = 0; i <= UINT8_MAX; ++i) {
+                if (config->options[i]) {
+                        if (config->options[i]->option == option)
+                                return N_DHCP4_E_DUPLICATE_OPTION;
+
+                        continue;
+                }
+
+                r = n_dhcp4_client_probe_option_new(&config->options[i],
+                                                    option,
+                                                    data,
+                                                    n_data);
+                if (r)
+                        return r;
+
+                return 0;
+        }
+
+        c_assert(0);
+        return -ENOTRECOVERABLE;
+}
+
+static void n_dhcp4_client_probe_config_initialize_random_seed(NDhcp4ClientProbeConfig *config) {
+        uint8_t hash_seed[] = {
+                0x25, 0x3f, 0x02, 0x75, 0x3a, 0xb8, 0x4f, 0x91,
+                0x9d, 0x0a, 0xd6, 0x15, 0x9d, 0x72, 0x7b, 0xcb,
+        };
+        CSipHash hash = C_SIPHASH_NULL;
+        unsigned short int seed16v[3];
+        const uint8_t *p;
+        uint64_t u64;
+        int r;
+
+        /*
+         * Initialize seed48_r(3)
+         *
+         * We need random jitter for all timeouts and delays, used to reduce
+         * network traffic during bursts. This is not meant as security measure
+         * but only meant to improve network utilization during bursts. The
+         * random source is thus negligible. However, we want, under all
+         * circumstances, avoid two instances running with the same seed. Thus
+         * we source the seed from AT_RANDOM, which grants us a per-process
+         * unique seed. We then add the current time to make sure consequetive
+         * instances use different seeds (to avoid clashes if processes are
+         * duplicated, or similar), and lastly we add the config memory address
+         * to avoid clashes of multiple parallel instances.
+         *
+         * Again, none of these are meant as security measure, but only to
+         * avoid *ACCIDENTAL* seed clashes. That is, in the case that many
+         * transactions are started in parallel, we delay the individual
+         * messages (as described in the spec), to reduce the traffic on the
+         * network and the chance of packets being dropped (and thus triggering
+         * timeouts and resends).
+         *
+         * We hash everything through SipHash, to avoid exposing AT_RANDOM and
+         * other sources to the network. We use a static salt to distinguish it
+         * from other implementations using the same random source.
+         */
+        c_siphash_init(&hash, hash_seed);
+
+        p = (const uint8_t *)getauxval(AT_RANDOM);
+        if (p)
+                c_siphash_append(&hash, p, 16);
+
+        u64 = n_dhcp4_gettime(CLOCK_MONOTONIC);
+        c_siphash_append(&hash, (const uint8_t *)&u64, sizeof(u64));
+
+        c_siphash_append(&hash, (const uint8_t *)&config, sizeof(config));
+
+        u64 = c_siphash_finalize(&hash);
+
+        seed16v[0] = (u64 >>  0) ^ (u64 >> 48);
+        seed16v[1] = (u64 >> 16) ^ (u64 >>  0);
+        seed16v[2] = (u64 >> 32) ^ (u64 >> 16);
+
+        r = seed48_r(seed16v, &config->entropy);
+        c_assert(!r);
+}
+
+/**
+ * n_dhcp4_client_probe_config_get_random() - get random data
+ * @config:                     config object to operate on
+ *
+ * Fetch the next 32bit random number from the entropy pool in @config.
+ * Note that this is in no way suitable for security purposes.
+ *
+ * Return: the random data.
+ */
+uint32_t n_dhcp4_client_probe_config_get_random(NDhcp4ClientProbeConfig *config) {
+        long int result;
+        int r;
+
+        r = mrand48_r(&config->entropy, &result);
+        c_assert(!r);
+
+        return result;
+};
+
+/**
+ * n_dhcp4_client_probe_new() - create new client probe
+ * @probep:                     output argument for new client probe
+ * @config:                     probe configuration
+ * @client:                     client to probe on behalf of
+ * @ns_now:                     the current time
+ *
+ * This creates a new client probe object.
+ *
+ * If one is already running, the new one will be immediately (but asynchronously)
+ * cancelled. Otherwise, a DISCOVER event is scheduled after a randomized delay.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+int n_dhcp4_client_probe_new(NDhcp4ClientProbe **probep,
+                             NDhcp4ClientProbeConfig *config,
+                             NDhcp4Client *client,
+                             uint64_t ns_now) {
+        _c_cleanup_(n_dhcp4_client_probe_freep) NDhcp4ClientProbe *probe = NULL;
+        bool active;
+        int r;
+
+        /*
+         * If there is already a probe attached, we create the new probe in
+         * detached state. It will not be linked into the epoll context and not
+         * be useful in any way. We immediately raise the CANCELLED event to
+         * notify the caller about it.
+         */
+        active = !client->current_probe;
+
+        probe = calloc(1, sizeof(*probe));
+        if (!probe)
+                return -ENOMEM;
+
+        *probe = (NDhcp4ClientProbe)N_DHCP4_CLIENT_PROBE_NULL(*probe);
+        probe->client = n_dhcp4_client_ref(client);
+
+        r = n_dhcp4_client_probe_config_dup(config, &probe->config);
+        if (r)
+                return r;
+
+        /*
+         * XXX: make seed initialization optional, so the entropy can be reused.
+         */
+        n_dhcp4_client_probe_config_initialize_random_seed(probe->config);
+
+        r = n_dhcp4_c_connection_init(&probe->connection,
+                                      client->config,
+                                      probe->config,
+                                      active ? client->fd_epoll : -1);
+        if (r)
+                return r;
+
+        if (active) {
+                /*
+                 * Defer the sending of DISCOVER by a random amount (by default up to 9 seconds).
+                 */
+                probe->ns_deferred = ns_now + (n_dhcp4_client_probe_config_get_random(probe->config) % (probe->config->ms_start_delay * 1000000ULL));
+                probe->client->current_probe = probe;
+        } else {
+                r = n_dhcp4_client_probe_raise(probe,
+                                               NULL,
+                                               N_DHCP4_CLIENT_EVENT_CANCELLED);
+                if (r)
+                        return r;
+        }
+
+        *probep = probe;
+        probe = NULL;
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_probe_free() - destroy a probe
+ * @probe:                      probe to operate on, or NULL
+ *
+ * This destroys a probe object and deallocates all its resources.
+ *
+ * If @probe is NULL, this is a no-op.
+ *
+ * Return: NULL is returned.
+ */
+_c_public_ NDhcp4ClientProbe *n_dhcp4_client_probe_free(NDhcp4ClientProbe *probe) {
+        NDhcp4CEventNode *node, *t_node;
+        NDhcp4ClientLease *lease, *t_lease;
+
+        if (!probe)
+                return NULL;
+
+        c_list_for_each_entry_safe(lease, t_lease, &probe->lease_list, probe_link)
+                n_dhcp4_client_lease_unlink(lease);
+
+        c_list_for_each_entry_safe(node, t_node, &probe->event_list, probe_link)
+                n_dhcp4_c_event_node_free(node);
+
+        if (probe == probe->client->current_probe)
+                probe->client->current_probe = NULL;
+
+        n_dhcp4_client_lease_unref(probe->current_lease);
+        n_dhcp4_c_connection_deinit(&probe->connection);
+        n_dhcp4_client_unref(probe->client);
+        n_dhcp4_client_probe_config_free(probe->config);
+
+        c_assert(c_list_is_empty(&probe->lease_list));
+        c_assert(c_list_is_empty(&probe->event_list));
+        free(probe);
+
+        return NULL;
+}
+
+/**
+ * n_dhcp4_client_probe_set_userdata() - set userdata pointer
+ * @probe:                      the probe to operate on
+ * @userdata:                   pointer to userdata
+ *
+ * Set a userdata pointer. The pointed to data is still owned by the caller, and
+ * is completely opaque to the probe.
+ */
+_c_public_ void n_dhcp4_client_probe_set_userdata(NDhcp4ClientProbe *probe, void *userdata) {
+        probe->userdata = userdata;
+}
+
+/**
+ * n_dhcp4_client_probe_get_userdata() - get userdata pointer
+ * @probe:                      the probe to operate on
+ * @userdatap:                  return pointer for userdata pointer
+ *
+ * Get the userdata pointer. The lifetime of the userdata and making sure it is
+ * still valid when accessed via the probe is the responsibility of the caller.
+ */
+_c_public_ void n_dhcp4_client_probe_get_userdata(NDhcp4ClientProbe *probe, void **userdatap) {
+        *userdatap = probe->userdata;
+}
+
+/**
+ * n_dhcp4_client_probe_raise() - XXX
+ */
+int n_dhcp4_client_probe_raise(NDhcp4ClientProbe *probe, NDhcp4CEventNode **nodep, unsigned int event) {
+        NDhcp4CEventNode *node;
+        int r;
+
+        r = n_dhcp4_client_raise(probe->client, &node, event);
+        if (r)
+                return r;
+
+        switch (event) {
+        case N_DHCP4_CLIENT_EVENT_OFFER:
+                node->event.offer.probe = probe;
+                break;
+        case N_DHCP4_CLIENT_EVENT_GRANTED:
+                node->event.granted.probe = probe;
+                break;
+        case N_DHCP4_CLIENT_EVENT_RETRACTED:
+                node->event.retracted.probe = probe;
+                break;
+        case N_DHCP4_CLIENT_EVENT_EXTENDED:
+                node->event.extended.probe = probe;
+                break;
+        case N_DHCP4_CLIENT_EVENT_EXPIRED:
+                node->event.expired.probe = probe;
+                break;
+        case N_DHCP4_CLIENT_EVENT_CANCELLED:
+                node->event.cancelled.probe = probe;
+                break;
+        default:
+                c_assert(0);
+                n_dhcp4_c_event_node_free(node);
+                return -ENOTRECOVERABLE;
+        }
+
+        if (nodep)
+                *nodep = node;
+        return 0;
+}
+
+void n_dhcp4_client_probe_get_timeout(NDhcp4ClientProbe *probe, uint64_t *timeoutp) {
+        uint64_t t1 = 0;
+        uint64_t t2 = 0;
+        uint64_t lifetime = 0;
+        uint64_t timeout = 0;
+
+        if (probe->current_lease) {
+                t1 = probe->current_lease->t1;
+                t2 = probe->current_lease->t2;
+                lifetime = probe->current_lease->lifetime;
+        }
+
+        n_dhcp4_c_connection_get_timeout(&probe->connection, &timeout);
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+                if (probe->ns_deferred && (!timeout || probe->ns_deferred < timeout))
+                        timeout = probe->ns_deferred;
+
+                break;
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+                if (t1 && (!timeout || t1 < timeout))
+                        timeout = t1;
+
+                /* fall-through */
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+                if (t2 && (!timeout || t2 < timeout))
+                        timeout = t2;
+
+                /* fall-through */
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+                if (lifetime && (!timeout || lifetime < timeout))
+                        timeout = lifetime;
+                break;
+        default:
+                /* ignore */
+                break;
+        }
+
+        *timeoutp = timeout;
+}
+
+static int n_dhcp4_client_probe_outgoing_append_options(NDhcp4ClientProbe *probe, NDhcp4Outgoing *outgoing) {
+        int r;
+
+        for (unsigned int i = 0; i <= UINT8_MAX; ++i) {
+                if (!probe->config->options[i])
+                        break;
+
+                r = n_dhcp4_outgoing_append(outgoing,
+                                            probe->config->options[i]->option,
+                                            probe->config->options[i]->data,
+                                            probe->config->options[i]->n_data);
+                if (r) {
+                        if (r == N_DHCP4_E_NO_SPACE)
+                                /* XXX */
+                                break;
+
+                        return r;
+                }
+        }
+
+        return 0;
+}
+
+static int n_dhcp4_client_probe_transition_deferred(NDhcp4ClientProbe *probe, uint64_t ns_now) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request = NULL;
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+                r = n_dhcp4_c_connection_listen(&probe->connection);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_c_connection_discover_new(&probe->connection, &request);
+                if (r)
+                        return r;
+
+                if (probe->config->requested_ip.s_addr != INADDR_ANY) {
+                        r = n_dhcp4_outgoing_append_requested_ip(request, probe->config->requested_ip);
+                        if (r)
+                                return r;
+                }
+
+                r = n_dhcp4_client_probe_outgoing_append_options(probe, request);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_c_connection_start_request(&probe->connection, request, ns_now);
+                if (r)
+                        return r;
+                else
+                        request = NULL; /* consumed */
+
+                probe->state = N_DHCP4_CLIENT_PROBE_STATE_SELECTING;
+                probe->ns_deferred = 0;
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                abort();
+                break;
+        }
+
+        return 0;
+}
+
+static int n_dhcp4_client_probe_transition_t1(NDhcp4ClientProbe *probe, uint64_t ns_now) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request = NULL;
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+                r = n_dhcp4_c_connection_renew_new(&probe->connection, &request);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_client_probe_outgoing_append_options(probe, request);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_c_connection_start_request(&probe->connection, request, ns_now);
+                if (r)
+                        return r;
+                else
+                        request = NULL; /* consumed */
+
+                probe->state = N_DHCP4_CLIENT_PROBE_STATE_RENEWING;
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                abort();
+                break;
+        }
+
+        return 0;
+}
+
+static int n_dhcp4_client_probe_transition_t2(NDhcp4ClientProbe *probe, uint64_t ns_now) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request = NULL;
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+                r = n_dhcp4_c_connection_rebind_new(&probe->connection, &request);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_client_probe_outgoing_append_options(probe, request);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_c_connection_start_request(&probe->connection, request, ns_now);
+                if (r)
+                        return r;
+                else
+                        request = NULL; /* consumed */
+
+                probe->state = N_DHCP4_CLIENT_PROBE_STATE_REBINDING;
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                abort();
+                break;
+        }
+
+        return 0;
+}
+
+static int n_dhcp4_client_probe_transition_lifetime(NDhcp4ClientProbe *probe) {
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+
+                /* XXX */
+
+                r = n_dhcp4_client_probe_raise(probe,
+                                               NULL,
+                                               N_DHCP4_CLIENT_EVENT_EXPIRED);
+                if (r)
+                        return r;
+
+                c_assert(probe->client->current_probe == probe);
+                probe->client->current_probe = NULL;
+
+                n_dhcp4_c_connection_close(&probe->connection);
+
+                probe->state = N_DHCP4_CLIENT_PROBE_STATE_EXPIRED;
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                abort();
+                break;
+        }
+
+        return 0;
+}
+
+static int n_dhcp4_client_probe_transition_offer(NDhcp4ClientProbe *probe, NDhcp4Incoming *message) {
+        _c_cleanup_(n_dhcp4_client_lease_unrefp) NDhcp4ClientLease *lease = NULL;
+        NDhcp4CEventNode *node;
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+
+                r = n_dhcp4_client_probe_raise(probe,
+                                               &node,
+                                               N_DHCP4_CLIENT_EVENT_OFFER);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_client_lease_new(&lease, message);
+                if (r)
+                        return r;
+
+                /* message consumed, do not fail */
+
+                n_dhcp4_client_lease_link(lease, probe);
+
+                node->event.offer.lease = n_dhcp4_client_lease_ref(lease);
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                /* ignore */
+                break;
+        }
+
+        return 0;
+}
+
+static int n_dhcp4_client_probe_transition_ack(NDhcp4ClientProbe *probe, NDhcp4Incoming *message) {
+        _c_cleanup_(n_dhcp4_client_lease_unrefp) NDhcp4ClientLease *lease = NULL;
+        NDhcp4CEventNode *node;
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+
+                r = n_dhcp4_client_probe_raise(probe,
+                                               &node,
+                                               N_DHCP4_CLIENT_EVENT_EXTENDED);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_client_lease_new(&lease, message);
+                if (r)
+                        return r;
+
+                /* message consumed, do not fail */
+
+                n_dhcp4_client_lease_link(lease, probe);
+
+                node->event.extended.lease = n_dhcp4_client_lease_ref(lease);
+                n_dhcp4_client_lease_unref(probe->current_lease);
+                probe->current_lease = n_dhcp4_client_lease_ref(lease);
+                probe->state = N_DHCP4_CLIENT_PROBE_STATE_BOUND;
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+
+                r = n_dhcp4_client_probe_raise(probe,
+                                               &node,
+                                               N_DHCP4_CLIENT_EVENT_GRANTED);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_client_lease_new(&lease, message);
+                if (r)
+                        return r;
+
+                /* message consumed, don to fail */
+
+                n_dhcp4_client_lease_link(lease, probe);
+
+                node->event.granted.lease = n_dhcp4_client_lease_ref(lease);
+                probe->current_lease = n_dhcp4_client_lease_ref(lease);
+                probe->state = N_DHCP4_CLIENT_PROBE_STATE_GRANTED;
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                /* ignore */
+                break;
+        }
+
+        return 0;
+}
+
+static int n_dhcp4_client_probe_transition_nak(NDhcp4ClientProbe *probe) {
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+
+                /* XXX */
+
+                r = n_dhcp4_client_probe_raise(probe,
+                                               NULL,
+                                               N_DHCP4_CLIENT_EVENT_RETRACTED);
+                if (r)
+                        return r;
+
+                probe->state = N_DHCP4_CLIENT_PROBE_STATE_INIT;
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                /* ignore */
+                break;
+        }
+
+        return 0;
+}
+
+int n_dhcp4_client_probe_transition_select(NDhcp4ClientProbe *probe, NDhcp4Incoming *offer, uint64_t ns_now) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request = NULL;
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+                r = n_dhcp4_c_connection_select_new(&probe->connection, &request, offer);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_client_probe_outgoing_append_options(probe, request);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_c_connection_start_request(&probe->connection, request, ns_now);
+                if (r)
+                        return r;
+                else
+                        request = NULL; /* consumed */
+
+                /* XXX: ignore other offers */
+
+                probe->state = N_DHCP4_CLIENT_PROBE_STATE_REQUESTING;
+
+                break;
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                /* ignore */
+                break;
+        }
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_probe_transition_accept() - XXX
+ */
+int n_dhcp4_client_probe_transition_accept(NDhcp4ClientProbe *probe, NDhcp4Incoming *ack) {
+        struct in_addr client = {};
+        struct in_addr server = {};
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+                n_dhcp4_incoming_get_yiaddr(ack, &client);
+
+                r = n_dhcp4_incoming_query_server_identifier(ack, &server);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_c_connection_connect(&probe->connection, &client, &server);
+                if (r)
+                        return r;
+
+                probe->state = N_DHCP4_CLIENT_PROBE_STATE_BOUND;
+
+                /* XXX: trigger timers */
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                /* ignore */
+                break;
+        }
+
+        return 0;
+}
+
+/**
+ * n_dhc4_client_probe_transition_decline() - XXX
+ */
+int n_dhcp4_client_probe_transition_decline(NDhcp4ClientProbe *probe, NDhcp4Incoming *offer, const char *error, uint64_t ns_now) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request = NULL;
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+                r = n_dhcp4_c_connection_decline_new(&probe->connection, &request, offer, error);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_c_connection_start_request(&probe->connection, request, ns_now);
+                if (r)
+                        return r;
+                else
+                        request = NULL; /* consumed */
+
+                /* XXX: what state to transition to? */
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                /* ignore */
+                break;
+        }
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_probe_dispatch_timer() - XXX
+ */
+int n_dhcp4_client_probe_dispatch_timer(NDhcp4ClientProbe *probe, uint64_t ns_now) {
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+                if (ns_now >= probe->ns_deferred) {
+                        r = n_dhcp4_client_probe_transition_deferred(probe, ns_now);
+                        if (r)
+                                return r;
+                }
+
+                break;
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+                if (ns_now >= probe->current_lease->lifetime) {
+                        r = n_dhcp4_client_probe_transition_lifetime(probe);
+                        if (r)
+                                return r;
+                }
+
+                break;
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+                if (ns_now >= probe->current_lease->lifetime) {
+                        r = n_dhcp4_client_probe_transition_lifetime(probe);
+                        if (r)
+                                return r;
+                } else if (ns_now >= probe->current_lease->t2) {
+                        r = n_dhcp4_client_probe_transition_t2(probe, ns_now);
+                        if (r)
+                                return r;
+                } else if (ns_now >= probe->current_lease->t1) {
+                        r = n_dhcp4_client_probe_transition_t1(probe, ns_now);
+                        if (r)
+                                return r;
+                }
+
+                break;
+        default:
+                /* ignore */
+                break;
+        }
+
+        r = n_dhcp4_c_connection_dispatch_timer(&probe->connection, ns_now);
+        if (r)
+                return r;
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_probe_dispatch_connection() - XXX
+ */
+int n_dhcp4_client_probe_dispatch_io(NDhcp4ClientProbe *probe, uint32_t events) {
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL;
+        uint8_t type;
+        int r;
+
+        r = n_dhcp4_c_connection_dispatch_io(&probe->connection, &message);
+        if (r) {
+                if (r == N_DHCP4_E_AGAIN)
+                        return 0;
+                else if (r == N_DHCP4_E_MALFORMED || r == N_DHCP4_E_UNEXPECTED) {
+                        /*
+                         * We fetched something from the sockets, which we
+                         * discarded. We don't know whether there is more data
+                         * to fetch, so we set the preempted flag to notify the
+                         * caller we want to be called again.
+                         */
+                        probe->client->preempted = true;
+                        return 0;
+                }
+
+                return r;
+        }
+
+        /*
+         * We fetched something from the sockets, which we will handle below.
+         * We don't know whether there is more data to fetch, so we set the
+         * preempted flag to notify the caller we want to be called again.
+         */
+        probe->client->preempted = true;
+
+        r = n_dhcp4_incoming_query_message_type(message, &type);
+        if (r == N_DHCP4_E_UNSET || r == N_DHCP4_E_MALFORMED)
+                /*
+                 * XXX: this can never happen as we already queried the message
+                 * type.
+                 */
+                return 0;
+
+        switch (type) {
+        case N_DHCP4_MESSAGE_OFFER:
+                r = n_dhcp4_client_probe_transition_offer(probe, message);
+                if (r)
+                        return r;
+                else
+                        message = NULL; /* consumed */
+                break;
+        case N_DHCP4_MESSAGE_ACK:
+                r = n_dhcp4_client_probe_transition_ack(probe, message);
+                if (r)
+                        return r;
+                else
+                        message = NULL; /* consumed */
+                break;
+        case N_DHCP4_MESSAGE_NAK:
+                r = n_dhcp4_client_probe_transition_nak(probe);
+                if (r)
+                        return r;
+                break;
+        default:
+                /*
+                 * We receiveda message type we do not support, simply discard
+                 * it.
+                 */
+                break;
+        }
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_probe_update_mtu() - XXX
+ */
+int n_dhcp4_client_probe_update_mtu(NDhcp4ClientProbe *probe, uint16_t mtu) {
+        return 0;
+}
diff --git a/shared/n-dhcp4/src/n-dhcp4-client.c b/shared/n-dhcp4/src/n-dhcp4-client.c
new file mode 100644
index 00000000..5f7794fb
--- /dev/null
+++ b/shared/n-dhcp4/src/n-dhcp4-client.c
@@ -0,0 +1,839 @@
+/*
+ * Client Side of the Dynamic Host Configuration Protocol for IPv4
+ *
+ * This implements the public API around the NDhcp4Client object. The client
+ * object is simply a context to track running probes. It manages pending
+ * events of all probes, as well as forwards the dispatching requests whenever
+ * the dispatcher is run.
+ */
+
+#include <assert.h>
+#include <c-list.h>
+#include <c-stdaux.h>
+#include <errno.h>
+#include <linux/if_ether.h>
+#include <linux/if_infiniband.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/epoll.h>
+#include <sys/time.h>
+#include <sys/timerfd.h>
+#include <time.h>
+#include <unistd.h>
+#include "n-dhcp4.h"
+#include "n-dhcp4-private.h"
+
+/**
+ * n_dhcp4_client_config_new() - allocate new client configuration
+ * @configp:                    output argument for new client config
+ *
+ * This creates a new client configuration object. Client configurations are
+ * unlinked objects that merely serve as collection of parameters. They do not
+ * perform validity checks.
+ *
+ * The new client configuration is fully owned by the caller. They are
+ * responsible to free the object if no longer needed.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_config_new(NDhcp4ClientConfig **configp) {
+        _c_cleanup_(n_dhcp4_client_config_freep) NDhcp4ClientConfig *config = NULL;
+
+        config = calloc(1, sizeof(*config));
+        if (!config)
+                return -ENOMEM;
+
+        *config = (NDhcp4ClientConfig)N_DHCP4_CLIENT_CONFIG_NULL(*config);
+
+        *configp = config;
+        config = NULL;
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_config_free() - destroy client configuration
+ * @config:                     client configuration to operate on, or NULL
+ *
+ * This destroys a client configuration and deallocates all its resources. If
+ * NULL is passed, this is a no-op.
+ *
+ * Return: NULL is returned.
+ */
+_c_public_ NDhcp4ClientConfig *n_dhcp4_client_config_free(NDhcp4ClientConfig *config) {
+        if (!config)
+                return NULL;
+
+        free(config->client_id);
+        free(config);
+
+        return NULL;
+}
+
+/**
+ * n_dhcp4_client_config_dup() - duplicate client configuration
+ * @config:                     client configuration to operate on
+ * @dupp:                       output argument for duplicate
+ *
+ * This duplicates the client configuration given as @config and returns it in
+ * @dupp to the caller.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int n_dhcp4_client_config_dup(NDhcp4ClientConfig *config, NDhcp4ClientConfig **dupp) {
+        _c_cleanup_(n_dhcp4_client_config_freep) NDhcp4ClientConfig *dup = NULL;
+        int r;
+
+        r = n_dhcp4_client_config_new(&dup);
+        if (r)
+                return r;
+
+        dup->ifindex = config->ifindex;
+        dup->transport = config->transport;
+        dup->request_broadcast = config->request_broadcast;
+        memcpy(dup->mac, config->mac, sizeof(dup->mac));
+        dup->n_mac = config->n_mac;
+        memcpy(dup->broadcast_mac, config->broadcast_mac, sizeof(dup->broadcast_mac));
+        dup->n_broadcast_mac = config->n_broadcast_mac;
+
+        r = n_dhcp4_client_config_set_client_id(dup,
+                                                config->client_id,
+                                                config->n_client_id);
+        if (r)
+                return r;
+
+        *dupp = dup;
+        dup = NULL;
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_config_set_ifindex() - set ifindex property
+ * @config:                     client configuration to operate on
+ * @ifindex:                    ifindex to set
+ *
+ * This sets the ifindex property of the client configuration. The ifindex
+ * specifies the network device that a DHCP client will run on.
+ */
+_c_public_ void n_dhcp4_client_config_set_ifindex(NDhcp4ClientConfig *config, int ifindex) {
+        config->ifindex = ifindex;
+}
+
+/**
+ * n_dhcp4_client_config_set_transport() - set transport property
+ * @config:                     client configuration to operate on
+ * @transport:                  transport to set
+ *
+ * This sets the transport property of the client configuration. The transport
+ * defines the hardware transport of the network device that a DHCP client
+ * runs on.
+ *
+ * This takes one of the N_DHCP4_TRANSPORT_* identifiers as argument.
+ */
+_c_public_ void n_dhcp4_client_config_set_transport(NDhcp4ClientConfig *config, unsigned int transport) {
+        config->transport = transport;
+}
+
+/**
+ * n_dhcp4_client_config_set_request_broadcast() - set request-broadcast property
+ * @config:                           configuration to operate on
+ * @request_broadcast:                value to set
+ *
+ * This sets the request_broadcast property of the given configuration object.
+ *
+ * The default is false. If set to true, a the server will be told to not unicast
+ * replies to the client's IP address before it has been configured, but broadcast
+ * to INADDR_ANY instead. In most cases, you do not want this.
+ *
+ * Background: OFFER and ACK messages from DHCP servers to clients are unicast
+ *             to the IP address handed out, even before the IP address has
+ *             been configured on the taregt interface. This usually works
+ *             because the correct destination hardware address is explicitly
+ *             set on the outgoing packets, rather than being resolved (which
+ *             would not work). However, some hardware does not accept incoming
+ *             IP packets destined for addresses they do not own, even if the
+ *             hardware address is correct. In this case, the server must
+ *             broadcast the replies in order for the client to receive them.
+ *             In general, unneccesary broadcasting is something one wants to
+ *             avoid, and some networks will not deliver broadcasts to the
+ *             client at all, in which case this flag must not be set.
+ */
+_c_public_ void n_dhcp4_client_config_set_request_broadcast(NDhcp4ClientConfig *config, bool request_broadcast) {
+        config->request_broadcast = request_broadcast;
+}
+
+/**
+ * n_dhcp4_client_config_set_mac() - set mac property
+ * @config:                     client configuration to operate on
+ * @mac:                        hardware address to set
+ * @n_mac:                      length of the hardware address
+ *
+ * This sets the mac property of the client configuration. It specifies the
+ * hardware address of the local interface that the DHCP client runs on.
+ *
+ * This function copies the specified hardware address into @config. Any
+ * hardware address is supported. It is up to the consumer of the client
+ * configuration to verify the validity of the hardware address.
+ *
+ * Note: This function may truncate the hardware address internally, but
+ *       retains the original length. The consumer of this configuration can
+ *       thus tell whether the data was truncated and will refuse it.
+ *       The internal buffer is big enough to hold any hardware address of all
+ *       supported transports. Thus, truncation only happens if you use
+ *       unsupported transports, and those will be rejected, anyway.
+ */
+_c_public_ void n_dhcp4_client_config_set_mac(NDhcp4ClientConfig *config, const uint8_t *mac, size_t n_mac) {
+        config->n_mac = n_mac;
+
+        if (n_mac > sizeof(config->mac))
+                n_mac = sizeof(config->mac);
+
+        memcpy(config->mac, mac, n_mac);
+}
+
+/**
+ * n_dhcp4_client_config_set_broadcast_mac() - set broadcast-mac property
+ * @config:                     client configuration to operate on
+ * @mac:                        hardware address to set
+ * @n_mac:                      length of the hardware address
+ *
+ * This sets the broadcast-mac property of the client configuration. It
+ * specifies the destination hardware address to use for broadcasts on the
+ * local interface that the DHCP client runs on.
+ *
+ * This function copies the specified hardware address into @config. Any
+ * hardware address is supported. It is up to the consumer of the client
+ * configuration to verify the validity of the hardware address.
+ *
+ * Note: This function may truncate the hardware address internally, but
+ *       retains the original length. The consumer of this configuration can
+ *       thus tell whether the data was truncated and will refuse it.
+ *       The internal buffer is big enough to hold any hardware address of all
+ *       supported transports. Thus, truncation only happens if you use
+ *       unsupported transports, and those will be rejected, anyway.
+ */
+_c_public_ void n_dhcp4_client_config_set_broadcast_mac(NDhcp4ClientConfig *config, const uint8_t *mac, size_t n_mac) {
+        config->n_broadcast_mac = n_mac;
+
+        if (n_mac > sizeof(config->mac))
+                n_mac = sizeof(config->mac);
+
+        memcpy(config->broadcast_mac, mac, n_mac);
+}
+
+/**
+ * n_dhcp4_client_config_set_client_id() - set client-id property
+ * @config:                     client configuration to operate on
+ * @id:                         client id
+ * @n_id:                       length of the client id in bytes
+ *
+ * This sets the client-id property of @config. It copies the entire client-id
+ * buffer into the configuration.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_config_set_client_id(NDhcp4ClientConfig *config, const uint8_t *id, size_t n_id) {
+        uint8_t *t;
+
+        t = malloc(n_id + 1);
+        if (!t)
+                return -ENOMEM;
+
+        free(config->client_id);
+        config->client_id = t;
+        config->n_client_id = n_id;
+
+        memcpy(config->client_id, id, n_id);
+        config->client_id[n_id] = 0; /* safety 0 for debugging */
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_c_event_node_new() - allocate new event
+ * @nodep:                      output argument for new event
+ *
+ * This allocates a new event node and returns it to the caller. The caller
+ * fully owns the event-node and is reposonsible to either link it somewhere,
+ * or release it.
+ *
+ * Event nodes can be linked on a client object, as well as optionally on a
+ * probe object. As long as an event-node is linked, it will be retrievable by
+ * the API user through n_dhcp4_client_pop_event(). Furthermore, destruction of
+ * the client, or probe respectively, will clean-up all pending events.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int n_dhcp4_c_event_node_new(NDhcp4CEventNode **nodep) {
+        NDhcp4CEventNode *node;
+
+        node = calloc(1, sizeof(*node));
+        if (!node)
+                return -ENOMEM;
+
+        *node = (NDhcp4CEventNode)N_DHCP4_C_EVENT_NODE_NULL(*node);
+
+        *nodep = node;
+        return 0;
+}
+
+/**
+ * n_dhcp4_c_event_node_free() - deallocate event
+ * @node:                       node to operate on, or NULL
+ *
+ * This deallocates the node given as @node. If the node is linked on a client
+ * or probe, it is unlinked automatically.
+ *
+ * If @probe is NULL, this is a no-op.
+ *
+ * Return: NULL is returned.
+ */
+NDhcp4CEventNode *n_dhcp4_c_event_node_free(NDhcp4CEventNode *node) {
+        if (!node)
+                return NULL;
+
+        switch (node->event.event) {
+        case N_DHCP4_CLIENT_EVENT_OFFER:
+                node->event.offer.lease = n_dhcp4_client_lease_unref(node->event.offer.lease);
+                break;
+        case N_DHCP4_CLIENT_EVENT_GRANTED:
+                node->event.granted.lease = n_dhcp4_client_lease_unref(node->event.granted.lease);
+                break;
+        case N_DHCP4_CLIENT_EVENT_EXTENDED:
+                node->event.extended.lease = n_dhcp4_client_lease_unref(node->event.extended.lease);
+                break;
+        default:
+                break;
+        }
+
+        c_list_unlink(&node->probe_link);
+        c_list_unlink(&node->client_link);
+        free(node);
+
+        return NULL;
+}
+
+/**
+ * n_dhcp4_client_new() - allocate new client
+ * @clientp:                    output argument for new client
+ * @config:                     configuration to use
+ *
+ * This allocates a new DHCP4 client object and returns it in @clientp to the
+ * caller. The caller then owns a single ref-count to the object and is
+ * responsible to drop it, when no longer needed.
+ *
+ * The configuration given as @config is used to initialize the client. The
+ * caller is free to destroy the configuration once this function returns.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_new(NDhcp4Client **clientp, NDhcp4ClientConfig *config) {
+        _c_cleanup_(n_dhcp4_client_unrefp) NDhcp4Client *client = NULL;
+        struct epoll_event ev = {
+                .events = EPOLLIN,
+        };
+        int r;
+
+        c_assert(clientp);
+
+        /* verify configuration */
+        {
+                if (config->ifindex < 1)
+                        return N_DHCP4_E_INVALID_IFINDEX;
+
+                switch (config->transport) {
+                case N_DHCP4_TRANSPORT_ETHERNET:
+                        if (config->n_mac != ETH_ALEN ||
+                            config->n_broadcast_mac != ETH_ALEN)
+                                return N_DHCP4_E_INVALID_ADDRESS;
+
+                        break;
+                case N_DHCP4_TRANSPORT_INFINIBAND:
+                        if (config->n_mac != INFINIBAND_ALEN ||
+                            config->n_broadcast_mac != INFINIBAND_ALEN)
+                                return N_DHCP4_E_INVALID_ADDRESS;
+
+                        break;
+                default:
+                        return N_DHCP4_E_INVALID_TRANSPORT;
+                }
+
+                if (config->n_client_id < 1)
+                        return N_DHCP4_E_INVALID_CLIENT_ID;
+        }
+
+        client = malloc(sizeof(*client));
+        if (!client)
+                return -ENOMEM;
+
+        *client = (NDhcp4Client)N_DHCP4_CLIENT_NULL(*client);
+
+        r = n_dhcp4_client_config_dup(config, &client->config);
+        if (r)
+                return r;
+
+        client->fd_epoll = epoll_create1(EPOLL_CLOEXEC);
+        if (client->fd_epoll < 0)
+                return -errno;
+
+        client->fd_timer = timerfd_create(CLOCK_BOOTTIME, TFD_CLOEXEC | TFD_NONBLOCK);
+        if (client->fd_timer < 0)
+                return -errno;
+
+        ev.data.u32 = N_DHCP4_CLIENT_EPOLL_TIMER;
+        r = epoll_ctl(client->fd_epoll, EPOLL_CTL_ADD, client->fd_timer, &ev);
+        if (r < 0)
+                return -errno;
+
+        *clientp = client;
+        client = NULL;
+        return 0;
+}
+
+static void n_dhcp4_client_free(NDhcp4Client *client) {
+        NDhcp4CEventNode *node, *t_node;
+
+        c_assert(!client->current_probe);
+
+        c_list_for_each_entry_safe(node, t_node, &client->event_list, client_link)
+                n_dhcp4_c_event_node_free(node);
+
+        if (client->fd_timer >= 0) {
+                epoll_ctl(client->fd_epoll, EPOLL_CTL_DEL, client->fd_timer, NULL);
+                close(client->fd_timer);
+        }
+
+        if (client->fd_epoll >= 0)
+                close(client->fd_epoll);
+
+        n_dhcp4_client_config_free(client->config);
+        free(client);
+}
+
+/**
+ * n_dhcp4_client_ref() - acquire client reference
+ * @client:                     client to operate on, or NULL
+ *
+ * This acquires a reference to the client given as @client. If @client is
+ * NULL, this function is a no-op.
+ *
+ * Return: @client is returned.
+ */
+_c_public_ NDhcp4Client *n_dhcp4_client_ref(NDhcp4Client *client) {
+        if (client)
+                ++client->n_refs;
+        return client;
+}
+
+/**
+ * n_dhcp4_client_unref() - release client reference
+ * @client:                     client to operate on, or NULL
+ *
+ * This releases a reference to the client given as @client. If @client is
+ * NULL, this is a no-op.
+ *
+ * Once the last reference is dropped, the client object will get destroyed and
+ * deallocated.
+ *
+ * Return: NULL is returned.
+ */
+_c_public_ NDhcp4Client *n_dhcp4_client_unref(NDhcp4Client *client) {
+        if (client && !--client->n_refs)
+                n_dhcp4_client_free(client);
+        return NULL;
+}
+
+/**
+ * n_dhcp4_client_raise() - raise event
+ * @client:                     client to operate on
+ * @nodep:                      output argument for new event, or NULL
+ * @event:                      event type to use
+ *
+ * This creates a new event-node on @client, setting the event-type to @event.
+ * The newly created event-node is returned to the caller in @nodep (unless
+ * @nodep is NULL).
+ *
+ * The event-node is automatically linked on @client.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int n_dhcp4_client_raise(NDhcp4Client *client, NDhcp4CEventNode **nodep, unsigned int event) {
+        NDhcp4CEventNode *node;
+        int r;
+
+        r = n_dhcp4_c_event_node_new(&node);
+        if (r)
+                return r;
+
+        node->event.event = event;
+        c_list_link_tail(&client->event_list, &node->client_link);
+
+        if (nodep)
+                *nodep = node;
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_arm_timer() - update timer
+ * @client:                     client to operate on
+ *
+ * This updates the timer on @client to fire on the next pending timeout. This
+ * must be called whenever a timeout on @client might have changed.
+ */
+void n_dhcp4_client_arm_timer(NDhcp4Client *client) {
+        uint64_t timeout = 0;
+        int r;
+
+        if (client->current_probe)
+                n_dhcp4_client_probe_get_timeout(client->current_probe, &timeout);
+
+        if (timeout != client->scheduled_timeout) {
+                r = timerfd_settime(client->fd_timer,
+                                    TFD_TIMER_ABSTIME,
+                                    &(struct itimerspec){
+                                        .it_value = {
+                                                .tv_sec = timeout / UINT64_C(1000000000),
+                                                .tv_nsec = timeout % UINT64_C(1000000000),
+                                        },
+                                    },
+                                    NULL);
+                c_assert(r >= 0);
+
+                client->scheduled_timeout = timeout;
+        }
+}
+
+/**
+ * n_dhcp4_client_get_fd() - retrieve event FD
+ * @client:                     client to operate on
+ * @fdp:                        output argument to store FD
+ *
+ * This retrieves the FD used by the client object given as @client. The FD is
+ * always valid, and returned in @fdp.
+ *
+ * The caller is expected to poll this FD for readable events and call
+ * n_dhcp4_client_dispatch() whenever the FD is readable.
+ */
+_c_public_ void n_dhcp4_client_get_fd(NDhcp4Client *client, int *fdp) {
+        *fdp = client->fd_epoll;
+}
+
+static int n_dhcp4_client_dispatch_timer(NDhcp4Client *client, struct epoll_event *event) {
+        uint64_t v, ns_now;
+        int r;
+
+        if (event->events & (EPOLLHUP | EPOLLERR)) {
+                /*
+                 * There is no way to handle either gracefully. If we ignored
+                 * them, we would busy-loop, so lets rather forward the error
+                 * to the caller.
+                 */
+                return -ENOTRECOVERABLE;
+        }
+
+        if (event->events & EPOLLIN) {
+                r = read(client->fd_timer, &v, sizeof(v));
+                if (r < 0) {
+                        if (errno == EAGAIN) {
+                                /*
+                                 * There are no more pending events, so nothing
+                                 * to be done. Return to the caller.
+                                 */
+                                return 0;
+                        }
+
+                        /*
+                         * Something failed. We use CLOCK_BOOTTIME/MONOTONIC,
+                         * so ECANCELED cannot happen. Hence, there is no error
+                         * that we could gracefully handle. Fail hard and let
+                         * the caller deal with it.
+                         */
+                        return -errno;
+                } else if (r != sizeof(v) || v == 0) {
+                        /*
+                         * Kernel guarantees 8-byte reads, and only to return
+                         * data if at least one timer triggered; fail hard if
+                         * it suddenly starts exposing unexpected behavior.
+                         */
+                        return -ENOTRECOVERABLE;
+                }
+
+                /*
+                 * Forward the timer-event to the active probe. Timers should
+                 * not fire if there is no probe running, but lets ignore them
+                 * for now, so probe-internals are not leaked to this generic
+                 * client dispatcher.
+                 */
+                if (client->current_probe) {
+                        /*
+                         * Read the current time *after* dispatching the timer,
+                         * to make sure we do not miss wakeups.
+                         */
+                        ns_now = n_dhcp4_gettime(CLOCK_BOOTTIME);
+
+                        r = n_dhcp4_client_probe_dispatch_timer(client->current_probe,
+                                                                ns_now);
+                        if (r)
+                                return r;
+                }
+        }
+
+        return 0;
+}
+
+static int n_dhcp4_client_dispatch_io(NDhcp4Client *client, struct epoll_event *event) {
+        int r;
+
+        if (client->current_probe)
+                r = n_dhcp4_client_probe_dispatch_io(client->current_probe,
+                                                     event->events);
+        else
+                return -ENOTRECOVERABLE;
+
+        return r;
+}
+
+/**
+ * n_dhcp4_client_dispatch() - dispatch client
+ * @client:                     client to operate on
+ *
+ * This dispatches pending operations on @client. It will read incoming
+ * messages, write pending data, and handle any timeouts.
+ *
+ * This function never blocks.
+ *
+ * If there are more events to dispatch, than would be reasonable to do in a
+ * single dispatch, this will return N_DHCP4_E_PREEMPTED. In this case the
+ * caller is expected to call into this function again when it is ready to
+ * dispatch more events.
+ * If your event loop is level-triggered (it very likely is), you can
+ * optionally ignore this return code and treat it as success.
+ *
+ * Return: 0 on success, negative error code on failure, N_DHCP4_E_PREEMPTED if
+ *         there is more data to dispatch.
+ */
+_c_public_ int n_dhcp4_client_dispatch(NDhcp4Client *client) {
+        struct epoll_event events[2];
+        int n, i, r = 0;
+
+        n = epoll_wait(client->fd_epoll, events, sizeof(events) / sizeof(*events), 0);
+        if (n < 0) {
+                /* Linux never returns EINTR if `timeout == 0'. */
+                return -errno;
+        }
+
+        client->preempted = false;
+
+        for (i = 0; i < n; ++i) {
+                switch (events[i].data.u32) {
+                case N_DHCP4_CLIENT_EPOLL_TIMER:
+                        r = n_dhcp4_client_dispatch_timer(client, events + i);
+                        break;
+                case N_DHCP4_CLIENT_EPOLL_IO:
+                        r = n_dhcp4_client_dispatch_io(client, events + i);
+                        break;
+                default:
+                        c_assert(0);
+                        r = 0;
+                        break;
+                }
+
+                if (r) {
+                        if (r == N_DHCP4_E_DOWN) {
+                                r = n_dhcp4_client_raise(client,
+                                                         NULL,
+                                                         N_DHCP4_CLIENT_EVENT_DOWN);
+                                if (r)
+                                        return r;
+
+                                /* continue normally */
+                        } else if (r) {
+                                c_assert(r < _N_DHCP4_E_INTERNAL);
+                                return r;
+                        }
+                }
+        }
+
+        n_dhcp4_client_arm_timer(client);
+
+        return client->preempted ? N_DHCP4_E_PREEMPTED : 0;
+}
+
+/**
+ * n_dhcp4_client_pop_event() - fetch pending event
+ * @client:                     client to operate on
+ * @eventp:                     output argument to store next event
+ *
+ * This fetches the next pending event from the event-queue and returns it to a
+ * caller. A pointer to the event is stored in @eventp. If there is no more
+ * event queued, NULL is returned.
+ *
+ * If a valid event is returned, it is accessible until the next call to this
+ * function, or the destruction of the context object (this might be either the
+ * client object or the probe object, pointed to by the event), whichever
+ * happens first.
+ * That is, the caller should not pin the returned event object, but copy
+ * required information into their own state tracking contexts.
+ *
+ * The possible events are:
+ * * N_DHCP4_CLIENT_EVENT_OFFER:     A lease offered from a server in response
+ *                                   to a probe. Several such offers may be
+ *                                   received until one of them is selected by
+ *                                   the caller. Only one lease may be selected.
+ *                                   The attached lease object may be queried
+ *                                   for information in order to decide which
+ *                                   lease to select, though the information is
+ *                                   not guaranteed to stay the same in the
+ *                                   final lease.
+ * * N_DHCP4_CLIENT_EVENT_GRANTED:   A selected lease was granted by the server.
+ *                                   The information in the attached lease
+ *                                   object should be used to configure the
+ *                                   client. Once the client has been
+ *                                   configured, the lease should be accepted.
+ * * N_DHCP4_CLIENT_EVENT_RETRACTED: A selected lease offer was retracted by the
+ *                                   server. This can happen in case the server
+ *                                   offers the same lease to several clients,
+ *                                   or the server discovers that the IP address
+ *                                   in the lease is already in use.
+ * * N_DHCP4_CLIENT_EVENT_EXTENDED:  An active lease is extended, if applicable
+ *                                   the kernel should be updated with the new
+ *                                   lifetime information for addresses and/or
+ *                                   routes.
+ * * N_DHCP4_CLIENT_EVENT_EXPIRED:   An active lease failed to be extended by
+ *                                   the end of its lifetime. The client should
+ *                                   immediately stop using the information
+ *                                   contained in the lease.
+ * * N_DHCP4_CLIENT_EVENT_DOWN:      The network interface was put down down.
+ *                                   The user is recommended to reestablish the
+ *                                   lease at the first opportunity when the
+ *                                   network comes back up. Note that this is
+ *                                   purely informational, the probe will keep
+ *                                   running, and if the network topology does
+ *                                   not change any lease we have will still be
+ *                                   valid.
+ * * N_DHCP4_CLIENT_EVENT_CANCELLED: The probe was cancelled. This can happen if
+ *                                   the client attempted several incompatible
+ *                                   probes in parallel, then the most recent
+ *                                   ones will be cancelled asynchronously.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_pop_event(NDhcp4Client *client, NDhcp4ClientEvent **eventp) {
+        NDhcp4CEventNode *node, *t_node;
+
+        c_list_for_each_entry_safe(node, t_node, &client->event_list, client_link) {
+                if (node->is_public) {
+                        n_dhcp4_c_event_node_free(node);
+                        continue;
+                }
+
+                node->is_public = true;
+                *eventp = &node->event;
+                return 0;
+        }
+
+        *eventp = NULL;
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_update_mtu() - update link mtu
+ * @client:                     client to operate on
+ * @mtu:                        new mtu
+ *
+ * This updates the link MTU used by the client object. By default, the minimum
+ * requirement given by the IP specification is assumed, which means 576
+ * bytes. The caller is advised to update this to the actual MTU used by the
+ * link layer.
+ *
+ * This value reflects the MTU of the link layer. That is, it is the maximum
+ * packet size that you can send on that link, excluding the link-header but
+ * including the IP-header. On ethernet-v2 this would be 1500.
+ *
+ * If unsure, it is safe to leave this unset. However, in this case a DHCP
+ * server will be required to omit information if it does not fit into the
+ * default MTU.
+ *
+ * Unless you keep the default MTU, you should update the MTU whenever the link
+ * MTU changes. That is, when it is increased *and* when it is decreased.
+ * However, you must be aware that decreasing the MTU on a link might cause
+ * temporary data loss.
+ *
+ * Background: Knowing the link MTU guarantees that we can possibly transmit
+ *             packets bigger than the IP minimum (i.e., 576 bytes). However,
+ *             it does not guarantee that a possible target supports parsing
+ *             packets bigger than the IP minimum. Hence, the MTU is used by a
+ *             client to send a hint to a server that it can receive replies
+ *             bigger than the minimum. As such, a server can reply with more
+ *             information than otherwise possible.
+ *             Since this DHCP client does not support fragmented packets, we
+ *             simply set the allowed packet-size to the local link MTU.
+ *             Note that DHCP relays might cause DHCP packets to be routed.
+ *             However, such relays are required to always reassemble any
+ *             fragments they receive into full DHCP packets, before they
+ *             forward them either way. This guarantees that incoming packets
+ *             are never fragmented, unless they exceed the local link MTU
+ *             (this would otherwise not neccessarily be true, if some other
+ *             part of the routed network had a lower MTU).
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_update_mtu(NDhcp4Client *client, uint16_t mtu) {
+        int r;
+
+        if (mtu == client->mtu)
+                return 0;
+
+        if (client->current_probe) {
+                r = n_dhcp4_client_probe_update_mtu(client->current_probe, mtu);
+                if (r)
+                        return r;
+        }
+
+        client->mtu = mtu;
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_probe() - create a new probe
+ * @client:                     client to operate on
+ * @probep:                     output argument to store new probe
+ * @config:                     probe configuration to use
+ *
+ * This creates a new probe on @client. Probes represent DHCP requests and
+ * track the state over the entire lifetime of a lease. Once a probe is created
+ * it will start looking for DHCP servers, request a lease from them, and renew
+ * the lease continously whenever it expires. Furthermore, if a lease cannot be
+ * renewed, a new lease will be requested.
+ *
+ * The API allows for many probes to be run at the same time. However, the DHCP
+ * specification forbids many of those cases (e.g., you must not reuse a client
+ * id, otherwise it will be impossible to track who to forward received packets
+ * to). Hence, so far only a single probe can run at a time. If you create a
+ * new probe, all older probes that conflict with that probe will be canceled
+ * (their state machine is halted and a N_DHCP4_CLIENT_EVENT_CANCELLED event is
+ * raised.
+ * This might change in the future, though. There might be cases where multiple
+ * probes can be run in parallel (e.g., with different client-ids, or an INFORM
+ * in parallel to a REQUEST, ...).
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_probe(NDhcp4Client *client,
+                                  NDhcp4ClientProbe **probep,
+                                  NDhcp4ClientProbeConfig *config) {
+        _c_cleanup_(n_dhcp4_client_probe_freep) NDhcp4ClientProbe *probe = NULL;
+        uint64_t ns_now;
+        int r;
+
+        ns_now = n_dhcp4_gettime(CLOCK_BOOTTIME);
+
+        r = n_dhcp4_client_probe_new(&probe, config, client, ns_now);
+        if (r)
+                return r;
+
+        n_dhcp4_client_arm_timer(client);
+
+        *probep = probe;
+        probe = NULL;
+        return 0;
+}
diff --git a/shared/n-dhcp4/src/n-dhcp4-incoming.c b/shared/n-dhcp4/src/n-dhcp4-incoming.c
new file mode 100644
index 00000000..255da458
--- /dev/null
+++ b/shared/n-dhcp4/src/n-dhcp4-incoming.c
@@ -0,0 +1,431 @@
+/*
+ * DHCPv4 Incoming Messages
+ *
+ * This file implements the message parser object for incoming DHCP4 messages.
+ * It takes a linear data blob as input, and provides accessors for the message
+ * content.
+ *
+ * This wrapper mainly deals with the OPTIONs array. That is, in hides the
+ * different overload-sections the DHCP4 spec defines, it concatenates
+ * duplicate option fields (as described by the spec), and provides a
+ * consistent view to the caller.
+ *
+ * Internally, for every incoming message we linearize its OPTIONs. This means,
+ * we create a copy of the contents, and merge all duplicate options into a
+ * single option entry. We then provide accessors to the caller to easily get
+ * O(1) access to individual fields.
+ */
+
+#include <assert.h>
+#include <c-stdaux.h>
+#include <endian.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <netinet/ip.h>
+#include <netinet/udp.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include "n-dhcp4.h"
+#include "n-dhcp4-private.h"
+
+static void n_dhcp4_incoming_prefetch(NDhcp4Incoming *incoming, size_t *offset, uint8_t option, const uint8_t *raw, size_t n_raw) {
+        uint8_t o, l;
+        size_t pos;
+
+        for (pos = 0; pos < n_raw; ) {
+                o = raw[pos++];
+                if (o == N_DHCP4_OPTION_PAD)
+                        continue;
+                if (o == N_DHCP4_OPTION_END)
+                        return;
+
+                /* bail out if no remaining space for length field */
+                if (pos >= n_raw)
+                        return;
+
+                /* bail out if length exceeds the available space */
+                l = raw[pos++];
+                if (l > n_raw || pos > n_raw - l)
+                        return;
+
+                /* prefetch content if it matches @option */
+                if (o == option) {
+                        memcpy((uint8_t *)&incoming->message + *offset, raw + pos, l);
+                        *offset += l;
+                }
+
+                pos += l;
+        }
+}
+
+static void n_dhcp4_incoming_merge(NDhcp4Incoming *incoming, size_t *offset, uint8_t overload, uint8_t option) {
+        uint8_t *m = (uint8_t *)&incoming->message;
+        size_t pos;
+
+        /*
+         * Prefetch all options matching @option from the 3 sections,
+         * concatenating their content. Remember the offset and size of the
+         * option in our message state.
+         */
+
+        pos = *offset;
+
+        /* prefetch option from OPTIONS */
+        n_dhcp4_incoming_prefetch(incoming, offset, option,
+                                  m + offsetof(NDhcp4Message, options),
+                                  incoming->n_message - offsetof(NDhcp4Message, options));
+
+        /* prefetch option from FILE */
+        if (overload & N_DHCP4_OVERLOAD_FILE)
+                n_dhcp4_incoming_prefetch(incoming, offset, option,
+                                          m + offsetof(NDhcp4Message, file),
+                                          sizeof(incoming->message.file));
+
+        /* prefetch option from SNAME */
+        if (overload & N_DHCP4_OVERLOAD_SNAME)
+                n_dhcp4_incoming_prefetch(incoming, offset, option,
+                                          m + offsetof(NDhcp4Message, sname),
+                                          sizeof(incoming->message.sname));
+
+        incoming->options[option].value = m + pos;
+        incoming->options[option].size = *offset - pos;
+}
+
+static void n_dhcp4_incoming_linearize(NDhcp4Incoming *incoming) {
+        uint8_t *m, o, l, overload;
+        size_t i, pos, end, offset;
+
+        /*
+         * Linearize all OPTIONs of the incoming message. We know that
+         * @incoming->message is preallocated to be big enough to hold the
+         * entire linearized message _trailing_ the original copy. All we have
+         * to do is walk the raw message in @incoming->message and for each
+         * option we find, copy it into the trailing space, concatenating all
+         * instances we find.
+         *
+         * Before we can copy the individual options, we must scan for the
+         * OVERLOAD option. This is required so our prefetcher knows which data
+         * arrays to scan for prefetching.
+         *
+         * So far, we require the OVERLOAD option to be present in the
+         * options-array (which is obvious and a given). However, if the option
+         * occurs multiple times outside of the options-array (i.e., SNAME or
+         * FILE), we silently ignore them. The specification does not allow
+         * multiple OVERLOAD options, anyway. Hence, this behavior only defines
+         * what we do when we see broken implementations, and we currently seem
+         * to support all styles we saw in the wild so far.
+         */
+
+        m = (uint8_t *)&incoming->message;
+        offset = incoming->n_message;
+
+        n_dhcp4_incoming_merge(incoming, &offset, 0, N_DHCP4_OPTION_OVERLOAD);
+        if (incoming->options[N_DHCP4_OPTION_OVERLOAD].size >= 1)
+                overload = *incoming->options[N_DHCP4_OPTION_OVERLOAD].value;
+        else
+                overload = 0;
+
+        for (i = 0; i < 3; ++i) {
+                if (i == 0) { /* walk OPTIONS */
+                        pos = offsetof(NDhcp4Message, options);
+                        end = incoming->n_message;
+                } else if (i == 1) { /* walk FILE */
+                        if (!(overload & N_DHCP4_OVERLOAD_FILE))
+                                continue;
+
+                        pos = offsetof(NDhcp4Message, file);
+                        end = pos + sizeof(incoming->message.file);
+                } else { /* walk SNAME */
+                        if (!(overload & N_DHCP4_OVERLOAD_SNAME))
+                                continue;
+
+                        pos = offsetof(NDhcp4Message, sname);
+                        end = pos + sizeof(incoming->message.sname);
+                }
+
+                while (pos < end) {
+                        o = m[pos++];
+                        if (o == N_DHCP4_OPTION_PAD)
+                                continue;
+                        if (o == N_DHCP4_OPTION_END)
+                                break;
+                        if (pos >= end)
+                                break;
+
+                        l = m[pos++];
+                        if (l > end || pos > end - l)
+                                break;
+
+                        if (!incoming->options[o].value)
+                                n_dhcp4_incoming_merge(incoming, &offset, overload, o);
+
+                        pos += l;
+                }
+        }
+}
+
+/**
+ * n_dhcp4_incoming_new() - Allocate new incoming message object
+ * @incomingp:          output argument for new object
+ * @raw:                raw message blob
+ * @n_raw:              length of the raw message blob
+ *
+ * This function allocates a new incoming-message object to wrap a received
+ * message blob. It performs basic verification of the message length and
+ * header, and then linearizes the DHCP4 options.
+ *
+ * The incoming-message object mainly provides accessors for the option-array.
+ * It handles all the different quirks around parsing and concatenating the
+ * options array, and provides the assembled data to the caller. It does not,
+ * however, in any way interpret the data of the individual options. This is up
+ * to the caller to do.
+ *
+ * Return: 0 on success, negative error code on failure, N_DHCP4_E_MALFORMED if
+ *         the message is not a valid DHCP4 message.
+ */
+int n_dhcp4_incoming_new(NDhcp4Incoming **incomingp, const void *raw, size_t n_raw) {
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *incoming = NULL;
+        size_t size;
+
+        if (n_raw < sizeof(NDhcp4Message) || n_raw > UINT16_MAX)
+                return N_DHCP4_E_MALFORMED;
+
+        /*
+         * Allocate enough space for book-keeping, a copy of @raw and trailing
+         * space for linearized options. The trailing space must be big enough
+         * to hold the entire options array unmodified (linearizing can only
+         * make it smaller). Hence, just allocate enough space to hold the raw
+         * message without the header.
+         */
+        size = sizeof(*incoming) + n_raw - sizeof(NDhcp4Message);
+        size += n_raw - sizeof(NDhcp4Header);
+
+        incoming = calloc(1, size);
+        if (!incoming)
+                return -ENOMEM;
+
+        *incoming = (NDhcp4Incoming)N_DHCP4_INCOMING_NULL(*incoming);
+        incoming->n_message = n_raw;
+        memcpy(&incoming->message, raw, n_raw);
+
+        if (incoming->message.magic != htobe32(N_DHCP4_MESSAGE_MAGIC))
+                return N_DHCP4_E_MALFORMED;
+
+        /* linearize options */
+        n_dhcp4_incoming_linearize(incoming);
+
+        *incomingp = incoming;
+        incoming = NULL;
+        return 0;
+}
+
+/**
+ * n_dhcp4_incoming_free() - Deallocate message object
+ * @incoming:           object to operate on, or NULL
+ *
+ * This deallocates and frees the given incoming-message object. If NULL is
+ * passed, this is a no-op.
+ *
+ * Return: NULL is returned.
+ */
+NDhcp4Incoming *n_dhcp4_incoming_free(NDhcp4Incoming *incoming) {
+        if (!incoming)
+                return NULL;
+
+        free(incoming);
+
+        return NULL;
+}
+
+/**
+ * n_dhcp4_incoming_get_header() - Return message header
+ * @incoming:           message to operate on
+ *
+ * This returns a pointer to the message header. Note that modifications to
+ * this header are permanent and will affect the original message.
+ *
+ * Return: A pointer to the message header is returned.
+ */
+NDhcp4Header *n_dhcp4_incoming_get_header(NDhcp4Incoming *incoming) {
+        return &incoming->message.header;
+}
+
+/**
+ * n_dhcp4_incoming_get_raw() - Get access to the raw original message
+ * @incoming:           message to operate on
+ * @rawp:               output argument for the raw blob, or NULL
+ *
+ * This hands out a pointer to the raw message blob to the caller. This will
+ * point to the original message content, rather than the linearized version.
+ *
+ * Note that if the caller queried the contents of the message before, any
+ * modifications done by the caller will not affect the original message. It
+ * only affects the linearized content (which is a duplicate trailing the
+ * original message). However, modifications to the message header *DO* also
+ * appear in the original, since the message header is not duplicated.
+ *
+ * In either case, it is better to never modify the message, if you intend to
+ * forward it further.
+ *
+ * Return: The length of the raw message blob is returned.
+ */
+size_t n_dhcp4_incoming_get_raw(NDhcp4Incoming *incoming, const void **rawp) {
+        if (rawp)
+                *rawp = &incoming->message;
+        return incoming->n_message;
+}
+
+/**
+ * n_dhcp4_incoming_query() - Query the contents of a specific option
+ * @incoming:           message to query
+ * @option:             option to look for
+ * @datap:              output argument for the option-data, or NULL
+ * @n_datap:            output argument for the length of the option, or NULL
+ *
+ * This returns a pointer to the requested option blob in the message. It
+ * points to a linearized version of all respective option-fields of the same
+ * type. Hence, the caller is not required to deal with multiple occurrences of
+ * the same option.
+ *
+ * If an option was not present in the incoming message, N_DHCP4_E_UNSET is
+ * returned. Note that this is different from an empty option! And empty option
+ * will return a valid pointer and size 0.
+ *
+ * Note that the pointer to the option-blob does not point to the original
+ * message, but a duplicated version. Modifications to the blob will not be
+ * reflected in the original message, but they will be permanent regarding
+ * further queries through this function.
+ *
+ * Note that the original message alignment might no longer be reflected in the
+ * returned blob. You must not alias the content of the blob, but always copy
+ * it out, or consume piecemeal.
+ *
+ * This function runs in O(1).
+ *
+ * Return: 0 on success, negative error code on failure, N_DHCP4_E_UNSET if the
+ *         option was not found,
+ */
+int n_dhcp4_incoming_query(NDhcp4Incoming *incoming, uint8_t option, uint8_t **datap, size_t *n_datap) {
+        if (!incoming->options[option].value)
+                return N_DHCP4_E_UNSET;
+
+        if (datap)
+                *datap = incoming->options[option].value;
+        if (n_datap)
+                *n_datap = incoming->options[option].size;
+        return 0;
+}
+
+static int n_dhcp4_incoming_query_u8(NDhcp4Incoming *message, uint8_t option, uint8_t *u8p) {
+        uint8_t *data;
+        size_t n_data;
+        int r;
+
+        r = n_dhcp4_incoming_query(message, option, &data, &n_data);
+        if (r)
+                return r;
+        else if (n_data != sizeof(*data))
+                return N_DHCP4_E_MALFORMED;
+
+        *u8p = *data;
+        return 0;
+}
+
+static int n_dhcp4_incoming_query_u16(NDhcp4Incoming *message, uint8_t option, uint16_t *u16p) {
+        uint8_t *data;
+        size_t n_data;
+        uint16_t be16;
+        int r;
+
+        r = n_dhcp4_incoming_query(message, option, &data, &n_data);
+        if (r)
+                return r;
+        else if (n_data != sizeof(be16))
+                return N_DHCP4_E_MALFORMED;
+
+        memcpy(&be16, data, sizeof(be16));
+
+        *u16p = ntohs(be16);
+        return 0;
+}
+
+static int n_dhcp4_incoming_query_u32(NDhcp4Incoming *message, uint8_t option, uint32_t *u32p) {
+        uint8_t *data;
+        size_t n_data;
+        uint32_t be32;
+        int r;
+
+        r = n_dhcp4_incoming_query(message, option, &data, &n_data);
+        if (r)
+                return r;
+        else if (n_data != sizeof(be32))
+                return N_DHCP4_E_MALFORMED;
+
+        memcpy(&be32, data, sizeof(be32));
+
+        if (be32 == (uint32_t)-1)
+                *u32p = 0;
+        else
+                *u32p = ntohl(be32);
+        return 0;
+}
+
+static int n_dhcp4_incoming_query_in_addr(NDhcp4Incoming *message, uint8_t option, struct in_addr *addrp) {
+        uint8_t *data;
+        size_t n_data;
+        uint32_t be32;
+        int r;
+
+        r = n_dhcp4_incoming_query(message, option, &data, &n_data);
+        if (r)
+                return r;
+        else if (n_data != sizeof(be32))
+                return N_DHCP4_E_MALFORMED;
+
+        memcpy(&be32, data, sizeof(be32));
+
+        addrp->s_addr = be32;
+        return 0;
+}
+
+int n_dhcp4_incoming_query_message_type(NDhcp4Incoming *message, uint8_t *typep) {
+        return n_dhcp4_incoming_query_u8(message, N_DHCP4_OPTION_MESSAGE_TYPE, typep);
+}
+
+int n_dhcp4_incoming_query_lifetime(NDhcp4Incoming *message, uint32_t *lifetimep) {
+        return n_dhcp4_incoming_query_u32(message, N_DHCP4_OPTION_IP_ADDRESS_LEASE_TIME, lifetimep);
+}
+
+int n_dhcp4_incoming_query_t2(NDhcp4Incoming *message, uint32_t *t2p) {
+        return n_dhcp4_incoming_query_u32(message, N_DHCP4_OPTION_REBINDING_T2_TIME, t2p);
+}
+
+int n_dhcp4_incoming_query_t1(NDhcp4Incoming *message, uint32_t *t1p) {
+        return n_dhcp4_incoming_query_u32(message, N_DHCP4_OPTION_RENEWAL_T1_TIME, t1p);
+}
+
+int n_dhcp4_incoming_query_server_identifier(NDhcp4Incoming *message, struct in_addr *idp) {
+        return n_dhcp4_incoming_query_in_addr(message, N_DHCP4_OPTION_SERVER_IDENTIFIER, idp);
+}
+
+int n_dhcp4_incoming_query_max_message_size(NDhcp4Incoming *message, uint16_t *max_message_sizep) {
+        return n_dhcp4_incoming_query_u16(message, N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE, max_message_sizep);
+}
+
+int n_dhcp4_incoming_query_requested_ip(NDhcp4Incoming *message, struct in_addr *requested_ipp) {
+        return n_dhcp4_incoming_query_in_addr(message, N_DHCP4_OPTION_REQUESTED_IP_ADDRESS, requested_ipp);
+}
+
+void n_dhcp4_incoming_get_xid(NDhcp4Incoming *message, uint32_t *xidp) {
+        NDhcp4Header *header = n_dhcp4_incoming_get_header(message);
+
+        *xidp = header->xid;
+}
+
+void n_dhcp4_incoming_get_yiaddr(NDhcp4Incoming *message, struct in_addr *yiaddr) {
+        NDhcp4Header *header = n_dhcp4_incoming_get_header(message);
+
+        yiaddr->s_addr = header->yiaddr;
+}
diff --git a/shared/n-dhcp4/src/n-dhcp4-outgoing.c b/shared/n-dhcp4/src/n-dhcp4-outgoing.c
new file mode 100644
index 00000000..c44b5880
--- /dev/null
+++ b/shared/n-dhcp4/src/n-dhcp4-outgoing.c
@@ -0,0 +1,372 @@
+/*
+ * DHCPv4 Outgoing Messages
+ *
+ * XXX
+ */
+
+#include <assert.h>
+#include <c-stdaux.h>
+#include <endian.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <netinet/ip.h>
+#include <netinet/udp.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include "n-dhcp4.h"
+#include "n-dhcp4-private.h"
+
+/**
+ * N_DHCP4_OUTGOING_MAX_PHDR - maximum protocol header size
+ *
+ * All DHCP4 messages-limits specify the size of the entire packet including
+ * the protocol layer (i.e., including the IP headers and UDP headers). To
+ * calculate the size we have remaining for the actual DHCP message, we need to
+ * substract the maximum possible header-length the linux-kernel might prepend
+ * to our messages. This turns out to be the maximum IP-header size (including
+ * optional IP headers, hence 60 bytes) plus the UDP header size (i.e., 8
+ * bytes).
+ */
+#define N_DHCP4_OUTGOING_MAX_PHDR (N_DHCP4_NETWORK_IP_MAXIMUM_HEADER_SIZE + sizeof(struct udphdr))
+
+/**
+ * n_dhcp4_outgoing_new() - Allocate new outgoing message
+ * @outgoingp:          output argument to return allocate object through
+ * @max_size:           maximum transmission size to use
+ * @overload:           select sections to overload
+ *
+ * This allocates a new outgoing message and returns it to the caller. The
+ * caller can then append data to it and send it over the wire.
+ *
+ * The @max_size parameter specifies the transport-layer MTU to consider. If 0,
+ * N_DHCP4_NETWORK_IP_MINIMUM_MAX_SIZE is used. Note that this argument
+ * specifies the maximum packet size *INCLUDING* the IP-headers and UDP-header.
+ * Internally, the allocator makes sure to never create packets bigger than the
+ * specified MTU. The append functions will return an error, if the packet size
+ * would exceed the MTU.
+ * If you use a full UDP stack that supports packet fragmentation, you can
+ * specify the maximum packet size here (e.g., UINT16_MAX).
+ *
+ * Return: 0 on success, error code on failure.
+ */
+int n_dhcp4_outgoing_new(NDhcp4Outgoing **outgoingp, size_t max_size, uint8_t overload) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *outgoing = NULL;
+
+        c_assert(!(overload & ~(N_DHCP4_OVERLOAD_FILE | N_DHCP4_OVERLOAD_SNAME)));
+
+        /*
+         * Make sure the minimum limit is bigger than the maximum protocol
+         * header plus the DHCP-message-header plus a single OPTION_END byte.
+         */
+        static_assert(N_DHCP4_NETWORK_IP_MINIMUM_MAX_SIZE >= N_DHCP4_OUTGOING_MAX_PHDR +
+                                                             sizeof(NDhcp4Message) + 1,
+                      "Invalid minimum IP packet limit");
+
+        outgoing = calloc(1, sizeof(*outgoing));
+        if (!outgoing)
+                return -ENOMEM;
+
+        *outgoing = (NDhcp4Outgoing)N_DHCP4_OUTGOING_NULL(*outgoing);
+        outgoing->n_message = N_DHCP4_NETWORK_IP_MINIMUM_MAX_SIZE - N_DHCP4_OUTGOING_MAX_PHDR;
+        outgoing->i_message = offsetof(NDhcp4Message, options);
+        outgoing->max_size = outgoing->n_message;
+        outgoing->overload = overload;
+
+        if (max_size > N_DHCP4_NETWORK_IP_MINIMUM_MAX_SIZE)
+                outgoing->max_size = max_size - N_DHCP4_OUTGOING_MAX_PHDR;
+
+        outgoing->message = calloc(1, outgoing->n_message);
+        if (!outgoing->message)
+                return -ENOMEM;
+
+        outgoing->message->magic = htonl(N_DHCP4_MESSAGE_MAGIC);
+        outgoing->message->options[0] = N_DHCP4_OPTION_END;
+
+        *outgoingp = outgoing;
+        outgoing = NULL;
+        return 0;
+}
+
+/**
+ * n_dhcp4_outgoing_free() - Deallocate outgoing message
+ * @outgoing:           message to deallocate, or NULL
+ *
+ * This is the opposite to n_dhcp4_outgoing_new(). It deallocates and frees the
+ * passed object. If @outgoing is NULL, this is a no-op.
+ *
+ * Return: NULL is returned.
+ */
+NDhcp4Outgoing *n_dhcp4_outgoing_free(NDhcp4Outgoing *outgoing) {
+        if (!outgoing)
+                return NULL;
+
+        free(outgoing->message);
+        free(outgoing);
+
+        return NULL;
+}
+
+/**
+ * n_dhcp4_outgoing_get_header() - Get pointer to the message header
+ * @outgoing:           message to operate on
+ *
+ * This returns a pointer to the DHCP4 message header to the caller. The caller
+ * can use this to fill-in the header-fields. Note that all fields are
+ * initialized to their default values. Hence, you only need to override the
+ * fields where the default is not sufficient.
+ *
+ * Return: A pointer to the message header is returned.
+ */
+NDhcp4Header *n_dhcp4_outgoing_get_header(NDhcp4Outgoing *outgoing) {
+        return &outgoing->message->header;
+}
+
+/**
+ * n_dhcp4_outgoing_get_raw() - Get the raw message blob
+ * @outgoing:           message to operat on
+ * @rawp:               output argument for the message-blob
+ *
+ * This function gives the caller access to the raw message-blob. That is, once
+ * message-marshaling is complete, use this to get the raw blob for sending.
+ * Note that this blob is only valid as long as you no longer append any
+ * further options to the message, nor modify it in any other way.
+ *
+ * Return: The size of the raw message blob is returned.
+ */
+size_t n_dhcp4_outgoing_get_raw(NDhcp4Outgoing *outgoing, const void **rawp) {
+        if (rawp)
+                *rawp = outgoing->message;
+
+        /*
+         * Return the DHCP message until the END option, excluding any
+         * trailing padding. We overallocate during append, so the
+         * allocated message might be bigger than what we want to
+         * send on the wire.
+         */
+        return outgoing->i_message + 1;
+}
+
+static void n_dhcp4_outgoing_append_option(NDhcp4Outgoing *outgoing,
+                                           uint8_t option,
+                                           const void *data,
+                                           uint8_t n_data) {
+        uint8_t *blob = (void *)outgoing->message;
+
+        blob[outgoing->i_message++] = option;
+        blob[outgoing->i_message++] = n_data;
+        memcpy(blob + outgoing->i_message, data, n_data);
+        outgoing->i_message += n_data;
+        blob[outgoing->i_message] = N_DHCP4_OPTION_END;
+}
+
+/**
+ * n_dhcp4_outgoing_append() - Append option to outgoing message
+ * @outgoing:           message to operate on
+ * @option:             option code to append
+ * @data:               data to append in the option
+ * @n_data:             length of the data blob
+ *
+ * This appends another option to the given outgoing message. The data is taken
+ * verbatim and copied into the message. Note that no validation is done. If
+ * you provide an option multiple times, it will be added multiple times (spec
+ * then requires them to be interpreted as concatenated option, in case the
+ * option is marked as such).
+ *
+ * The size of a message is limited, based on the restriction passed to the
+ * outgoing-message constructor. If there is not enough free space to copy in
+ * the new option, N_DHCP4_E_NO_SPACE is returned.
+ *
+ * The order in which you append options might matter to some implementations.
+ * For example, the message-type is often expected to be the first option. We
+ * do not place such restrictions, but for compatibility with external
+ * implementations, you should follow these recommendations.
+ * Furthermore, we do not implement any kind of smart allocators. That is, all
+ * options are simply appended when you call this. But due to the overloading
+ * feature, fragmentation might matter. Hence, if you use overloading, overly
+ * big options might cause padding, and as such waste space.
+ *
+ * Return: 0 on success, negative error code on failure, N_DHCP4_E_NO_SPACE
+ *         when there is not sufficient free space in the message.
+ */
+int n_dhcp4_outgoing_append(NDhcp4Outgoing *outgoing,
+                            uint8_t option,
+                            const void *data,
+                            uint8_t n_data) {
+        NDhcp4Message *m;
+        uint8_t overload;
+        size_t rem, n;
+
+        c_assert(option != N_DHCP4_OPTION_PAD);
+        c_assert(option != N_DHCP4_OPTION_END);
+        c_assert(option != N_DHCP4_OPTION_OVERLOAD);
+
+        /*
+         * If the iterator is on the OPTIONs field, try appending the new blob.
+         * We need 2 header-bytes plus @n_data bytes. Additionally, we always
+         * reserve 3 trailing bytes for a possible OVERLOAD option, and 1 byte
+         * for the END marker.
+         */
+        if (outgoing->i_message >= offsetof(NDhcp4Message, options)) {
+                rem = outgoing->n_message - outgoing->i_message;
+
+                /* try fitting into remaining OPTIONs space */
+                if (rem >= n_data + 2U + 3U + 1U) {
+                        n_dhcp4_outgoing_append_option(outgoing, option, data, n_data);
+                        return 0;
+                }
+
+                /* try fitting into allowed OPTIONs space */
+                if (outgoing->max_size - outgoing->i_message >= n_data + 2U + 3U + 1U) {
+                        /* try over-allocation to reduce allocation pressure */
+                        n = outgoing->n_message + n_data + 128;
+                        if (n > outgoing->max_size)
+                                n = outgoing->max_size;
+                        m = realloc(outgoing->message, n);
+                        if (!m)
+                                return -ENOMEM;
+
+                        memset((void *)m + outgoing->i_message, 0, n - outgoing->i_message);
+                        outgoing->message = m;
+                        outgoing->n_message = n;
+                        n_dhcp4_outgoing_append_option(outgoing, option, data, n_data);
+                        return 0;
+                }
+
+                /* not enough remaining space, try OVERLOAD */
+                if (!outgoing->overload)
+                        return N_DHCP4_E_NO_SPACE;
+
+                /*
+                 * We ran out of space in the OPTIONs array, but overloading
+                 * was enabled. This means, we can insert an OVERLOAD option
+                 * and then use SNAME/FILE to store more options.
+                 * Note that the three different sections cannot overlap and
+                 * all must have an END marker. So as soon as we add the
+                 * OVERLOAD option, we must make sure the other sections have
+                 * the valid END marker. From then on, our *_append_option()
+                 * helper makes sure to move the END marker with every
+                 * insertion.
+                 */
+                overload = outgoing->overload;
+                n_dhcp4_outgoing_append_option(outgoing, N_DHCP4_OPTION_OVERLOAD, &overload, 1);
+
+                if (overload & N_DHCP4_OVERLOAD_FILE)
+                        outgoing->message->file[0] = N_DHCP4_OPTION_END;
+                if (overload & N_DHCP4_OVERLOAD_SNAME)
+                        outgoing->message->sname[0] = N_DHCP4_OPTION_END;
+
+                if (overload & N_DHCP4_OVERLOAD_FILE)
+                        outgoing->i_message = offsetof(NDhcp4Message, file);
+                else if (overload & N_DHCP4_OVERLOAD_SNAME)
+                        outgoing->i_message = offsetof(NDhcp4Message, sname);
+        }
+
+        /*
+         * The OPTIONs section is full and OVERLOAD was enabled. Try writing
+         * into the FILE section. Always reserve 1 byte for the trailing END
+         * marker.
+         */
+        if (outgoing->i_message >= offsetof(NDhcp4Message, file)) {
+                rem = sizeof(outgoing->message->file);
+                rem -= outgoing->i_message - offsetof(NDhcp4Message, file);
+
+                if (rem >= n_data + 2U + 1U) {
+                        n_dhcp4_outgoing_append_option(outgoing, option, data, n_data);
+                        return 0;
+                }
+
+                if (overload & N_DHCP4_OVERLOAD_SNAME)
+                        outgoing->i_message = offsetof(NDhcp4Message, sname);
+                else
+                        return N_DHCP4_E_NO_SPACE;
+        }
+
+        /*
+         * OPTIONs and FILE are full, try putting data into the SNAME section
+         * as a last resort.
+         */
+        if (outgoing->i_message >= offsetof(NDhcp4Message, sname)) {
+                rem = sizeof(outgoing->message->sname);
+                rem -= outgoing->i_message - offsetof(NDhcp4Message, sname);
+
+                if (rem >= n_data + 2U + 1U) {
+                        n_dhcp4_outgoing_append_option(outgoing, option, data, n_data);
+                        return 0;
+                }
+        }
+
+        return N_DHCP4_E_NO_SPACE;
+}
+
+static int n_dhcp4_outgoing_append_u32(NDhcp4Outgoing *message, uint8_t option, uint32_t u32) {
+        uint32_t be32 = htonl(u32);
+        int r;
+
+        r = n_dhcp4_outgoing_append(message, option, &be32, sizeof(be32));
+        if (r)
+                return r;
+
+        return 0;
+}
+
+static int n_dhcp4_outgoing_append_in_addr(NDhcp4Outgoing *message, uint8_t option, struct in_addr addr) {
+        int r;
+
+        r = n_dhcp4_outgoing_append(message, option, &addr.s_addr, sizeof(addr.s_addr));
+        if (r)
+                return r;
+
+        return 0;
+}
+
+int n_dhcp4_outgoing_append_t1(NDhcp4Outgoing *message, uint32_t t1) {
+        return n_dhcp4_outgoing_append_u32(message, N_DHCP4_OPTION_RENEWAL_T1_TIME, t1);
+}
+
+int n_dhcp4_outgoing_append_t2(NDhcp4Outgoing *message, uint32_t t2) {
+        return n_dhcp4_outgoing_append_u32(message, N_DHCP4_OPTION_REBINDING_T2_TIME, t2);
+}
+
+int n_dhcp4_outgoing_append_lifetime(NDhcp4Outgoing *message, uint32_t lifetime) {
+        return n_dhcp4_outgoing_append_u32(message, N_DHCP4_OPTION_IP_ADDRESS_LEASE_TIME, lifetime);
+}
+
+int n_dhcp4_outgoing_append_server_identifier(NDhcp4Outgoing *message, struct in_addr addr) {
+        return n_dhcp4_outgoing_append_in_addr(message, N_DHCP4_OPTION_SERVER_IDENTIFIER, addr);
+}
+
+int n_dhcp4_outgoing_append_requested_ip(NDhcp4Outgoing *message, struct in_addr addr) {
+        return n_dhcp4_outgoing_append_in_addr(message, N_DHCP4_OPTION_REQUESTED_IP_ADDRESS, addr);
+}
+
+void n_dhcp4_outgoing_set_secs(NDhcp4Outgoing *message, uint32_t secs) {
+        NDhcp4Header *header = n_dhcp4_outgoing_get_header(message);
+
+        /*
+         * Some DHCP servers will reject DISCOVER or REQUEST messages if 'secs'
+         * is not set (i.e., set to 0), even though the spec allows it.
+         */
+        c_assert(secs);
+
+        header->secs = htonl(secs);
+}
+
+void n_dhcp4_outgoing_set_xid(NDhcp4Outgoing *message, uint32_t xid) {
+        NDhcp4Header *header = n_dhcp4_outgoing_get_header(message);
+
+        header->xid = xid;
+}
+
+void n_dhcp4_outgoing_get_xid(NDhcp4Outgoing *message, uint32_t *xidp) {
+        NDhcp4Header *header = n_dhcp4_outgoing_get_header(message);
+
+        *xidp = header->xid;
+}
+
+void n_dhcp4_outgoing_set_yiaddr(NDhcp4Outgoing *message, struct in_addr yiaddr) {
+        NDhcp4Header *header = n_dhcp4_outgoing_get_header(message);
+
+        header->yiaddr = yiaddr.s_addr;
+}
diff --git a/shared/n-dhcp4/src/n-dhcp4-private.h b/shared/n-dhcp4/src/n-dhcp4-private.h
new file mode 100644
index 00000000..c38ddbfc
--- /dev/null
+++ b/shared/n-dhcp4/src/n-dhcp4-private.h
@@ -0,0 +1,688 @@
+#pragma once
+
+#include <arpa/inet.h>
+#include <assert.h>
+#include <c-list.h>
+#include <c-stdaux.h>
+#include <endian.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <time.h>
+#include <unistd.h>
+#include "n-dhcp4.h"
+
+typedef struct NDhcp4CConnection NDhcp4CConnection;
+typedef struct NDhcp4CEventNode NDhcp4CEventNode;
+typedef struct NDhcp4ClientProbeOption NDhcp4ClientProbeOption;
+typedef struct NDhcp4Header NDhcp4Header;
+typedef struct NDhcp4Incoming NDhcp4Incoming;
+typedef struct NDhcp4Message NDhcp4Message;
+typedef struct NDhcp4Outgoing NDhcp4Outgoing;
+typedef struct NDhcp4SConnection NDhcp4SConnection;
+typedef struct NDhcp4SConnectionIp NDhcp4SConnectionIp;
+typedef struct NDhcp4SEventNode NDhcp4SEventNode;
+
+/* specs */
+
+#define N_DHCP4_NETWORK_IP_MAXIMUM_HEADER_SIZE (60) /* See RFC791 */
+#define N_DHCP4_NETWORK_IP_MINIMUM_MAX_SIZE (576) /* See RFC791 */
+#define N_DHCP4_NETWORK_SERVER_PORT (67)
+#define N_DHCP4_NETWORK_CLIENT_PORT (68)
+#define N_DHCP4_MESSAGE_MAGIC ((uint32_t)(0x63825363))
+#define N_DHCP4_MESSAGE_FLAG_BROADCAST (htons(0x8000))
+
+enum {
+        N_DHCP4_OP_BOOTREQUEST                          = 1,
+        N_DHCP4_OP_BOOTREPLY                            = 2,
+};
+
+enum {
+        N_DHCP4_OPTION_PAD                              = 0,
+        N_DHCP4_OPTION_SUBNET_MASK                      = 1,
+        N_DHCP4_OPTION_TIME_OFFSET                      = 2,
+        N_DHCP4_OPTION_ROUTER                           = 3,
+        N_DHCP4_OPTION_DOMAIN_NAME_SERVER               = 6,
+        N_DHCP4_OPTION_HOST_NAME                        = 12,
+        N_DHCP4_OPTION_BOOT_FILE_SIZE                   = 13,
+        N_DHCP4_OPTION_DOMAIN_NAME                      = 15,
+        N_DHCP4_OPTION_ROOT_PATH                        = 17,
+        N_DHCP4_OPTION_ENABLE_IP_FORWARDING             = 19,
+        N_DHCP4_OPTION_ENABLE_IP_FORWARDING_NL          = 20,
+        N_DHCP4_OPTION_POLICY_FILTER                    = 21,
+        N_DHCP4_OPTION_INTERFACE_MDR                    = 22,
+        N_DHCP4_OPTION_INTERFACE_TTL                    = 23,
+        N_DHCP4_OPTION_INTERFACE_MTU_AGING_TIMEOUT      = 24,
+        N_DHCP4_OPTION_INTERFACE_MTU                    = 26,
+        N_DHCP4_OPTION_BROADCAST                        = 28,
+        N_DHCP4_OPTION_STATIC_ROUTE                     = 33,
+        N_DHCP4_OPTION_NTP_SERVER                       = 42,
+        N_DHCP4_OPTION_VENDOR_SPECIFIC                  = 43,
+        N_DHCP4_OPTION_REQUESTED_IP_ADDRESS             = 50,
+        N_DHCP4_OPTION_IP_ADDRESS_LEASE_TIME            = 51,
+        N_DHCP4_OPTION_OVERLOAD                         = 52,
+        N_DHCP4_OPTION_MESSAGE_TYPE                     = 53,
+        N_DHCP4_OPTION_SERVER_IDENTIFIER                = 54,
+        N_DHCP4_OPTION_PARAMETER_REQUEST_LIST           = 55,
+        N_DHCP4_OPTION_ERROR_MESSAGE                    = 56,
+        N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE             = 57,
+        N_DHCP4_OPTION_RENEWAL_T1_TIME                  = 58,
+        N_DHCP4_OPTION_REBINDING_T2_TIME                = 59,
+        N_DHCP4_OPTION_VENDOR_CLASS_IDENTIFIER          = 60,
+        N_DHCP4_OPTION_CLIENT_IDENTIFIER                = 61,
+        N_DHCP4_OPTION_FQDN                             = 81,
+        N_DHCP4_OPTION_NEW_POSIX_TIMEZONE               = 100,
+        N_DHCP4_OPTION_NEW_TZDB_TIMEZONE                = 101,
+        N_DHCP4_OPTION_CLASSLESS_STATIC_ROUTE           = 121,
+        N_DHCP4_OPTION_PRIVATE_BASE                     = 224,
+        N_DHCP4_OPTION_PRIVATE_LAST                     = 254,
+        N_DHCP4_OPTION_END                              = 255,
+        _N_DHCP4_OPTION_N                               = 256,
+};
+
+enum {
+        N_DHCP4_OVERLOAD_FILE                           = 1,
+        N_DHCP4_OVERLOAD_SNAME                          = 2,
+};
+
+enum {
+        N_DHCP4_MESSAGE_DISCOVER                        = 1,
+        N_DHCP4_MESSAGE_OFFER                           = 2,
+        N_DHCP4_MESSAGE_REQUEST                         = 3,
+        N_DHCP4_MESSAGE_DECLINE                         = 4,
+        N_DHCP4_MESSAGE_ACK                             = 5,
+        N_DHCP4_MESSAGE_NAK                             = 6,
+        N_DHCP4_MESSAGE_RELEASE                         = 7,
+        N_DHCP4_MESSAGE_INFORM                          = 8,
+        N_DHCP4_MESSAGE_FORCERENEW                      = 9,
+};
+
+struct NDhcp4Header {
+        uint8_t op;
+        uint8_t htype;
+        uint8_t hlen;
+        uint8_t hops;
+        uint32_t xid;
+        uint16_t secs;
+        uint16_t flags;
+        uint32_t ciaddr;
+        uint32_t yiaddr;
+        uint32_t siaddr;
+        uint32_t giaddr;
+        uint8_t chaddr[16];
+} _c_packed_;
+
+struct NDhcp4Message {
+        NDhcp4Header header;
+        uint8_t sname[64];
+        uint8_t file[128];
+        uint32_t magic;
+        uint8_t options[];
+} _c_packed_;
+
+/* objects */
+
+enum {
+        _N_DHCP4_E_INTERNAL = _N_DHCP4_E_N,
+
+        N_DHCP4_E_UNEXPECTED,
+
+        N_DHCP4_E_NO_SPACE,
+        N_DHCP4_E_MALFORMED,
+
+        N_DHCP4_E_DROPPED,
+        N_DHCP4_E_DOWN,
+        N_DHCP4_E_AGAIN,
+};
+
+enum {
+        N_DHCP4_C_CONNECTION_STATE_INIT,
+        N_DHCP4_C_CONNECTION_STATE_PACKET,
+        N_DHCP4_C_CONNECTION_STATE_DRAINING,
+        N_DHCP4_C_CONNECTION_STATE_UDP,
+        N_DHCP4_C_CONNECTION_STATE_CLOSED,
+};
+
+enum {
+        N_DHCP4_CLIENT_EPOLL_TIMER,
+        N_DHCP4_CLIENT_EPOLL_IO,
+};
+
+enum {
+        N_DHCP4_CLIENT_PROBE_STATE_INIT,
+        N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT,
+        N_DHCP4_CLIENT_PROBE_STATE_SELECTING,
+        N_DHCP4_CLIENT_PROBE_STATE_REBOOTING,
+        N_DHCP4_CLIENT_PROBE_STATE_REQUESTING,
+        N_DHCP4_CLIENT_PROBE_STATE_GRANTED,
+        N_DHCP4_CLIENT_PROBE_STATE_BOUND,
+        N_DHCP4_CLIENT_PROBE_STATE_RENEWING,
+        N_DHCP4_CLIENT_PROBE_STATE_REBINDING,
+        N_DHCP4_CLIENT_PROBE_STATE_EXPIRED,
+};
+
+enum {
+        N_DHCP4_CLIENT_LEASE_STATE_INIT,
+        N_DHCP4_CLIENT_LEASE_STATE_OFFERED,
+        N_DHCP4_CLIENT_LEASE_STATE_SELECTED,
+        N_DHCP4_CLIENT_LEASE_STATE_DECLINED,
+        N_DHCP4_CLIENT_LEASE_STATE_ACKED,
+};
+
+enum {
+        N_DHCP4_SERVER_EPOLL_TIMER,
+        N_DHCP4_SERVER_EPOLL_IO,
+};
+
+enum {
+        _N_DHCP4_C_MESSAGE_INVALID = 0,
+        N_DHCP4_C_MESSAGE_DISCOVER,
+        N_DHCP4_C_MESSAGE_INFORM,
+        N_DHCP4_C_MESSAGE_SELECT,
+        N_DHCP4_C_MESSAGE_IGNORE,
+        N_DHCP4_C_MESSAGE_RENEW,
+        N_DHCP4_C_MESSAGE_REBIND,
+        N_DHCP4_C_MESSAGE_REBOOT,
+        N_DHCP4_C_MESSAGE_RELEASE,
+        N_DHCP4_C_MESSAGE_DECLINE,
+};
+
+struct NDhcp4Outgoing {
+        NDhcp4Message *message;
+        size_t n_message;
+        size_t i_message;
+        size_t max_size;
+
+        uint8_t overload : 2;
+
+        struct {
+                uint8_t type;
+                uint64_t start_time;
+                uint64_t base_time;
+                uint64_t send_time;
+                uint64_t send_jitter;
+                size_t n_send;
+        } userdata;
+};
+
+#define N_DHCP4_OUTGOING_NULL(_x) {                                             \
+        }
+
+struct NDhcp4Incoming {
+        struct {
+                uint8_t *value;
+                size_t size;
+        } options[_N_DHCP4_OPTION_N];
+
+        struct {
+                uint8_t type;
+                uint64_t start_time;
+                uint64_t base_time;
+        } userdata;
+
+        size_t n_message;
+        NDhcp4Message message;
+        /* @message must be the last member */
+};
+
+#define N_DHCP4_INCOMING_NULL(_x) {                                             \
+        }
+
+struct NDhcp4ClientConfig {
+        int ifindex;
+        unsigned int transport;
+        bool request_broadcast;
+        uint8_t mac[32]; /* MAX_ADDR_LEN */
+        size_t n_mac;
+        uint8_t broadcast_mac[32]; /* MAX_ADDR_LEN */
+        size_t n_broadcast_mac;
+        uint8_t *client_id;
+        size_t n_client_id;
+};
+
+#define N_DHCP4_CLIENT_CONFIG_NULL(_x) {                                        \
+                .transport = _N_DHCP4_TRANSPORT_N,                              \
+        }
+
+struct NDhcp4ClientProbeOption {
+        uint8_t option;
+        uint8_t n_data;
+        uint8_t data[];
+};
+
+#define N_DHCP4_CLIENT_PROBE_OPTION_NULL(_x) {                                  \
+                .option = N_DHCP4_OPTION_PAD,                                   \
+        }
+
+struct NDhcp4ClientProbeConfig {
+        bool inform_only;
+        bool init_reboot;
+        struct in_addr requested_ip;
+        struct drand48_data entropy;    /* entropy pool */
+        uint64_t ms_start_delay;        /* max ms to wait before starting probe */
+        NDhcp4ClientProbeOption *options[UINT8_MAX + 1];
+        int8_t request_parameters[UINT8_MAX + 1];
+        size_t n_request_parameters;
+};
+
+#define N_DHCP4_CLIENT_PROBE_CONFIG_NULL(_x) {                                  \
+                .ms_start_delay = N_DHCP4_CLIENT_START_DELAY_RFC2131,           \
+        }
+
+struct NDhcp4CEventNode {
+        CList client_link;
+        CList probe_link;
+        NDhcp4ClientEvent event;
+        bool is_public : 1;
+};
+
+#define N_DHCP4_C_EVENT_NODE_NULL(_x) {                                         \
+                .client_link = C_LIST_INIT((_x).client_link),                   \
+                .probe_link = C_LIST_INIT((_x).probe_link),                     \
+        }
+
+struct NDhcp4CConnection {
+        NDhcp4ClientConfig *client_config;
+        NDhcp4ClientProbeConfig *probe_config;
+        int fd_epoll;
+
+        unsigned int state;             /* current connection state */
+        int fd_packet;                  /* packet socket */
+        int fd_udp;                     /* udp socket */
+
+        NDhcp4Outgoing *request;        /* current request */
+
+        uint32_t client_ip;             /* client IP address, or 0 */
+        uint32_t server_ip;             /* server IP address, or 0 */
+        uint16_t mtu;                   /* client mtu, or 0 */
+
+        /*
+         * When we get DHCP packets from the kernel, we need a buffer to read
+         * the data into. Since UDP packets can be up to 2^16 bytes in size, we
+         * avoid placing it on the stack and instead read into this scratch
+         * buffer. It is purely meant as stack replacement, no data is returned
+         * through this buffer.
+         */
+        uint8_t scratch_buffer[UINT16_MAX];
+};
+
+#define N_DHCP4_C_CONNECTION_NULL(_x) {                                         \
+                .fd_packet = -1,                                                \
+                .fd_udp = -1,                                                   \
+        }
+
+struct NDhcp4Client {
+        unsigned long n_refs;
+        NDhcp4ClientConfig *config;
+        CList event_list;
+        int fd_epoll;
+        int fd_timer;
+
+        uint16_t mtu;
+        NDhcp4ClientProbe *current_probe;
+        uint64_t scheduled_timeout;
+
+        bool preempted : 1;
+};
+
+#define N_DHCP4_CLIENT_NULL(_x) {                                               \
+                .n_refs = 1,                                                    \
+                .event_list = C_LIST_INIT((_x).event_list),                     \
+                .fd_epoll = -1,                                                 \
+                .fd_timer = -1,                                                 \
+        }
+
+struct NDhcp4ClientProbe {
+        NDhcp4ClientProbeConfig *config;
+        NDhcp4Client *client;
+        CList event_list;
+        CList lease_list;
+        void *userdata;
+
+        unsigned int state;                     /* current probe state */
+        uint64_t ns_deferred;                   /* timeout for deferred action */
+        NDhcp4ClientLease *current_lease;       /* current lease */
+
+        NDhcp4CConnection connection;           /* client connection wrapper */
+};
+
+#define N_DHCP4_CLIENT_PROBE_NULL(_x) {                                         \
+                .event_list = C_LIST_INIT((_x).event_list),                     \
+                .lease_list = C_LIST_INIT((_x).lease_list),                     \
+                .connection = N_DHCP4_C_CONNECTION_NULL((_x).connection),       \
+        }
+
+struct NDhcp4ClientLease {
+        unsigned long n_refs;
+
+        NDhcp4ClientProbe *probe;
+        CList probe_link;
+
+        NDhcp4Incoming *message;
+
+        uint64_t t1;
+        uint64_t t2;
+        uint64_t lifetime;
+};
+
+#define N_DHCP4_CLIENT_LEASE_NULL(_x) {                                         \
+                .n_refs = 1,                                                    \
+                .probe_link = C_LIST_INIT((_x).probe_link),                     \
+        }
+
+struct NDhcp4ServerConfig {
+        int ifindex;
+};
+
+#define N_DHCP4_SERVER_CONFIG_NULL(_x) {                                        \
+        }
+
+struct NDhcp4SEventNode {
+        CList server_link;
+        NDhcp4ServerEvent event;
+        bool is_public : 1;
+};
+
+#define N_DHCP4_S_EVENT_NODE_NULL(_x) {                                         \
+                .server_link = C_LIST_INIT((_x).server_link),                   \
+        }
+
+struct NDhcp4SConnection {
+        int ifindex;                    /* interface index */
+        int fd_packet;                  /* packet socket */
+        int fd_udp;                     /* udp socket */
+        uint8_t buf[UINT16_MAX];        /* scratch recevie buffer */
+
+        /* XXX: support a set of server addresses */
+        NDhcp4SConnectionIp *ip;        /* server IP address, or NULL */
+};
+
+#define N_DHCP4_S_CONNECTION_NULL(_x) {                                         \
+                .fd_packet = -1,                                                \
+                .fd_udp = -1,                                                   \
+        }
+
+struct NDhcp4SConnectionIp {
+        NDhcp4SConnection *connection;
+        struct in_addr ip;
+};
+
+#define N_DHCP4_S_CONNECTION_IP_NULL(_x) {                                      \
+}
+
+struct NDhcp4Server {
+        unsigned long n_refs;
+        CList event_list;
+        CList lease_list;
+
+        bool preempted : 1;
+
+        NDhcp4SConnection connection;
+};
+
+#define N_DHCP4_SERVER_NULL(_x) {                                               \
+                .n_refs = 1,                                                    \
+                .event_list = C_LIST_INIT((_x).event_list),                     \
+                .lease_list = C_LIST_INIT((_x).lease_list),                     \
+                .connection = N_DHCP4_S_CONNECTION_NULL((_x).connection),       \
+        }
+
+struct NDhcp4ServerIp {
+                NDhcp4SConnectionIp ip;
+};
+
+#define N_DHCP4_SERVER_IP_NULL(_x) {                                            \
+                .ip = N_DHCP4_S_CONNECTION_IP_NULL((_x).ip),                    \
+        }
+
+struct NDhcp4ServerLease {
+        unsigned long n_refs;
+
+        NDhcp4Server *server;
+        CList server_link;
+
+        NDhcp4Incoming *request;
+        NDhcp4Incoming *reply;
+};
+
+#define N_DHCP4_SERVER_LEASE_NULL(_x) {                                         \
+                .n_refs = 1,                                                    \
+                .server_link = C_LIST_INIT((_x).server_link),                   \
+        }
+
+/* outgoing messages */
+
+int n_dhcp4_outgoing_new(NDhcp4Outgoing **outgoingp, size_t max_size, uint8_t overload);
+NDhcp4Outgoing *n_dhcp4_outgoing_free(NDhcp4Outgoing *outgoing);
+
+NDhcp4Header *n_dhcp4_outgoing_get_header(NDhcp4Outgoing *outgoing);
+size_t n_dhcp4_outgoing_get_raw(NDhcp4Outgoing *outgoing, const void **rawp);
+int n_dhcp4_outgoing_append(NDhcp4Outgoing *outgoing, uint8_t option, const void *data, uint8_t n_data);
+
+int n_dhcp4_outgoing_append_t1(NDhcp4Outgoing *message, uint32_t t1);
+int n_dhcp4_outgoing_append_t2(NDhcp4Outgoing *message, uint32_t t2);
+int n_dhcp4_outgoing_append_lifetime(NDhcp4Outgoing *message, uint32_t lifetime);
+int n_dhcp4_outgoing_append_server_identifier(NDhcp4Outgoing *message, struct in_addr addr);
+int n_dhcp4_outgoing_append_requested_ip(NDhcp4Outgoing *message, struct in_addr addr);
+
+void n_dhcp4_outgoing_set_secs(NDhcp4Outgoing *message, uint32_t secs);
+void n_dhcp4_outgoing_set_xid(NDhcp4Outgoing *message, uint32_t xid);
+void n_dhcp4_outgoing_set_yiaddr(NDhcp4Outgoing *message, struct in_addr yiaddr);
+
+void n_dhcp4_outgoing_get_xid(NDhcp4Outgoing *message, uint32_t *xidp);
+
+/* incoming messages */
+
+int n_dhcp4_incoming_new(NDhcp4Incoming **incomingp, const void *raw, size_t n_raw);
+NDhcp4Incoming *n_dhcp4_incoming_free(NDhcp4Incoming *incoming);
+
+NDhcp4Header *n_dhcp4_incoming_get_header(NDhcp4Incoming *incoming);
+size_t n_dhcp4_incoming_get_raw(NDhcp4Incoming *incoming, const void **rawp);
+int n_dhcp4_incoming_query(NDhcp4Incoming *incoming, uint8_t option, uint8_t **datap, size_t *n_datap);
+
+int n_dhcp4_incoming_query_message_type(NDhcp4Incoming *message, uint8_t *typep);
+int n_dhcp4_incoming_query_lifetime(NDhcp4Incoming *message, uint32_t *lifetimep);
+int n_dhcp4_incoming_query_t2(NDhcp4Incoming *message, uint32_t *t2p);
+int n_dhcp4_incoming_query_t1(NDhcp4Incoming *message, uint32_t *t1p);
+int n_dhcp4_incoming_query_server_identifier(NDhcp4Incoming *message, struct in_addr *idp);
+int n_dhcp4_incoming_query_max_message_size(NDhcp4Incoming *message, uint16_t *max_message_sizep);
+int n_dhcp4_incoming_query_requested_ip(NDhcp4Incoming *message, struct in_addr *requested_ipp);
+
+void n_dhcp4_incoming_get_xid(NDhcp4Incoming *message, uint32_t *xidp);
+void n_dhcp4_incoming_get_yiaddr(NDhcp4Incoming *message, struct in_addr *yiaddr);
+
+/* sockets */
+
+int n_dhcp4_c_socket_packet_new(int *sockfdp, int ifindex);
+int n_dhcp4_c_socket_udp_new(int *sockfdp,
+                             int ifindex,
+                             const struct in_addr *client_addr,
+                             const struct in_addr *server_addr);
+int n_dhcp4_s_socket_packet_new(int *sockfdp);
+int n_dhcp4_s_socket_udp_new(int *sockfdp, int ifindex);
+
+int n_dhcp4_c_socket_packet_send(int sockfd,
+                                 int ifindex,
+                                 const unsigned char *dest_haddr,
+                                 unsigned char halen,
+                                 NDhcp4Outgoing *message);
+int n_dhcp4_c_socket_udp_send(int sockfd, NDhcp4Outgoing *message);
+int n_dhcp4_c_socket_udp_broadcast(int sockfd, NDhcp4Outgoing *message);
+int n_dhcp4_s_socket_packet_send(int sockfd,
+                                 int ifindex,
+                                 const struct in_addr *src_inaddr,
+                                 const unsigned char *dest_haddr,
+                                 unsigned char halen,
+                                 const struct in_addr *dest_inaddr,
+                                 NDhcp4Outgoing *message);
+int n_dhcp4_s_socket_udp_send(int sockfd,
+                              const struct in_addr *inaddr_src,
+                              const struct in_addr *inaddr_dest,
+                              NDhcp4Outgoing *message);
+int n_dhcp4_s_socket_udp_broadcast(int sockfd,
+                                   const struct in_addr *inaddr_src,
+                                   NDhcp4Outgoing *message);
+
+int n_dhcp4_c_socket_packet_recv(int sockfd,
+                                 uint8_t *buf,
+                                 size_t n_buf,
+                                 NDhcp4Incoming **messagep);
+int n_dhcp4_c_socket_udp_recv(int sockfd,
+                              uint8_t *buf,
+                              size_t n_buf,
+                              NDhcp4Incoming **messagep);
+int n_dhcp4_s_socket_udp_recv(int sockfd,
+                              uint8_t *buf,
+                              size_t n_buf,
+                              NDhcp4Incoming **messagep,
+                              struct sockaddr_in *dest);
+
+/* client configs */
+
+int n_dhcp4_client_config_dup(NDhcp4ClientConfig *config,
+                              NDhcp4ClientConfig **dupp);
+
+/* client probe configs */
+
+int n_dhcp4_client_probe_config_dup(NDhcp4ClientProbeConfig *config,
+                                    NDhcp4ClientProbeConfig **dupp);
+uint32_t n_dhcp4_client_probe_config_get_random(NDhcp4ClientProbeConfig *config);
+
+/* client events */
+
+int n_dhcp4_c_event_node_new(NDhcp4CEventNode **nodep);
+NDhcp4CEventNode *n_dhcp4_c_event_node_free(NDhcp4CEventNode *node);
+
+/* client connections */
+
+int n_dhcp4_c_connection_init(NDhcp4CConnection *connection,
+                              NDhcp4ClientConfig *client_config,
+                              NDhcp4ClientProbeConfig *probe_config,
+                              int fd_epoll);
+void n_dhcp4_c_connection_deinit(NDhcp4CConnection *connection);
+
+int n_dhcp4_c_connection_listen(NDhcp4CConnection *connection);
+int n_dhcp4_c_connection_connect(NDhcp4CConnection *connection,
+                                 const struct in_addr *client,
+                                 const struct in_addr *server);
+void n_dhcp4_c_connection_close(NDhcp4CConnection *connection);
+
+void n_dhcp4_c_connection_get_timeout(NDhcp4CConnection *connection,
+                                      uint64_t *timeoutp);
+
+int n_dhcp4_c_connection_discover_new(NDhcp4CConnection *connection,
+                                      NDhcp4Outgoing **request);
+int n_dhcp4_c_connection_select_new(NDhcp4CConnection *connection,
+                                    NDhcp4Outgoing **request,
+                                    NDhcp4Incoming *offer);
+int n_dhcp4_c_connection_reboot_new(NDhcp4CConnection *connection,
+                                    NDhcp4Outgoing **request,
+                                    const struct in_addr *client);
+int n_dhcp4_c_connection_renew_new(NDhcp4CConnection *connection,
+                                   NDhcp4Outgoing **request);
+int n_dhcp4_c_connection_rebind_new(NDhcp4CConnection *connection,
+                                    NDhcp4Outgoing **request);
+int n_dhcp4_c_connection_decline_new(NDhcp4CConnection *connection,
+                                     NDhcp4Outgoing **request,
+                                     NDhcp4Incoming *ack,
+                                     const char *error);
+int n_dhcp4_c_connection_inform_new(NDhcp4CConnection *connection,
+                                    NDhcp4Outgoing **request);
+int n_dhcp4_c_connection_release_new(NDhcp4CConnection *connection,
+                                     NDhcp4Outgoing **request,
+                                     const char *error);
+
+int n_dhcp4_c_connection_start_request(NDhcp4CConnection *connection,
+                                       NDhcp4Outgoing *request,
+                                       uint64_t timestamp);
+int n_dhcp4_c_connection_dispatch_timer(NDhcp4CConnection *connection,
+                                        uint64_t timestamp);
+int n_dhcp4_c_connection_dispatch_io(NDhcp4CConnection *connection,
+                                     NDhcp4Incoming **messagep);
+
+/* clients */
+
+int n_dhcp4_client_raise(NDhcp4Client *client, NDhcp4CEventNode **nodep, unsigned int event);
+void n_dhcp4_client_arm_timer(NDhcp4Client *client);
+
+/* client probes */
+
+int n_dhcp4_client_probe_new(NDhcp4ClientProbe **probep,
+                             NDhcp4ClientProbeConfig *config,
+                             NDhcp4Client *client,
+                             uint64_t ns_now);
+
+int n_dhcp4_client_probe_raise(NDhcp4ClientProbe *probe, NDhcp4CEventNode **nodep, unsigned int event);
+void n_dhcp4_client_probe_get_timeout(NDhcp4ClientProbe *probe, uint64_t *timeoutp);
+int n_dhcp4_client_probe_dispatch_timer(NDhcp4ClientProbe *probe, uint64_t ns_now);
+int n_dhcp4_client_probe_dispatch_io(NDhcp4ClientProbe *probe, uint32_t events);
+int n_dhcp4_client_probe_transition_select(NDhcp4ClientProbe *probe, NDhcp4Incoming *offer, uint64_t ns_now);
+int n_dhcp4_client_probe_transition_accept(NDhcp4ClientProbe *probe, NDhcp4Incoming *ack);
+int n_dhcp4_client_probe_transition_decline(NDhcp4ClientProbe *probe, NDhcp4Incoming *offer, const char *error, uint64_t ns_now);
+int n_dhcp4_client_probe_update_mtu(NDhcp4ClientProbe *probe, uint16_t mtu);
+
+/* client leases */
+
+int n_dhcp4_client_lease_new(NDhcp4ClientLease **leasep, NDhcp4Incoming *message);
+void n_dhcp4_client_lease_link(NDhcp4ClientLease *lease, NDhcp4ClientProbe *probe);
+void n_dhcp4_client_lease_unlink(NDhcp4ClientLease *lease);
+
+/* server connections */
+
+int n_dhcp4_s_connection_init(NDhcp4SConnection *connection, int ifindex);
+void n_dhcp4_s_connection_deinit(NDhcp4SConnection *connection);
+
+void n_dhcp4_s_connection_get_fd(NDhcp4SConnection *connection, int *fdp);
+int n_dhcp4_s_connection_dispatch_io(NDhcp4SConnection *connection, NDhcp4Incoming **messagep);
+
+int n_dhcp4_s_connection_offer_new(NDhcp4SConnection *connection,
+                                   NDhcp4Outgoing **replyp,
+                                   NDhcp4Incoming *request,
+                                   const struct in_addr *server_address,
+                                   const struct in_addr *client_address,
+                                   uint32_t lifetime);
+int n_dhcp4_s_connection_ack_new(NDhcp4SConnection *connection,
+                                   NDhcp4Outgoing **replyp,
+                                   NDhcp4Incoming *request,
+                                   const struct in_addr *server_address,
+                                   const struct in_addr *client_address,
+                                   uint32_t lifetime);
+int n_dhcp4_s_connection_nak_new(NDhcp4SConnection *connection,
+                                 NDhcp4Outgoing **replyp,
+                                 NDhcp4Incoming *request,
+                                 const struct in_addr *server_address);
+
+int n_dhcp4_s_connection_send_reply(NDhcp4SConnection *connection,
+                                    const struct in_addr *server_addr,
+                                    NDhcp4Outgoing *reply);
+
+/* server connection ips */
+
+void n_dhcp4_s_connection_ip_init(NDhcp4SConnectionIp *ip, struct in_addr addr);
+void n_dhcp4_s_connection_ip_deinit(NDhcp4SConnectionIp *ip);
+
+void n_dhcp4_s_connection_ip_link(NDhcp4SConnectionIp *ip, NDhcp4SConnection *connection);
+void n_dhcp4_s_connection_ip_unlink(NDhcp4SConnectionIp *ip);
+
+/* inline helpers */
+
+static inline void n_dhcp4_outgoing_freep(NDhcp4Outgoing **outgoing) {
+        if (*outgoing)
+                n_dhcp4_outgoing_free(*outgoing);
+}
+
+static inline void n_dhcp4_incoming_freep(NDhcp4Incoming **incoming) {
+        if (*incoming)
+                n_dhcp4_incoming_free(*incoming);
+}
+
+static inline uint64_t n_dhcp4_gettime(clockid_t clock) {
+        struct timespec ts;
+        int r;
+
+        r = clock_gettime(clock, &ts);
+        c_assert(r >= 0);
+
+        return ts.tv_sec * 1000ULL * 1000ULL * 1000ULL + ts.tv_nsec;
+}
diff --git a/shared/n-dhcp4/src/n-dhcp4-socket.c b/shared/n-dhcp4/src/n-dhcp4-socket.c
new file mode 100644
index 00000000..b9ac176f
--- /dev/null
+++ b/shared/n-dhcp4/src/n-dhcp4-socket.c
@@ -0,0 +1,657 @@
+/*
+ * DHCP specific low-level socket helpers
+ */
+
+#include <c-stdaux.h>
+#include <errno.h>
+#include <linux/filter.h>
+#include <sys/socket.h> /* needed by linux/if.h */
+#include <linux/if.h>
+#include <linux/if_packet.h>
+#include <linux/netdevice.h>
+#include <linux/udp.h>
+#include <netinet/ip.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/types.h>
+#include "n-dhcp4-private.h"
+#include "util/packet.h"
+#include "util/socket.h"
+
+/**
+ * n_dhcp4_c_socket_packet_new() - create a new DHCP4 client packet socket
+ * @sockfdp:            return argumnet for the new socket
+ * @ifindex:            interface index to bind to
+ *
+ * Create a new AF_PACKET/SOCK_DGRAM socket usable to listen to and send DHCP client
+ * packets before an IP address has been configured.
+ *
+ * Only unfragmented DHCP packets from a server to a client destined for the given
+ * ifindex is returned.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+int n_dhcp4_c_socket_packet_new(int *sockfdp, int ifindex) {
+        _c_cleanup_(c_closep) int sockfd = -1;
+        struct sock_filter filter[] = {
+                /*
+                 * IP
+                 *
+                 * Check
+                 *  - UDP
+                 *  - Unfragmented
+                 *  - Large enough to fit the DHCP header
+                 *
+                 *  Leave X the size of the IP header, for future indirect reads.
+                 */
+                BPF_STMT(BPF_LD + BPF_B + BPF_ABS, offsetof(struct iphdr, protocol)),                           /* A <- IP protocol */
+                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, IPPROTO_UDP, 1, 0),                                         /* IP protocol == UDP ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                BPF_STMT(BPF_LD + BPF_B + BPF_ABS, offsetof(struct iphdr, frag_off)),                           /* A <- Flags */
+                BPF_STMT(BPF_ALU + BPF_AND + BPF_K, ntohs(IP_MF | IP_OFFMASK)),                                 /* A <- A & (IP_MF | IP_OFFMASK) */
+                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0, 1, 0),                                                   /* fragmented packet ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                BPF_STMT(BPF_LDX + BPF_B + BPF_MSH, 0),                                                         /* X <- IP header length */
+                BPF_STMT(BPF_LD + BPF_W + BPF_LEN, 0),                                                          /* A <- packet length */
+                BPF_STMT(BPF_ALU + BPF_SUB + BPF_X, 0),                                                         /* A -= X */
+                BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, sizeof(struct udphdr) + sizeof(NDhcp4Message), 1, 0),       /* packet >= DHCPPacket ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                /*
+                 * UDP
+                 *
+                 * Check
+                 *  - DHCP client port
+                 *
+                 * Leave X the size of IP and UDP headers, for future indirect reads.
+                 */
+                BPF_STMT(BPF_LD + BPF_H + BPF_IND, offsetof(struct udphdr, dest)),                              /* A <- UDP destination port */
+                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_NETWORK_CLIENT_PORT, 1, 0),                         /* UDP destination port == DHCP client port ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                BPF_STMT(BPF_LD + BPF_W + BPF_K, sizeof(struct udphdr)),                                        /* A <- size of UDP header */
+                BPF_STMT(BPF_ALU + BPF_ADD + BPF_X, 0),                                                         /* A += X */
+                BPF_STMT(BPF_MISC + BPF_TAX, 0),                                                                /* X <- A */
+
+                /*
+                 * DHCP
+                 *
+                 * Check
+                 *  - BOOTREPLY (from server to client)
+                 *  - DHCP magic cookie
+                 */
+                BPF_STMT(BPF_LD + BPF_B + BPF_IND, offsetof(NDhcp4Header, op)),                                 /* A <- DHCP op */
+                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_OP_BOOTREPLY, 1, 0),                                /* op == BOOTREPLY ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                BPF_STMT(BPF_LD + BPF_W + BPF_IND, offsetof(NDhcp4Message, magic)),                             /* A <- DHCP magic cookie */
+                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_MESSAGE_MAGIC, 1, 0),                               /* cookie == DHCP magic cookie ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                BPF_STMT(BPF_RET + BPF_K, 65535),                                                               /* return all */
+        };
+        struct sock_fprog fprog = {
+                .filter = filter,
+                .len = sizeof(filter) / sizeof(filter[0]),
+        };
+        struct sockaddr_ll addr = {
+                .sll_family = AF_PACKET,
+                .sll_protocol = htons(ETH_P_IP),
+                .sll_ifindex = ifindex,
+        };
+        int r, on = 1;
+
+        sockfd = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
+        if (sockfd < 0)
+                return -errno;
+
+        r = setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &fprog, sizeof(fprog));
+        if (r < 0)
+                return -errno;
+
+        /* We need the flag that tells us if the checksum is correct. */
+        r = setsockopt(sockfd, SOL_PACKET, PACKET_AUXDATA, &on, sizeof(on));
+        if (r < 0)
+                return -errno;
+
+        r = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
+        if (r < 0)
+                return -errno;
+
+        *sockfdp = sockfd;
+        sockfd = -1;
+        return 0;
+}
+
+/**
+ * n_dhcp4_c_socket_udp_new() - create a new DHCP4 client UDP socket
+ * @sockfdp:            return argumnet for the new socket
+ * @ifindex:            interface index to bind to
+ * @client_addr:        client address to bind to
+ * @server_addr:        server address to connect to
+ *
+ * Create a new AF_INET/SOCK_DGRAM socket usable to listen to and send DHCP client
+ * packets.
+ *
+ * The client address given in @addr must be configured on the interface @ifindex
+ * before the socket is created.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+int n_dhcp4_c_socket_udp_new(int *sockfdp,
+                             int ifindex,
+                             const struct in_addr *client_addr,
+                             const struct in_addr *server_addr) {
+        _c_cleanup_(c_closep) int sockfd = -1;
+        struct sock_filter filter[] = {
+                /*
+                 * IP/UDP
+                 *
+                 * Set X to the size of IP and UDP headers, for future indirect reads.
+                 */
+                BPF_STMT(BPF_LDX + BPF_B + BPF_MSH, 0),                                                         /* X <- IP header length */
+                BPF_STMT(BPF_LD + BPF_W + BPF_K, sizeof(struct udphdr)),                                        /* A <- size of UDP header */
+                BPF_STMT(BPF_ALU + BPF_ADD + BPF_X, 0),                                                         /* A += X */
+                BPF_STMT(BPF_MISC + BPF_TAX, 0),                                                                /* X <- A */
+
+                /*
+                 * DHCP
+                 *
+                 * Check
+                 *  - BOOTREPLY (from server to client)
+                 *  - DHCP magic cookie
+                 */
+                BPF_STMT(BPF_LD + BPF_B + BPF_IND, offsetof(NDhcp4Header, op)),                                 /* A <- DHCP op */
+                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_OP_BOOTREPLY, 1, 0),                                /* op == BOOTREPLY ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                BPF_STMT(BPF_LD + BPF_W + BPF_IND, offsetof(NDhcp4Message, magic)),                             /* A <- DHCP magic cookie */
+                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_MESSAGE_MAGIC, 1, 0),                               /* cookie == DHCP magic cookie ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                BPF_STMT(BPF_RET + BPF_K, 65535),                                                               /* return all */
+        };
+        struct sock_fprog fprog = {
+                .filter = filter,
+                .len = sizeof(filter) / sizeof(filter[0]),
+        };
+        struct sockaddr_in saddr = {
+                .sin_family = AF_INET,
+                .sin_addr = *client_addr,
+                .sin_port = htons(N_DHCP4_NETWORK_CLIENT_PORT),
+        };
+        struct sockaddr_in daddr = {
+                .sin_family = AF_INET,
+                .sin_addr = *server_addr,
+                .sin_port = htons(N_DHCP4_NETWORK_SERVER_PORT),
+        };
+        int r, tos = IPTOS_CLASS_CS6, on = 1;
+
+        sockfd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
+        if (sockfd < 0)
+                return -errno;
+
+        r = setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &fprog, sizeof(fprog));
+        if (r < 0)
+                return -errno;
+
+        r = socket_bind_if(sockfd, ifindex);
+        if (r)
+                return r;
+
+        r = setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
+        if (r < 0)
+                return -errno;
+
+        r = setsockopt(sockfd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos));
+        if (r < 0)
+                return -errno;
+
+        r = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
+        if (r < 0)
+                return -errno;
+
+        r = connect(sockfd, (struct sockaddr*)&daddr, sizeof(daddr));
+        if (r < 0)
+                return -errno;
+
+        *sockfdp = sockfd;
+        sockfd = -1;
+        return 0;
+}
+
+/**
+ * n_dhcp4_s_socket_packet_new() - create a new DHCP4 server packet socket
+ * @sockfdp:            return argumnet for the new socket
+ *
+ * Create a new AF_PACKET/SOCK_DGRAM socket usable to send DHCP packets to clients
+ * before they have an IP address configured, on the given interface.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+int n_dhcp4_s_socket_packet_new(int *sockfdp) {
+        _c_cleanup_(c_closep) int sockfd = -1;
+
+        sockfd = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
+        if (sockfd < 0)
+                return -errno;
+
+        *sockfdp = sockfd;
+        sockfd = -1;
+        return 0;
+}
+
+/**
+ * n_dhcp4_s_socket_udp_new() - create a new DHCP4 server UDP socket
+ * @sockfdp:            return argumnet for the new socket
+ * @ifindex:            intercafe index to bind to
+ *
+ * Create a new AF_INET/SOCK_DGRAM socket usable to listen to DHCP server packets,
+ * on the given interface.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+int n_dhcp4_s_socket_udp_new(int *sockfdp, int ifindex) {
+        _c_cleanup_(c_closep) int sockfd = -1;
+        struct sock_filter filter[] = {
+                /*
+                 * IP/UDP
+                 *
+                 * Set X to the size of IP and UDP headers, for future indirect reads.
+                 */
+                BPF_STMT(BPF_LDX + BPF_B + BPF_MSH, 0),                                                         /* X <- IP header length */
+                BPF_STMT(BPF_LD + BPF_W + BPF_K, sizeof(struct udphdr)),                                        /* A <- size of UDP header */
+                BPF_STMT(BPF_ALU + BPF_ADD + BPF_X, 0),                                                         /* A += X */
+                BPF_STMT(BPF_MISC + BPF_TAX, 0),                                                                /* X <- A */
+
+                /*
+                 * DHCP
+                 *
+                 * Check
+                 *  - BOOTREQUEST (from client to server)
+                 *  - DHCP magic cookie
+                 */
+
+                BPF_STMT(BPF_LD + BPF_B + BPF_IND, offsetof(NDhcp4Header, op)),                                 /* A <- DHCP op */
+                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_OP_BOOTREQUEST, 1, 0),                              /* op == BOOTREQUEST ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                BPF_STMT(BPF_LD + BPF_W + BPF_IND, offsetof(NDhcp4Message, magic)),                             /* A <- DHCP magic cookie */
+                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_MESSAGE_MAGIC, 1, 0),                               /* cookie == DHCP magic cookie ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                BPF_STMT(BPF_RET + BPF_K, 65535),                                                               /* return all */
+        };
+        struct sock_fprog fprog = {
+                .filter = filter,
+                .len = sizeof(filter) / sizeof(filter[0]),
+        };
+        struct sockaddr_in addr = {
+                .sin_family = AF_INET,
+                .sin_addr = { INADDR_ANY },
+                .sin_port = htons(N_DHCP4_NETWORK_SERVER_PORT),
+        };
+        int r, tos = IPTOS_CLASS_CS6, on = 1;
+
+        sockfd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
+        if (sockfd < 0)
+                return -errno;
+
+        r = setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &fprog, sizeof(fprog));
+        if (r < 0)
+                return -errno;
+
+        r = socket_bind_if(sockfd, ifindex);
+        if (r)
+                return r;
+
+        r = setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
+        if (r < 0)
+                return -errno;
+
+        r = setsockopt(sockfd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos));
+        if (r < 0)
+                return -errno;
+
+        r = setsockopt(sockfd, IPPROTO_IP, IP_PKTINFO, &on, sizeof(on));
+        if (r < 0)
+                return -errno;
+
+        r = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
+        if (r < 0)
+                return -errno;
+
+        *sockfdp = sockfd;
+        sockfd = -1;
+        return 0;
+}
+
+static int n_dhcp4_socket_packet_send(int sockfd,
+                                      int ifindex,
+                                      const struct sockaddr_in *src_paddr,
+                                      const unsigned char *dest_haddr,
+                                      unsigned char halen,
+                                      const struct sockaddr_in *dest_paddr,
+                                      NDhcp4Outgoing *message) {
+        struct packet_sockaddr_ll haddr = {
+                .sll_family = AF_PACKET,
+                .sll_protocol = htons(ETH_P_IP),
+                .sll_ifindex = ifindex,
+                .sll_halen = halen,
+        };
+        const void *buf;
+        size_t n_buf, len;
+        int r;
+
+        c_assert(halen <= sizeof(haddr.sll_addr));
+
+        memcpy(haddr.sll_addr, dest_haddr, halen);
+
+        n_buf = n_dhcp4_outgoing_get_raw(message, &buf);
+
+        r = packet_sendto_udp(sockfd, buf, n_buf, &len, src_paddr, &haddr, dest_paddr);
+        if (r < 0) {
+                if (r == -EAGAIN || r == -ENOBUFS)
+                        return N_DHCP4_E_DROPPED;
+                else if (r == -ENETDOWN || r == -ENXIO)
+                        return N_DHCP4_E_DOWN;
+                else
+                        return r;
+        } else if (len != n_buf) {
+                return N_DHCP4_E_DROPPED;
+        }
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_c_socket_packet_send() - XXX
+ */
+int n_dhcp4_c_socket_packet_send(int sockfd,
+                                 int ifindex,
+                                 const unsigned char *dest_haddr,
+                                 unsigned char halen,
+                                 NDhcp4Outgoing *message) {
+        struct sockaddr_in src_paddr = {
+                .sin_family = AF_INET,
+                .sin_port = htons(N_DHCP4_NETWORK_CLIENT_PORT),
+                .sin_addr = { INADDR_ANY },
+        };
+        struct sockaddr_in dest_paddr = {
+                .sin_family = AF_INET,
+                .sin_port = htons(N_DHCP4_NETWORK_SERVER_PORT),
+                .sin_addr = { INADDR_BROADCAST }
+        };
+
+        return n_dhcp4_socket_packet_send(sockfd,
+                                          ifindex,
+                                          &src_paddr,
+                                          dest_haddr,
+                                          halen,
+                                          &dest_paddr,
+                                          message);
+}
+
+/**
+ * n_dhcp4_c_socket_udp_send() - XXX
+ */
+int n_dhcp4_c_socket_udp_send(int sockfd,
+                              NDhcp4Outgoing *message) {
+        const void *buf;
+        size_t n_buf;
+        ssize_t len;
+
+        n_buf = n_dhcp4_outgoing_get_raw(message, &buf);
+
+        len = send(sockfd, buf, n_buf, 0);
+        if (len < 0) {
+                if (errno == EAGAIN || errno == ENOBUFS)
+                        return N_DHCP4_E_DROPPED;
+                else if (errno == ENETDOWN || errno == ENXIO)
+                        return N_DHCP4_E_DOWN;
+                else
+                        return -errno;
+        } else if ((size_t)len != n_buf)
+                return N_DHCP4_E_DROPPED;
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_c_socket_udp_broadcast() - XXX
+ */
+int n_dhcp4_c_socket_udp_broadcast(int sockfd, NDhcp4Outgoing *message) {
+        struct sockaddr_in sockaddr_dest = {
+                .sin_family = AF_INET,
+                .sin_port = htons(N_DHCP4_NETWORK_SERVER_PORT),
+                .sin_addr = { INADDR_BROADCAST },
+        };
+        const void *buf;
+        size_t n_buf;
+        ssize_t len;
+
+        n_buf = n_dhcp4_outgoing_get_raw(message, &buf);
+
+        len = sendto(sockfd,
+                     buf,
+                     n_buf,
+                     0,
+                     (struct sockaddr*)&sockaddr_dest,
+                     sizeof(sockaddr_dest));
+        if (len < 0) {
+                if (errno == EAGAIN || errno == ENOBUFS)
+                        return N_DHCP4_E_DROPPED;
+                else if (errno == ENETDOWN || errno == ENXIO)
+                        return N_DHCP4_E_DOWN;
+                else
+                        return -errno;
+        } else if ((size_t)len != n_buf)
+                return N_DHCP4_E_DROPPED;
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_s_socket_packet_send() - XXX
+ */
+int n_dhcp4_s_socket_packet_send(int sockfd,
+                                 int ifindex,
+                                 const struct in_addr *src_inaddr,
+                                 const unsigned char *dest_haddr,
+                                 unsigned char halen,
+                                 const struct in_addr *dest_inaddr,
+                                 NDhcp4Outgoing *message) {
+        struct sockaddr_in src_paddr = {
+                .sin_family = AF_INET,
+                .sin_port = htons(N_DHCP4_NETWORK_SERVER_PORT),
+                .sin_addr = *src_inaddr,
+        };
+        struct sockaddr_in dest_paddr = {
+                .sin_family = AF_INET,
+                .sin_port = htons(N_DHCP4_NETWORK_CLIENT_PORT),
+                .sin_addr = *dest_inaddr,
+        };
+
+        return n_dhcp4_socket_packet_send(sockfd,
+                                          ifindex,
+                                          &src_paddr,
+                                          dest_haddr,
+                                          halen,
+                                          &dest_paddr,
+                                          message);
+}
+
+/**
+ * n_dhcp4_s_socket_udp_send() - XXX
+ */
+int n_dhcp4_s_socket_udp_send(int sockfd,
+                              const struct in_addr *inaddr_src,
+                              const struct in_addr *inaddr_dest,
+                              NDhcp4Outgoing *message) {
+        struct sockaddr_in sockaddr_dest = {
+                .sin_family = AF_INET,
+                .sin_port = htons(N_DHCP4_NETWORK_CLIENT_PORT),
+                .sin_addr = *inaddr_dest,
+        };
+        struct iovec iov = {};
+        union {
+               struct cmsghdr align; /* ensure correct stack alignment */
+               char buf[CMSG_SPACE(sizeof(struct in_pktinfo))];
+        } control = {};
+        struct in_pktinfo pktinfo = {
+                .ipi_spec_dst = *inaddr_src,
+        };
+        struct msghdr msg = {
+                .msg_name = (void*)&sockaddr_dest,
+                .msg_namelen = sizeof(sockaddr_dest),
+                .msg_iov = &iov,
+                .msg_iovlen = 1,
+                .msg_control = &control.buf,
+                .msg_controllen = sizeof(control.buf),
+        };
+        struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+        ssize_t len;
+
+        cmsg->cmsg_level = IPPROTO_IP;
+        cmsg->cmsg_type = IP_PKTINFO;
+        cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo));
+        memcpy(CMSG_DATA(cmsg), &pktinfo, sizeof(pktinfo));
+
+        iov.iov_len = n_dhcp4_outgoing_get_raw(message, (const void **)&iov.iov_base);
+
+        len = sendmsg(sockfd, &msg, 0);
+        if (len < 0) {
+                if (errno == EAGAIN || errno == ENOBUFS)
+                        return N_DHCP4_E_DROPPED;
+                else if (errno == ENETDOWN || errno == ENXIO)
+                        return N_DHCP4_E_DOWN;
+                else
+                        return -errno;
+        } else if ((size_t)len != iov.iov_len)
+                return N_DHCP4_E_DROPPED;
+
+        return 0;
+}
+
+int n_dhcp4_s_socket_udp_broadcast(int sockfd,
+                                   const struct in_addr *inaddr_src,
+                                   NDhcp4Outgoing *message) {
+        return n_dhcp4_s_socket_udp_send(sockfd,
+                                         inaddr_src,
+                                         &(const struct in_addr){INADDR_BROADCAST},
+                                         message);
+}
+
+int n_dhcp4_c_socket_packet_recv(int sockfd,
+                                 uint8_t *buf,
+                                 size_t n_buf,
+                                 NDhcp4Incoming **messagep) {
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL;
+        size_t len;
+        int r;
+
+        r = packet_recv_udp(sockfd, buf, n_buf, &len);
+        if (r < 0) {
+                if (r == -ENETDOWN)
+                        return N_DHCP4_E_DOWN;
+                else if (r == -EAGAIN)
+                        return N_DHCP4_E_AGAIN;
+                else
+                        return -errno;
+        } else if (len == 0) {
+                return N_DHCP4_E_MALFORMED;
+        }
+
+        r = n_dhcp4_incoming_new(&message, buf, len);
+        if (r)
+                return r;
+
+        *messagep = message;
+        message = NULL;
+        return 0;
+}
+
+static int n_dhcp4_socket_udp_recv(int sockfd,
+                                   uint8_t *buf,
+                                   size_t n_buf,
+                                   NDhcp4Incoming **messagep,
+                                   struct in_pktinfo *pktinfo) {
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL;
+        struct iovec iov = {
+                .iov_base = buf,
+                .iov_len = n_buf,
+        };
+        uint8_t cmsgbuf[CMSG_LEN(sizeof(struct in_pktinfo))];
+        struct msghdr msg = {
+                .msg_iov = &iov,
+                .msg_iovlen = 1,
+                .msg_control = cmsgbuf,
+                .msg_controllen = sizeof(cmsgbuf),
+        };
+        ssize_t len;
+        int r;
+
+        len = recvmsg(sockfd, &msg, MSG_TRUNC);
+        if (len < 0) {
+                if (errno == ENETDOWN)
+                        return N_DHCP4_E_DOWN;
+                else if (errno == EAGAIN)
+                        return N_DHCP4_E_AGAIN;
+                else
+                        return -errno;
+        } else if (len == 0 || (size_t)len > n_buf) {
+                return N_DHCP4_E_MALFORMED;
+        }
+
+        r = n_dhcp4_incoming_new(&message, buf, len);
+        if (r)
+                return r;
+
+        if (pktinfo) {
+                struct cmsghdr *cmsg;
+
+                cmsg = CMSG_FIRSTHDR(&msg);
+                c_assert(cmsg);
+                c_assert(cmsg->cmsg_level == IPPROTO_IP);
+                c_assert(cmsg->cmsg_type == IP_PKTINFO);
+                c_assert(cmsg->cmsg_len == CMSG_LEN(sizeof(struct in_pktinfo)));
+
+                memcpy(pktinfo, (void*)CMSG_DATA(cmsg), sizeof(struct in_pktinfo));
+        }
+
+        *messagep = message;
+        message = NULL;
+        return 0;
+}
+
+int n_dhcp4_c_socket_udp_recv(int sockfd,
+                              uint8_t *buf,
+                              size_t n_buf,
+                              NDhcp4Incoming **messagep) {
+        return n_dhcp4_socket_udp_recv(sockfd, buf, n_buf, messagep, NULL);
+}
+
+int n_dhcp4_s_socket_udp_recv(int sockfd,
+                              uint8_t *buf,
+                              size_t n_buf,
+                              NDhcp4Incoming **messagep,
+                              struct sockaddr_in *dest) {
+        struct in_pktinfo pktinfo = {};
+        int r;
+
+        r = n_dhcp4_socket_udp_recv(sockfd, buf, n_buf, messagep, &pktinfo);
+        if (r)
+                return r;
+
+        if (dest) {
+                dest->sin_family = AF_INET;
+                dest->sin_port = htons(N_DHCP4_NETWORK_SERVER_PORT);
+                dest->sin_addr.s_addr = pktinfo.ipi_addr.s_addr;
+        }
+
+        return 0;
+}
diff --git a/shared/n-dhcp4/src/n-dhcp4.h b/shared/n-dhcp4/src/n-dhcp4.h
new file mode 100644
index 00000000..58a3cb80
--- /dev/null
+++ b/shared/n-dhcp4/src/n-dhcp4.h
@@ -0,0 +1,286 @@
+#pragma once
+
+/*
+ * Dynamic Host Configuration Protocol for IPv4
+ *
+ * This is the public header of the n-dhcp4 library, implementing IPv4 Dynamic
+ * Host Configuration Protocol as described in RFC-2132. This header defines
+ * the public API and all entry points of n-dhcp4.
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <inttypes.h>
+#include <netinet/in.h>
+#include <stdbool.h>
+#include <stdlib.h>
+
+typedef struct NDhcp4Client NDhcp4Client;
+typedef struct NDhcp4ClientConfig NDhcp4ClientConfig;
+typedef struct NDhcp4ClientEvent NDhcp4ClientEvent;
+typedef struct NDhcp4ClientLease NDhcp4ClientLease;
+typedef struct NDhcp4ClientProbe NDhcp4ClientProbe;
+typedef struct NDhcp4ClientProbeConfig NDhcp4ClientProbeConfig;
+typedef struct NDhcp4Server NDhcp4Server;
+typedef struct NDhcp4ServerConfig NDhcp4ServerConfig;
+typedef struct NDhcp4ServerEvent NDhcp4ServerEvent;
+typedef struct NDhcp4ServerIp NDhcp4ServerIp;
+typedef struct NDhcp4ServerLease NDhcp4ServerLease;
+
+#define N_DHCP4_CLIENT_START_DELAY_RFC2131 (UINT64_C(9000))
+
+enum {
+        _N_DHCP4_E_SUCCESS,
+
+        N_DHCP4_E_PREEMPTED,
+        N_DHCP4_E_INTERNAL,
+
+        N_DHCP4_E_INVALID_IFINDEX,
+        N_DHCP4_E_INVALID_TRANSPORT,
+        N_DHCP4_E_INVALID_ADDRESS,
+        N_DHCP4_E_INVALID_CLIENT_ID,
+        N_DHCP4_E_DUPLICATE_OPTION,
+        N_DHCP4_E_UNSET,
+
+        _N_DHCP4_E_N,
+};
+
+enum {
+        N_DHCP4_TRANSPORT_ETHERNET,
+        N_DHCP4_TRANSPORT_INFINIBAND,
+        _N_DHCP4_TRANSPORT_N,
+};
+
+enum {
+        N_DHCP4_CLIENT_EVENT_DOWN,
+        N_DHCP4_CLIENT_EVENT_OFFER,
+        N_DHCP4_CLIENT_EVENT_GRANTED,
+        N_DHCP4_CLIENT_EVENT_RETRACTED,
+        N_DHCP4_CLIENT_EVENT_EXTENDED,
+        N_DHCP4_CLIENT_EVENT_EXPIRED,
+        N_DHCP4_CLIENT_EVENT_CANCELLED,
+        _N_DHCP4_CLIENT_EVENT_N,
+};
+
+enum {
+        N_DHCP4_SERVER_EVENT_DOWN,
+        N_DHCP4_SERVER_EVENT_DISCOVER,
+        N_DHCP4_SERVER_EVENT_REQUEST,
+        N_DHCP4_SERVER_EVENT_RENEW,
+        N_DHCP4_SERVER_EVENT_DECLINE,
+        N_DHCP4_SERVER_EVENT_RELEASE,
+        _N_DHCP4_SERVER_EVENT_N,
+};
+
+struct NDhcp4ClientEvent {
+        unsigned int event;
+        union {
+                struct {
+                } down;
+                struct {
+                        NDhcp4ClientProbe *probe;
+                        NDhcp4ClientLease *lease;
+                } offer, granted, extended;
+                struct {
+                        NDhcp4ClientProbe *probe;
+                } retracted, expired, cancelled;
+        };
+};
+
+struct NDhcp4ServerEvent {
+        unsigned int event;
+        union {
+                struct {
+                } down;
+                struct {
+                        NDhcp4ServerLease *lease;
+                } discover, request, decline, release;
+        };
+};
+
+/* client configs */
+
+int n_dhcp4_client_config_new(NDhcp4ClientConfig **configp);
+NDhcp4ClientConfig *n_dhcp4_client_config_free(NDhcp4ClientConfig *config);
+
+void n_dhcp4_client_config_set_ifindex(NDhcp4ClientConfig *config, int ifindex);
+void n_dhcp4_client_config_set_transport(NDhcp4ClientConfig *config, unsigned int transport);
+void n_dhcp4_client_config_set_request_broadcast(NDhcp4ClientConfig *config, bool request_broadcast);
+void n_dhcp4_client_config_set_mac(NDhcp4ClientConfig *config, const uint8_t *mac, size_t n_mac);
+void n_dhcp4_client_config_set_broadcast_mac(NDhcp4ClientConfig *config, const uint8_t *mac, size_t n_mac);
+int n_dhcp4_client_config_set_client_id(NDhcp4ClientConfig *config, const uint8_t *id, size_t n_id);
+
+/* client-probe configs */
+
+int n_dhcp4_client_probe_config_new(NDhcp4ClientProbeConfig **configp);
+NDhcp4ClientProbeConfig *n_dhcp4_client_probe_config_free(NDhcp4ClientProbeConfig *config);
+
+void n_dhcp4_client_probe_config_set_inform_only(NDhcp4ClientProbeConfig *config, bool inform_only);
+void n_dhcp4_client_probe_config_set_init_reboot(NDhcp4ClientProbeConfig *config, bool init_reboot);
+void n_dhcp4_client_probe_config_set_requested_ip(NDhcp4ClientProbeConfig *config, struct in_addr ip);
+void n_dhcp4_client_probe_config_set_start_delay(NDhcp4ClientProbeConfig *config, uint64_t msecs);
+void n_dhcp4_client_probe_config_request_option(NDhcp4ClientProbeConfig *config, uint8_t option);
+int n_dhcp4_client_probe_config_append_option(NDhcp4ClientProbeConfig *config,
+                                              uint8_t option,
+                                              const void *data,
+                                              uint8_t n_data);
+
+/* clients */
+
+int n_dhcp4_client_new(NDhcp4Client **clientp, NDhcp4ClientConfig *config);
+NDhcp4Client *n_dhcp4_client_ref(NDhcp4Client *client);
+NDhcp4Client *n_dhcp4_client_unref(NDhcp4Client *client);
+
+void n_dhcp4_client_get_fd(NDhcp4Client *client, int *fdp);
+int n_dhcp4_client_dispatch(NDhcp4Client *client);
+int n_dhcp4_client_pop_event(NDhcp4Client *client, NDhcp4ClientEvent **eventp);
+
+int n_dhcp4_client_update_mtu(NDhcp4Client *client, uint16_t mtu);
+
+int n_dhcp4_client_probe(NDhcp4Client *client,
+                         NDhcp4ClientProbe **probep,
+                         NDhcp4ClientProbeConfig *config);
+
+/* client probes */
+
+NDhcp4ClientProbe *n_dhcp4_client_probe_free(NDhcp4ClientProbe *probe);
+
+void n_dhcp4_client_probe_set_userdata(NDhcp4ClientProbe *probe, void *userdata);
+void n_dhcp4_client_probe_get_userdata(NDhcp4ClientProbe *probe, void **userdatap);
+
+/* client leases */
+
+NDhcp4ClientLease *n_dhcp4_client_lease_ref(NDhcp4ClientLease *lease);
+NDhcp4ClientLease *n_dhcp4_client_lease_unref(NDhcp4ClientLease *lease);
+
+void n_dhcp4_client_lease_get_yiaddr(NDhcp4ClientLease *lease, struct in_addr *yiaddr);
+void n_dhcp4_client_lease_get_lifetime(NDhcp4ClientLease *lease, uint64_t *ns_lifetimep);
+int n_dhcp4_client_lease_query(NDhcp4ClientLease *lease, uint8_t option, uint8_t **datap, size_t *n_datap);
+
+int n_dhcp4_client_lease_select(NDhcp4ClientLease *lease);
+int n_dhcp4_client_lease_accept(NDhcp4ClientLease *lease);
+int n_dhcp4_client_lease_decline(NDhcp4ClientLease *lease, const char *error);
+
+/* server configs */
+
+int n_dhcp4_server_config_new(NDhcp4ServerConfig **configp);
+NDhcp4ServerConfig *n_dhcp4_server_config_free(NDhcp4ServerConfig *config);
+
+void n_dhcp4_server_config_set_ifindex(NDhcp4ServerConfig *config, int ifindex);
+
+/* servers */
+
+int n_dhcp4_server_new(NDhcp4Server **serverp, NDhcp4ServerConfig *config);
+NDhcp4Server *n_dhcp4_server_ref(NDhcp4Server *server);
+NDhcp4Server *n_dhcp4_server_unref(NDhcp4Server *server);
+
+void n_dhcp4_server_get_fd(NDhcp4Server *server, int *fdp);
+int n_dhcp4_server_dispatch(NDhcp4Server *server);
+int n_dhcp4_server_pop_event(NDhcp4Server *server, NDhcp4ServerEvent **eventp);
+
+int n_dhcp4_server_add_ip(NDhcp4Server *server, NDhcp4ServerIp **ipp, struct in_addr ip);
+
+/* server ip addresses */
+
+NDhcp4ServerIp *n_dhcp4_server_ip_free(NDhcp4ServerIp *ip);
+
+/* server leases */
+
+NDhcp4ServerLease *n_dhcp4_server_lease_ref(NDhcp4ServerLease *lease);
+NDhcp4ServerLease *n_dhcp4_server_lease_unref(NDhcp4ServerLease *lease);
+
+int n_dhcp4_server_lease_query(NDhcp4ServerLease *lease, uint8_t option, uint8_t **datap, size_t *n_datap);
+int n_dhcp4_server_lease_append(NDhcp4ServerLease *lease, uint8_t option, uint8_t *data, size_t n_data);
+
+int n_dhcp4_server_lease_offer(NDhcp4ServerLease *lease);
+int n_dhcp4_server_lease_ack(NDhcp4ServerLease *lease);
+int n_dhcp4_server_lease_nack(NDhcp4ServerLease *lease);
+
+/* inline helpers */
+
+static inline void n_dhcp4_client_config_freep(NDhcp4ClientConfig **p) {
+        if (*p)
+                n_dhcp4_client_config_free(*p);
+}
+
+static inline void n_dhcp4_client_config_freev(NDhcp4ClientConfig *p) {
+        n_dhcp4_client_config_free(p);
+}
+
+static inline void n_dhcp4_client_probe_config_freep(NDhcp4ClientProbeConfig **p) {
+        if (*p)
+                n_dhcp4_client_probe_config_free(*p);
+}
+
+static inline void n_dhcp4_client_probe_config_freev(NDhcp4ClientProbeConfig *p) {
+        n_dhcp4_client_probe_config_free(p);
+}
+
+static inline void n_dhcp4_client_unrefp(NDhcp4Client **p) {
+        if (*p)
+                n_dhcp4_client_unref(*p);
+}
+
+static inline void n_dhcp4_client_unrefv(NDhcp4Client *p) {
+        n_dhcp4_client_unref(p);
+}
+
+static inline void n_dhcp4_client_probe_freep(NDhcp4ClientProbe **p) {
+        if (*p)
+                n_dhcp4_client_probe_free(*p);
+}
+
+static inline void n_dhcp4_client_probe_freev(NDhcp4ClientProbe *p) {
+        n_dhcp4_client_probe_free(p);
+}
+
+static inline void n_dhcp4_client_lease_unrefp(NDhcp4ClientLease **p) {
+        if (*p)
+                n_dhcp4_client_lease_unref(*p);
+}
+
+static inline void n_dhcp4_client_lease_unrefv(NDhcp4ClientLease *p) {
+        n_dhcp4_client_lease_unref(p);
+}
+
+static inline void n_dhcp4_server_config_freep(NDhcp4ServerConfig **p) {
+        if (*p)
+                n_dhcp4_server_config_free(*p);
+}
+
+static inline void n_dhcp4_server_config_freev(NDhcp4ServerConfig *p) {
+        n_dhcp4_server_config_free(p);
+}
+
+static inline void n_dhcp4_server_unrefp(NDhcp4Server **p) {
+        if (*p)
+                n_dhcp4_server_unref(*p);
+}
+
+static inline void n_dhcp4_server_unrefv(NDhcp4Server *p) {
+        n_dhcp4_server_unref(p);
+}
+
+static inline void n_dhcp4_server_ip_freep(NDhcp4ServerIp **p) {
+        if (*p)
+                n_dhcp4_server_ip_free(*p);
+}
+
+static inline void n_dhcp4_server_ip_freev(NDhcp4ServerIp *p) {
+        n_dhcp4_server_ip_free(p);
+}
+
+static inline void n_dhcp4_server_lease_unrefp(NDhcp4ServerLease **p) {
+        if (*p)
+                n_dhcp4_server_lease_unref(*p);
+}
+
+static inline void n_dhcp4_server_lease_unrefv(NDhcp4ServerLease *p) {
+        n_dhcp4_server_lease_unref(p);
+}
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/shared/n-dhcp4/src/util/packet.c b/shared/n-dhcp4/src/util/packet.c
new file mode 100644
index 00000000..38cb399d
--- /dev/null
+++ b/shared/n-dhcp4/src/util/packet.c
@@ -0,0 +1,456 @@
+/*
+ * Packet Sockets
+ */
+
+#include <assert.h>
+#include <c-stdaux.h>
+#include <endian.h>
+#include <errno.h>
+#include <linux/filter.h>
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
+#include <linux/udp.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include "packet.h"
+
+/**
+ * packet_internet_checksum() - compute the internet checksum
+ * @data:               the data to checksum
+ * @size:               the length of @data in bytes
+ *
+ * Computes the internet checksum for a given blob according to RFC1071.
+ *
+ * The internet checksum is the one's complement of the one's complement sum of
+ * the 16-bit words of the data, padded with zero-bytes if the data does not
+ * end on a 16-bit boundary.
+ *
+ * Return: Checksum is returned.
+ */
+uint16_t packet_internet_checksum(const uint8_t *data, size_t size) {
+        uint64_t acc = 0;
+        uint32_t local;
+
+        while (size >= sizeof(local)) {
+                memcpy(&local, data, sizeof(local));
+                acc += local;
+
+                data += sizeof(local);
+                size -= sizeof(local);
+        }
+
+        if (size) {
+                local = 0;
+                memcpy(&local, data, size);
+                acc += local;
+        }
+
+        while (acc >> 16)
+                acc = (acc & 0xffff) + (acc >> 16);
+
+        return ~acc;
+}
+
+/**
+ * packet_internet_checksum_udp() - compute the internet checkum for UDP packets
+ * @src_addr:           source IP address
+ * @dst_addr:           destination IP address
+ * @src_port:           source port
+ * @dst_port:           destination port
+ * @data:               payload
+ * @size:               length of payload in bytes
+ * @checksum:           current checksum, or 0
+ *
+ * Computes the internet checksum for a UDP packet, given the relevant IP and
+ * UDP header fields.
+ *
+ * Note that since a UDP packet contains the checksum itself, the resulting
+ * checksum will always be 0 (this fact is used to verify that a UDP packet is
+ * valid).
+ * Inversely, when calculating the checksum for outgoing packets, you have to
+ * specify 0 as @checksum, and this function will return the checksum for the
+ * caller to use for the packet. In this case, though, the caller must check
+ * whether the returned checksum might coincidentally be 0, in which case it
+ * must be flipped to -1 (0xffff), since 0 is not allowed as checksum in UDP
+ * packets, and -1 is arithmetically equivalent in the checksum calculation.
+ *
+ * Return: Checksum is returned.
+ */
+uint16_t packet_internet_checksum_udp(const struct in_addr *src_addr,
+                                      const struct in_addr *dst_addr,
+                                      uint16_t src_port,
+                                      uint16_t dst_port,
+                                      const uint8_t *data,
+                                      size_t size,
+                                      uint16_t checksum) {
+        struct {
+                uint32_t src;
+                uint32_t dst;
+                uint8_t _zeros;
+                uint8_t protocol;
+                uint16_t length;
+                struct udphdr udp;
+        } _c_packed_ udp_phdr = {
+                .src = src_addr->s_addr,
+                .dst = dst_addr->s_addr,
+                .protocol = IPPROTO_UDP,
+                .length = htons(sizeof(struct udphdr) + size),
+                .udp = {
+                        .source = htons(src_port),
+                        .dest = htons(dst_port),
+                        .len = htons(sizeof(struct udphdr) + size),
+                        .check = checksum,
+                },
+        };
+        const uint8_t *iter;
+        uint64_t acc = 0;
+        uint32_t local;
+
+        _Static_assert(!(sizeof(udp_phdr) % sizeof(local)),
+                       "UDP header structure size is not a multiple of 4");
+
+        for (iter = (const uint8_t *)&udp_phdr;
+             iter < (const uint8_t *)(&udp_phdr + 1);
+             iter += sizeof(local)) {
+                memcpy(&local, iter, sizeof(local));
+                acc += local;
+        }
+
+        while (size >= sizeof(local)) {
+                memcpy(&local, data, sizeof(local));
+                acc += local;
+
+                data += sizeof(local);
+                size -= sizeof(local);
+        }
+
+        if (size) {
+                local = 0;
+                memcpy(&local, data, size);
+                acc += local;
+        }
+
+        while (acc >> 16)
+                acc = (acc & 0xffff) + (acc >> 16);
+
+        return ~acc;
+}
+
+/**
+ * packet_sendto_udp() - send UDP packet on AF_PACKET socket
+ * @sockfd:             AF_PACKET/SOCK_DGRAM socket
+ * @buf:                payload
+ * @n_buf:              length of payload in bytes
+ * @n_transmittedp:     output argument for number of transmitted bytes
+ * @src_paddr:          source protocol address, see ip(7)
+ * @dest_haddr:         destination hardware address, see packet(7)
+ * @dest_paddr:         destination protocol address, see ip(7)
+ *
+ * Sends an UDP packet on a AF_PACKET socket directly to a hardware
+ * address. The difference between this and sendto() on an AF_INET
+ * socket is that no routing is performed, so the packet is delivered
+ * even if the destination IP is not yet configured on the destination
+ * host.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int packet_sendto_udp(int sockfd,
+                      const void *buf,
+                      size_t n_buf,
+                      size_t *n_transmittedp,
+                      const struct sockaddr_in *src_paddr,
+                      const struct packet_sockaddr_ll *dest_haddr,
+                      const struct sockaddr_in *dest_paddr) {
+        struct iphdr ip_hdr = {
+                .version = IPVERSION,
+                .ihl = sizeof(ip_hdr) / 4, /* Length of header in multiples of four bytes */
+                .tos = IPTOS_CLASS_CS6, /* Class Selector for network control */
+                .tot_len = htons(sizeof(struct iphdr) + sizeof(struct udphdr) + n_buf),
+                .frag_off = htons(IP_DF), /* Do not fragment */
+                .ttl = IPDEFTTL,
+                .protocol = IPPROTO_UDP,
+                .saddr = src_paddr->sin_addr.s_addr,
+                .daddr = dest_paddr->sin_addr.s_addr,
+        };
+        struct udphdr udp_hdr = {
+                .source = src_paddr->sin_port,
+                .dest = dest_paddr->sin_port,
+                .len = htons(sizeof(udp_hdr) + n_buf),
+        };
+        struct iovec iov[3] = {
+                {
+                        .iov_base = &ip_hdr,
+                        .iov_len = sizeof(ip_hdr),
+                },
+                {
+                        .iov_base = &udp_hdr,
+                        .iov_len = sizeof(udp_hdr),
+                },
+                {
+                        .iov_base = (void *)buf,
+                        .iov_len = n_buf,
+                },
+        };
+        struct msghdr msg = {
+                .msg_name = (void*)dest_haddr,
+                .msg_namelen = sizeof(*dest_haddr),
+                .msg_iov = iov,
+                .msg_iovlen = sizeof(iov) / sizeof(iov[0]),
+        };
+        ssize_t pktlen;
+
+        ip_hdr.check = packet_internet_checksum((void*)&ip_hdr, sizeof(ip_hdr));
+        udp_hdr.check = packet_internet_checksum_udp(&src_paddr->sin_addr,
+                                                     &dest_paddr->sin_addr,
+                                                     ntohs(src_paddr->sin_port),
+                                                     ntohs(dest_paddr->sin_port),
+                                                     buf,
+                                                     n_buf,
+                                                     0);
+
+        /*
+         * 0x0000 and 0xffff are equivalent for computing the UDP checksum,
+         * but 0x0000 is reserved in UDP headers, to mean that the checksum is
+         * not set and should be ignored by the receiver. Hence, flip it to
+         * 0xffff in that case.
+         */
+        udp_hdr.check = udp_hdr.check ?: 0xffff;
+
+        pktlen = sendmsg(sockfd, &msg, 0);
+        if (pktlen < 0)
+                return -errno;
+
+        /*
+         * Kernel never truncates. Worst case, we get -EMSGSIZE. Kernel *might*
+         * prepend VNET headers, in which case a bigger length than sent is
+         * returned.
+         * Lets assert on this, and then return to the caller the proportion of
+         * its own buffer that we sent (which is always exactly the requested
+         * size).
+         */
+        c_assert((size_t)pktlen >= sizeof(ip_hdr) + sizeof(udp_hdr) + n_buf);
+        *n_transmittedp = n_buf;
+        return 0;
+}
+
+/**
+ * packet_recvfrom_upd() - receive UDP packet from AF_PACKET socket
+ * @sockfd:             AF_PACKET/SOCK_DGRAM socket
+ * @buf:                buffor for payload
+ * @n_buf:              max length of payload in bytes
+ * @n_transmittedp:     output argument for number transmitted bytes
+ * @src:                return argumnet for source address, or NULL, see ip(7)
+ *
+ * Receives an UDP packet on a AF_PACKET socket. The difference between
+ * this and recvfrom() on an AF_INET socket is that the packet will be
+ * received even if the destination IP address has not been configured
+ * on the interface.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int packet_recvfrom_udp(int sockfd,
+                        void *buf,
+                        size_t n_buf,
+                        size_t *n_transmittedp,
+                        struct sockaddr_in *src) {
+        union {
+                struct iphdr hdr;
+                /*
+                 * Maximum IP-header length is 15 * 4, since it is specified in
+                 * the `ihl` field, which is four bits and interpreted as
+                 * factor of 4. So maximum `ihl` value is `(2^4 - 1) * 4`.
+                 */
+                uint8_t data[15 * 4];
+        } ip_hdr;
+        struct udphdr udp_hdr;
+        struct iovec iov[3] = {
+                {
+                        .iov_base = &ip_hdr,
+                },
+                {
+                        .iov_base = &udp_hdr,
+                        .iov_len = sizeof(udp_hdr),
+                },
+                {
+                        .iov_base = buf,
+                        .iov_len = n_buf,
+                },
+        };
+        uint8_t cmsgbuf[CMSG_LEN(sizeof(struct tpacket_auxdata))];
+        struct msghdr msg = {
+                .msg_iov = iov,
+                .msg_iovlen = sizeof(iov) / sizeof(iov[0]),
+                .msg_control = cmsgbuf,
+                .msg_controllen = sizeof(cmsgbuf),
+        };
+        struct cmsghdr *cmsg;
+        bool checksum = true;
+        ssize_t pktlen;
+        size_t hdrlen;
+
+        /* Peek packet to obtain the real IP header length */
+        pktlen = recv(sockfd, &ip_hdr.hdr, sizeof(ip_hdr.hdr), MSG_PEEK);
+        if (pktlen < 0)
+                return -errno;
+
+        if ((size_t)pktlen < sizeof(ip_hdr.hdr)) {
+                /*
+                 * Received packet is smaller than the minimal IP header length,
+                 * discard it.
+                 */
+                recv(sockfd, NULL, 0, 0);
+                *n_transmittedp = 0;
+                return 0;
+        }
+
+        if (ip_hdr.hdr.version != IPVERSION) {
+                /*
+                 * This is not an IPv4 packet, discard it.
+                 */
+                recv(sockfd, NULL, 0, 0);
+                *n_transmittedp = 0;
+                return 0;
+        }
+
+        hdrlen = ip_hdr.hdr.ihl * 4;
+        if (hdrlen < sizeof(ip_hdr.hdr)) {
+                /*
+                 * The length given in the header is smaller than the minimum
+                 * header length, discard the packet.
+                 */
+                recv(sockfd, NULL, 0, 0);
+                *n_transmittedp = 0;
+                return 0;
+        }
+
+        /*
+         * Now that we know the ip-header length, we can prepare the iovec to
+         * read the entire packet into the correct buffers.
+         */
+        iov[0].iov_len = hdrlen;
+        pktlen = recvmsg(sockfd, &msg, 0);
+        if (pktlen < 0)
+                return -errno;
+
+        cmsg = CMSG_FIRSTHDR(&msg);
+        if (cmsg) {
+                if (cmsg->cmsg_level == SOL_PACKET &&
+                    cmsg->cmsg_type == PACKET_AUXDATA &&
+                    cmsg->cmsg_len == CMSG_LEN(sizeof(struct tpacket_auxdata))) {
+                        struct tpacket_auxdata *aux = (void *)CMSG_DATA(cmsg);
+                        checksum = !(aux->tp_status & TP_STATUS_CSUMNOTREADY);
+                }
+        }
+
+        if (ntohs(ip_hdr.hdr.tot_len) > pktlen) {
+                /*
+                 * The IP-packet is bigger than the chunk returned by the
+                 * kernel. So either the packet is corrupt, or our caller
+                 * provided too small a buffer. In both cases, we simply drop
+                 * the packet.
+                 */
+                *n_transmittedp = 0;
+                return 0;
+        }
+
+        /* Truncate trailing garbage. */
+        pktlen = ntohs(ip_hdr.hdr.tot_len);
+
+        if ((size_t)pktlen < hdrlen + sizeof(udp_hdr)) {
+                /*
+                 * The packet is too small to even contain an entire UDP
+                 * header, so discard it entirely.
+                 */
+                *n_transmittedp = 0;
+                return 0;
+        } else if ((size_t)pktlen < hdrlen + ntohs(udp_hdr.len)) {
+                /*
+                 * The UDP header specified a longer length than the returned
+                 * packet, so discard it entirely.
+                 */
+                *n_transmittedp = 0;
+                return 0;
+        }
+
+        /*
+         * Make @pktlen the length of the packet payload, without IP/UDP
+         * headers, since that is what the caller is interested in.
+         */
+        pktlen = ntohs(udp_hdr.len) - sizeof(struct udphdr);
+
+        /* IP */
+
+        if (ip_hdr.hdr.protocol != IPPROTO_UDP) {
+                *n_transmittedp = 0;
+                return 0; /* not a UDP packet, discard it */
+        } else if (ip_hdr.hdr.frag_off & htons(IP_MF | IP_OFFMASK)) {
+                *n_transmittedp = 0;
+                return 0; /* fragmented packet, discard it */
+        } else if (checksum && packet_internet_checksum(ip_hdr.data, hdrlen)) {
+                *n_transmittedp = 0;
+                return 0; /* invalid checksum, discard it */
+        }
+
+        /* UDP */
+
+        if (checksum && udp_hdr.check) {
+                /*
+                 * Computing the checksum of a packet that has the checksum set
+                 * must yield 0. If it does not yield 0, the packet is invalid,
+                 * in which case we discard it.
+                 */
+               if (packet_internet_checksum_udp(&(struct in_addr){ ip_hdr.hdr.saddr },
+                                                &(struct in_addr){ ip_hdr.hdr.daddr },
+                                                ntohs(udp_hdr.source),
+                                                ntohs(udp_hdr.dest),
+                                                buf,
+                                                pktlen,
+                                                udp_hdr.check)) {
+                        *n_transmittedp = 0;
+                        return 0;
+               }
+        }
+
+        if (src) {
+                src->sin_family = AF_INET;
+                src->sin_addr.s_addr = ip_hdr.hdr.saddr;
+                src->sin_port = udp_hdr.source;
+        }
+
+        /* Return length of UDP payload (i.e., data written to @buf). */
+        *n_transmittedp = pktlen;
+        return 0;
+}
+
+/**
+ * packet_shutdown() - shutdown socket for future receive operations
+ * @sockfd:     socket
+ *
+ * Partially emulates `shutdown(sockfd, SHUT_RD)`, in the sense that no
+ * further packets may be queued on the socket. All packets that are
+ * already queued will still be delivered, but once -EAGAIN is returned
+ * we are guaranteed never to be able to read more packets in the future.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+int packet_shutdown(int sockfd) {
+        struct sock_filter filter[] = {
+                BPF_STMT(BPF_RET + BPF_K, 0), /* discard all packets */
+        };
+        struct sock_fprog fprog = {
+                .filter = filter,
+                .len = sizeof(filter) / sizeof(filter[0]),
+        };
+        int r;
+
+        r = setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &fprog, sizeof(fprog));
+        if (r < 0)
+                return -errno;
+
+        return 0;
+}
diff --git a/shared/n-dhcp4/src/util/packet.h b/shared/n-dhcp4/src/util/packet.h
new file mode 100644
index 00000000..98dabf7f
--- /dev/null
+++ b/shared/n-dhcp4/src/util/packet.h
@@ -0,0 +1,61 @@
+#pragma once
+
+/*
+ * Packet Sockets
+ */
+
+#include <c-stdaux.h>
+#include <inttypes.h>
+#include <linux/if_packet.h>
+#include <netinet/in.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+/*
+ * `struct sockaddr_ll` is too small to fit the Infiniband hardware address.
+ * Introduce `struct packet_sockaddr_ll` which is the same as the original,
+ * except the `sl_addr` field is extended to fit all the supported hardware
+ * addresses.
+ */
+struct packet_sockaddr_ll {
+        unsigned short  sll_family;
+        __be16          sll_protocol;
+        int             sll_ifindex;
+        unsigned short  sll_hatype;
+        unsigned char   sll_pkttype;
+        unsigned char   sll_halen;
+        unsigned char   sll_addr[32]; /* MAX_ADDR_LEN */
+};
+
+uint16_t packet_internet_checksum(const uint8_t *data, size_t len);
+uint16_t packet_internet_checksum_udp(const struct in_addr *src_addr,
+                                      const struct in_addr *dst_addr,
+                                      uint16_t src_port,
+                                      uint16_t dst_port,
+                                      const uint8_t *data,
+                                      size_t size,
+                                      uint16_t checksum);
+
+int packet_sendto_udp(int sockfd,
+                      const void *buf,
+                      size_t n_buf,
+                      size_t *n_transmittedp,
+                      const struct sockaddr_in *src_paddr,
+                      const struct packet_sockaddr_ll *dest_haddr,
+                      const struct sockaddr_in *dest_paddr);
+int packet_recvfrom_udp(int sockfd,
+                        void *buf,
+                        size_t n_buf,
+                        size_t *n_transmittedp,
+                        struct sockaddr_in *src);
+
+int packet_shutdown(int sockfd);
+
+/* inline helpers */
+
+static inline int packet_recv_udp(int sockfd,
+                                  void *buf,
+                                  size_t n_buf,
+                                  size_t *n_transmittedp) {
+        return packet_recvfrom_udp(sockfd, buf, n_buf, n_transmittedp, NULL);
+}
diff --git a/shared/n-dhcp4/src/util/socket.c b/shared/n-dhcp4/src/util/socket.c
new file mode 100644
index 00000000..c25f76fd
--- /dev/null
+++ b/shared/n-dhcp4/src/util/socket.c
@@ -0,0 +1,121 @@
+/*
+ * Socket Utilities
+ */
+
+#include <assert.h>
+#include <c-stdaux.h>
+#include <errno.h>
+#include <net/if.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include "socket.h"
+
+/**
+ * socket_SIOCGIFNAME() - resolve an ifindex to an ifname
+ * @socket:                     socket to operate on
+ * @ifindex:                    index of network interface to resolve
+ * @ifname:                     buffer to store resolved name
+ *
+ * This uses the SIOCGIFNAME ioctl to resolve an ifindex to an ifname. The
+ * buffer provided in @ifnamep must be at least IFNAMSIZ bytes in size. The
+ * maximum ifname length is IFNAMSIZ-1, and this function always
+ * zero-terminates the result.
+ *
+ * This function is similar to if_indextoname(3) provided by glibc, but it
+ * allows to specify the target socket explicitly. This allows the caller to
+ * control the target network-namespace, rather than relying on the network
+ * namespace of the running process.
+ *
+ * Return: 0 on success, negative kernel error code on failure.
+ */
+int socket_SIOCGIFNAME(int socket, int ifindex, char (*ifnamep)[IFNAMSIZ]) {
+        struct ifreq req = { .ifr_ifindex = ifindex };
+        int r;
+
+        r = ioctl(socket, SIOCGIFNAME, &req);
+        if (r < 0)
+                return -errno;
+
+        /*
+         * The linux kernel guarantees that an interface name is always
+         * zero-terminated, and it always fully fits into IFNAMSIZ bytes,
+         * including the zero-terminator.
+         */
+        memcpy(ifnamep, req.ifr_name, IFNAMSIZ);
+        return 0;
+}
+
+/**
+ * socket_bind_if() - bind socket to a network interface
+ * @socket:                     socket to operate on
+ * @ifindex:                    index of network interface to bind to, or 0
+ *
+ * This binds the socket given via @socket to the network interface specified
+ * via @ifindex. It uses the underlying SO_BINDTODEVICE ioctl of the linux
+ * kernel. However, if available, if prefers the newer SO_BINDTOIFINDEX ioctl,
+ * which avoids resolving the interface name temporarily, and thus does not
+ * suffer from a race-condition.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int socket_bind_if(int socket, int ifindex) {
+        char ifname[IFNAMSIZ] = {};
+        int r;
+
+        c_assert(ifindex >= 0);
+
+        /*
+         * We first try the newer SO_BINDTOIFINDEX. If it is not available on
+         * the running kernel, we fall back to SO_BINDTODEVICE. This, however,
+         * requires us to first resolve the ifindex to an ifname. Note that
+         * this is racy, since the device name might theoretically change
+         * asynchronously.
+         *
+         * Using 0 as ifindex will remove the device-binding. For
+         * SO_BINDTOIFINDEX we simply pass-through the 0 to the kernel, which
+         * recognizes this correctly. For SO_BINDTODEVICE we pass the empty
+         * string, which the kernel recognizes as a request to remove the
+         * binding.
+         *
+         * The commit introducing SO_BINDTOIFINDEX first appeared in linux-5.1:
+         *
+         *     commit f5dd3d0c9638a9d9a02b5964c4ad636f06cf7e2c
+         *     Author: David Herrmann <dh.herrmann@gmail.com>
+         *     Date:   Tue Jan 15 14:42:14 2019 +0100
+         *
+         *         net: introduce SO_BINDTOIFINDEX sockopt
+         *
+         * In older kernels, setsockopt(2) is guaranteed to return ENOPROTOOPT
+         * for this ioctl.
+         */
+
+#ifdef SO_BINDTOIFINDEX
+        r = setsockopt(socket,
+                       SOL_SOCKET,
+                       SO_BINDTOIFINDEX,
+                       &ifindex,
+                       sizeof(ifindex));
+        if (r >= 0)
+                return 0;
+        else if (errno != ENOPROTOOPT)
+                return -errno;
+#endif /* SO_BINDTOIFINDEX */
+
+        if (ifindex > 0) {
+                r = socket_SIOCGIFNAME(socket, ifindex, &ifname);
+                if (r)
+                        return r;
+        }
+
+        r = setsockopt(socket,
+                       SOL_SOCKET,
+                       SO_BINDTODEVICE,
+                       ifname,
+                       strlen(ifname));
+        if (r < 0)
+                return -errno;
+
+        return 0;
+}
diff --git a/shared/n-dhcp4/src/util/socket.h b/shared/n-dhcp4/src/util/socket.h
new file mode 100644
index 00000000..b5ecf2c2
--- /dev/null
+++ b/shared/n-dhcp4/src/util/socket.h
@@ -0,0 +1,11 @@
+#pragma once
+
+/*
+ * Socket Utilities
+ */
+
+#include <c-stdaux.h>
+#include <stdlib.h>
+
+int socket_SIOCGIFNAME(int socket, int ifindex, char (*ifnamep)[IFNAMSIZ]);
+int socket_bind_if(int socket, int ifindex);