about summary refs log tree commit diff
path: root/src/n-dhcp4
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2024-05-05 00:07:30 +0200
committerMichael Biebl <biebl@debian.org>2024-05-05 00:07:30 +0200
commit34bb501be08aa2b313d88e67d6e0a7e0a3f9cfa6 (patch)
tree4e6220877828be4c6f261de09ec0cb2d80e32389 /src/n-dhcp4
parentbba2e4b4de668db525cbfdfc35292e5a0b51671a (diff)
New upstream version 1.47.90 upstream/1.47.90
Diffstat (limited to 'src/n-dhcp4')
-rw-r--r--src/n-dhcp4/.editorconfig11
-rw-r--r--src/n-dhcp4/.github/workflows/ci.yml50
-rw-r--r--src/n-dhcp4/.gitmodules9
-rw-r--r--src/n-dhcp4/AUTHORS37
-rw-r--r--src/n-dhcp4/NEWS.md20
-rw-r--r--src/n-dhcp4/README.md53
-rw-r--r--src/n-dhcp4/meson.build23
-rw-r--r--src/n-dhcp4/src/libndhcp4.sym71
-rw-r--r--src/n-dhcp4/src/meson.build90
-rw-r--r--src/n-dhcp4/src/n-dhcp4-c-connection.c4
-rw-r--r--src/n-dhcp4/src/n-dhcp4-c-probe.c27
-rw-r--r--src/n-dhcp4/src/n-dhcp4-s-connection.c412
-rw-r--r--src/n-dhcp4/src/n-dhcp4-s-lease.c103
-rw-r--r--src/n-dhcp4/src/n-dhcp4-server.c241
-rw-r--r--src/n-dhcp4/src/n-dhcp4.h1
-rw-r--r--src/n-dhcp4/src/test-api.c157
-rw-r--r--src/n-dhcp4/src/test-connection.c390
-rw-r--r--src/n-dhcp4/src/test-message.c159
-rw-r--r--src/n-dhcp4/src/test-run-client.c713
-rw-r--r--src/n-dhcp4/src/test-socket.c317
-rw-r--r--src/n-dhcp4/src/test.h107
-rw-r--r--src/n-dhcp4/src/util/link.c287
-rw-r--r--src/n-dhcp4/src/util/link.h38
-rw-r--r--src/n-dhcp4/src/util/netns.c165
-rw-r--r--src/n-dhcp4/src/util/netns.h27
-rw-r--r--src/n-dhcp4/src/util/test-packet.c411
l---------src/n-dhcp4/subprojects/c-list1
l---------src/n-dhcp4/subprojects/c-siphash1
28 files changed, 3923 insertions, 2 deletions
diff --git a/src/n-dhcp4/.editorconfig b/src/n-dhcp4/.editorconfig
new file mode 100644
index 00000000..b10bb4f3
--- /dev/null
+++ b/src/n-dhcp4/.editorconfig
@@ -0,0 +1,11 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+charset = utf-8
+
+[*.{c,h}]
+indent_style = space
+indent_size = 8
diff --git a/src/n-dhcp4/.github/workflows/ci.yml b/src/n-dhcp4/.github/workflows/ci.yml
new file mode 100644
index 00000000..3583fdaa
--- /dev/null
+++ b/src/n-dhcp4/.github/workflows/ci.yml
@@ -0,0 +1,50 @@
+name: Continuous Integration
+
+on:
+  push:
+  pull_request:
+  schedule:
+  - cron:  '0 0 * * *'
+
+jobs:
+  ci:
+    name: CI with Default Configuration
+    runs-on: ubuntu-latest
+
+    steps:
+    #
+    # Prepare CI
+    #
+    # We cannot use the github-action of the `ci-c-util` project, because we
+    # need privileges in the container. Therefore, fetch the CI sources and
+    # build the container manually.
+    #
+    - name: Fetch CI
+      uses: actions/checkout@v2
+      with:
+        repository: c-util/automation
+        ref: v1
+        path: automation
+    - name: Build CI
+      working-directory: automation/src/ci-c-util
+      run: docker build --tag ci-c-util:v1 .
+
+    #
+    # Run CI
+    #
+    # Take the CI image we built and run the CI with the default project
+    # configuration. We do not use valgrind, since it falls-over with bpf(2)
+    # syscalls.
+    #
+    - name: Fetch Sources
+      uses: actions/checkout@v2
+      with:
+        path: source
+    - name: Run through C-Util CI
+      run: |
+        docker run \
+                --privileged \
+                -v "$(pwd)/source:/github/workspace" \
+                "ci-c-util:v1" \
+                "--m32=1" \
+                "--source=/github/workspace"
diff --git a/src/n-dhcp4/.gitmodules b/src/n-dhcp4/.gitmodules
new file mode 100644
index 00000000..ffe67922
--- /dev/null
+++ b/src/n-dhcp4/.gitmodules
@@ -0,0 +1,9 @@
+[submodule "subprojects/c-list"]
+	path = subprojects/c-list
+	url = https://github.com/c-util/c-list.git
+[submodule "subprojects/c-siphash"]
+	path = subprojects/c-siphash
+	url = https://github.com/c-util/c-siphash.git
+[submodule "subprojects/c-stdaux"]
+	path = subprojects/c-stdaux
+	url = https://github.com/c-util/c-stdaux.git
diff --git a/src/n-dhcp4/AUTHORS b/src/n-dhcp4/AUTHORS
new file mode 100644
index 00000000..b59660c5
--- /dev/null
+++ b/src/n-dhcp4/AUTHORS
@@ -0,0 +1,37 @@
+LICENSE:
+        This project is dual-licensed under both the Apache License, Version
+        2.0, and the GNU Lesser General Public License, Version 2.1+.
+
+AUTHORS-ASL:
+        Licensed under the Apache License, Version 2.0 (the "License");
+        you may not use this file except in compliance with the License.
+        You may obtain a copy of the License at
+
+                http://www.apache.org/licenses/LICENSE-2.0
+
+        Unless required by applicable law or agreed to in writing, software
+        distributed under the License is distributed on an "AS IS" BASIS,
+        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+        See the License for the specific language governing permissions and
+        limitations under the License.
+
+AUTHORS-LGPL:
+        This program is free software; you can redistribute it and/or modify it
+        under the terms of the GNU Lesser General Public License as published
+        by the Free Software Foundation; either version 2.1 of the License, or
+        (at your option) any later version.
+
+        This program is distributed in the hope that it will be useful, but
+        WITHOUT ANY WARRANTY; without even the implied warranty of
+        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+        Lesser General Public License for more details.
+
+        You should have received a copy of the GNU Lesser General Public License
+        along with this program; If not, see <http://www.gnu.org/licenses/>.
+
+COPYRIGHT: (ordered alphabetically)
+        Copyright (C) 2015-2019 Red Hat, Inc.
+
+AUTHORS: (ordered alphabetically)
+        David Rheinsberg <david.rheinsberg@gmail.com>
+        Tom Gundersen <teg@jklm.no>
diff --git a/src/n-dhcp4/NEWS.md b/src/n-dhcp4/NEWS.md
new file mode 100644
index 00000000..b41de08a
--- /dev/null
+++ b/src/n-dhcp4/NEWS.md
@@ -0,0 +1,20 @@
+# n-dhcp4 - Dynamic Host Configuration Protocol for IPv4
+
+## CHANGES WITH 1:
+
+        * Initial release of n-dhcp4, an implementation of the IPv4 Dynamic
+          Host Configuration Protocol as defined in RFC-2132+.
+
+        * This is a pre-release. The code is not yet ready for production.
+          Furthermore, the server implementation is incomplete and subject to
+          change.
+          The client implementation is considered complete, but the API is not
+          set in stone. We will have to adapt it according to the needs of the
+          users. Hence, this release does not provide any ABI stability
+          guarantees, yet.
+
+        * The n-dhcp4 project is now dual-licensed: ASL-2.0 and LGPL-2.1+
+
+        Contributions from: David Rheinsberg, Tom Gundersen
+
+        - Tübingen, 2019-05-10
diff --git a/src/n-dhcp4/README.md b/src/n-dhcp4/README.md
new file mode 100644
index 00000000..298b4972
--- /dev/null
+++ b/src/n-dhcp4/README.md
@@ -0,0 +1,53 @@
+n-dhcp4
+=======
+
+Dynamic Host Configuration Protocol for IPv4
+
+The n-dhcp4 project implements the IPv4 Dynamic Host Configuration Protocol as
+defined in RFC-2132+.
+
+### Project
+
+ * **Website**: <https://nettools.github.io/n-dhcp4>
+ * **Bug Tracker**: <https://github.com/nettools/n-dhcp4/issues>
+ * **Mailing-List**: <https://groups.google.com/forum/#!forum/nettools-devel>
+
+### Requirements
+
+The requirements for this project are:
+
+ * `Linux kernel >= 3.19`
+ * `libc` (e.g., `glibc >= 2.16`)
+
+At build-time, the following software is required:
+
+ * `meson >= 0.41`
+ * `pkg-config >= 0.29`
+
+### Build
+
+The meson build-system is used for this project. Contact upstream
+documentation for detailed help. In most situations the following
+commands are sufficient to build and install from source:
+
+```sh
+mkdir build
+cd build
+meson setup ..
+ninja
+meson test
+ninja install
+```
+
+No custom configuration options are available.
+
+### Repository:
+
+ - **web**:   <https://github.com/nettools/n-dhcp4>
+ - **https**: `https://github.com/nettools/n-dhcp4.git`
+ - **ssh**:   `git@github.com:nettools/n-dhcp4.git`
+
+### License:
+
+ - **Apache-2.0** OR **LGPL-2.1-or-later**
+ - See AUTHORS file for details.
diff --git a/src/n-dhcp4/meson.build b/src/n-dhcp4/meson.build
new file mode 100644
index 00000000..8cdcb368
--- /dev/null
+++ b/src/n-dhcp4/meson.build
@@ -0,0 +1,23 @@
+project(
+        'n-dhcp4',
+        'c',
+        version: '1',
+        license: 'Apache',
+        default_options: [
+                'c_std=c11',
+        ],
+)
+project_description = 'Dynamic Host Configuration Protocol for IPv4'
+
+add_project_arguments('-D_GNU_SOURCE', language: 'c')
+mod_pkgconfig = import('pkgconfig')
+
+sub_clist = subproject('c-list')
+sub_csiphash = subproject('c-siphash')
+sub_cstdaux = subproject('libcstdaux-1')
+
+dep_clist = sub_clist.get_variable('libclist_dep')
+dep_csiphash = sub_csiphash.get_variable('libcsiphash_dep')
+dep_cstdaux = sub_cstdaux.get_variable('libcstdaux_dep')
+
+subdir('src')
diff --git a/src/n-dhcp4/src/libndhcp4.sym b/src/n-dhcp4/src/libndhcp4.sym
new file mode 100644
index 00000000..adaf6d6f
--- /dev/null
+++ b/src/n-dhcp4/src/libndhcp4.sym
@@ -0,0 +1,71 @@
+LIBNDHCP4_1 {
+global:
+        n_dhcp4_client_config_new;
+        n_dhcp4_client_config_free;
+        n_dhcp4_client_config_set_ifindex;
+        n_dhcp4_client_config_set_transport;
+        n_dhcp4_client_config_set_request_broadcast;
+        n_dhcp4_client_config_set_mac;
+        n_dhcp4_client_config_set_broadcast_mac;
+        n_dhcp4_client_config_set_client_id;
+
+        n_dhcp4_client_probe_config_new;
+        n_dhcp4_client_probe_config_free;
+        n_dhcp4_client_probe_config_set_inform_only;
+        n_dhcp4_client_probe_config_set_init_reboot;
+        n_dhcp4_client_probe_config_set_requested_ip;
+        n_dhcp4_client_probe_config_set_start_delay;
+        n_dhcp4_client_probe_config_request_option;
+        n_dhcp4_client_probe_config_append_option;
+
+        n_dhcp4_client_new;
+        n_dhcp4_client_ref;
+        n_dhcp4_client_unref;
+        n_dhcp4_client_get_fd;
+        n_dhcp4_client_dispatch;
+        n_dhcp4_client_pop_event;
+        n_dhcp4_client_update_mtu;
+        n_dhcp4_client_probe;
+        n_dhcp4_client_set_log_level;
+
+        n_dhcp4_client_probe_free;
+        n_dhcp4_client_probe_get_userdata;
+        n_dhcp4_client_probe_set_userdata;
+
+        n_dhcp4_client_lease_ref;
+        n_dhcp4_client_lease_unref;
+        n_dhcp4_client_lease_get_yiaddr;
+        n_dhcp4_client_lease_get_siaddr;
+        n_dhcp4_client_lease_get_basetime;
+        n_dhcp4_client_lease_get_lifetime;
+        n_dhcp4_client_lease_get_server_identifier;
+        n_dhcp4_client_lease_get_file;
+        n_dhcp4_client_lease_query;
+        n_dhcp4_client_lease_select;
+        n_dhcp4_client_lease_accept;
+        n_dhcp4_client_lease_decline;
+
+        n_dhcp4_server_config_new;
+        n_dhcp4_server_config_free;
+        n_dhcp4_server_config_set_ifindex;
+
+        n_dhcp4_server_new;
+        n_dhcp4_server_ref;
+        n_dhcp4_server_unref;
+        n_dhcp4_server_get_fd;
+        n_dhcp4_server_dispatch;
+        n_dhcp4_server_pop_event;
+        n_dhcp4_server_add_ip;
+
+        n_dhcp4_server_ip_free;
+
+        n_dhcp4_server_lease_ref;
+        n_dhcp4_server_lease_unref;
+        n_dhcp4_server_lease_query;
+        n_dhcp4_server_lease_append;
+        n_dhcp4_server_lease_offer;
+        n_dhcp4_server_lease_ack;
+        n_dhcp4_server_lease_nack;
+local:
+       *;
+};
diff --git a/src/n-dhcp4/src/meson.build b/src/n-dhcp4/src/meson.build
new file mode 100644
index 00000000..0a3685aa
--- /dev/null
+++ b/src/n-dhcp4/src/meson.build
@@ -0,0 +1,90 @@
+#
+# target: libndhcp4.so
+#
+
+libndhcp4_symfile = join_paths(meson.current_source_dir(), 'libndhcp4.sym')
+
+libndhcp4_deps = [
+        dep_clist,
+        dep_csiphash,
+        dep_cstdaux,
+]
+
+libndhcp4_private = static_library(
+        'ndhcp4-private',
+        [
+                'n-dhcp4-c-connection.c',
+                'n-dhcp4-c-lease.c',
+                'n-dhcp4-c-probe.c',
+                'n-dhcp4-client.c',
+                'n-dhcp4-incoming.c',
+                'n-dhcp4-outgoing.c',
+                'n-dhcp4-s-connection.c',
+                'n-dhcp4-s-lease.c',
+                'n-dhcp4-server.c',
+                'n-dhcp4-socket.c',
+                'util/link.c',
+                'util/netns.c',
+                'util/packet.c',
+                'util/socket.c',
+        ],
+        c_args: [
+                '-fvisibility=hidden',
+                '-fno-common'
+        ],
+        dependencies: libndhcp4_deps,
+        pic: true,
+)
+
+libndhcp4_shared = shared_library(
+        'ndhcp4',
+        objects: libndhcp4_private.extract_all_objects(),
+        dependencies: libndhcp4_deps,
+        install: not meson.is_subproject(),
+        soversion: 0,
+        link_depends: libndhcp4_symfile,
+        link_args: [
+                '-Wl,--version-script=@0@'.format(libndhcp4_symfile)
+        ],
+)
+
+libndhcp4_dep = declare_dependency(
+        include_directories: include_directories('.'),
+        link_with: libndhcp4_private,
+        dependencies: libndhcp4_deps,
+        version: meson.project_version(),
+)
+
+if not meson.is_subproject()
+        install_headers('n-dhcp4.h')
+
+        mod_pkgconfig.generate(
+                libraries: libndhcp4_shared,
+                version: meson.project_version(),
+                name: 'libndhcp4',
+                filebase: 'libndhcp4',
+                description: project_description,
+        )
+endif
+
+#
+# target: test-*
+#
+
+test_api = executable('test-api', ['test-api.c'], link_with: libndhcp4_shared)
+test('API Symbol Visibility', test_api)
+
+test_connection = executable('test-connection', ['test-connection.c'], dependencies: libndhcp4_dep)
+test('Connection Handling', test_connection)
+
+test_message = executable('test-message', ['test-message.c'], dependencies: libndhcp4_dep)
+test('Message Handling', test_message)
+
+test_run_client = executable('test-run-client', ['test-run-client.c'], dependencies: libndhcp4_dep)
+test('Client Runner', test_run_client, args: ['--test'])
+
+test_socket = executable('test-socket', ['test-socket.c'], dependencies: libndhcp4_dep)
+test('Socket Handling', test_socket)
+
+test_util_packet = executable('test-util-packet', ['util/test-packet.c'], dependencies: libndhcp4_dep)
+test('Packet Utility Library', test_util_packet)
diff --git a/src/n-dhcp4/src/n-dhcp4-c-connection.c b/src/n-dhcp4/src/n-dhcp4-c-connection.c
index 7cb4f23f..7024b71a 100644
--- a/src/n-dhcp4/src/n-dhcp4-c-connection.c
+++ b/src/n-dhcp4/src/n-dhcp4-c-connection.c
@@ -1031,13 +1031,13 @@ static int n_dhcp4_c_connection_send_request(NDhcp4CConnection *connection,
         case N_DHCP4_C_MESSAGE_REBOOT:
         case N_DHCP4_C_MESSAGE_REBIND:
         case N_DHCP4_C_MESSAGE_RENEW:
+        case N_DHCP4_C_MESSAGE_DECLINE:
+        case N_DHCP4_C_MESSAGE_RELEASE:
                 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);
diff --git a/src/n-dhcp4/src/n-dhcp4-c-probe.c b/src/n-dhcp4/src/n-dhcp4-c-probe.c
index a5b38bcd..ee3a8886 100644
--- a/src/n-dhcp4/src/n-dhcp4-c-probe.c
+++ b/src/n-dhcp4/src/n-dhcp4-c-probe.c
@@ -1319,3 +1319,30 @@ int n_dhcp4_client_probe_dispatch_io(NDhcp4ClientProbe *probe, uint32_t events)
 int n_dhcp4_client_probe_update_mtu(NDhcp4ClientProbe *probe, uint16_t mtu) {
         return 0;
 }
+
+/**
+ * n_dhcp4_client_probe_release() - send a release request
+ * @probe:                          probe to operate on
+ *
+ * This sends a RELEASE message on the connection used by the probe.
+ *
+ * Return: 0 if successful otherwise non-zero value is returned.
+ */
+int n_dhcp4_client_probe_release(NDhcp4ClientProbe *probe) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request_out = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_release_new(&probe->connection, &request_out, NULL);
+        if (r)
+                return r;
+
+        r = n_dhcp4_c_connection_start_request(&probe->connection, request_out, 0);
+        if (r)
+                return r;
+
+        probe->state = N_DHCP4_CLIENT_PROBE_STATE_INIT;
+        n_dhcp4_client_lease_unlink(probe->current_lease);
+        request_out = NULL;
+
+        return 0;
+}
diff --git a/src/n-dhcp4/src/n-dhcp4-s-connection.c b/src/n-dhcp4/src/n-dhcp4-s-connection.c
new file mode 100644
index 00000000..5ed190a8
--- /dev/null
+++ b/src/n-dhcp4/src/n-dhcp4-s-connection.c
@@ -0,0 +1,412 @@
+/*
+ * DHCPv4 Server Connection
+ *
+ * XXX
+ */
+
+#include <assert.h>
+#include <c-stdaux.h>
+#include <errno.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"
+
+int n_dhcp4_s_connection_init(NDhcp4SConnection *connection, int ifindex) {
+        int r;
+
+        *connection = (NDhcp4SConnection)N_DHCP4_S_CONNECTION_NULL(*connection);
+
+        r = n_dhcp4_s_socket_packet_new(&connection->fd_packet);
+        if (r)
+                return r;
+
+        r = n_dhcp4_s_socket_udp_new(&connection->fd_udp, ifindex);
+        if (r)
+                return r;
+
+        connection->ifindex = ifindex;
+
+        return 0;
+}
+
+void n_dhcp4_s_connection_deinit(NDhcp4SConnection *connection) {
+        c_assert(!connection->ip);
+
+        if (connection->fd_udp >= 0) {
+                close(connection->fd_udp);
+        }
+
+        if (connection->fd_packet >= 0) {
+                close(connection->fd_packet);
+        }
+
+        *connection = (NDhcp4SConnection)N_DHCP4_S_CONNECTION_NULL(*connection);
+}
+
+void n_dhcp4_s_connection_get_fd(NDhcp4SConnection *connection, int *fdp) {
+        *fdp = connection->fd_udp;
+}
+
+static bool n_dhcp4_s_connection_owns_ip(NDhcp4SConnection *connection, struct in_addr addr) {
+        if (!connection->ip)
+                return false;
+        return (connection->ip->ip.s_addr == addr.s_addr);
+}
+
+static int n_dhcp4_s_connection_verify_incoming(NDhcp4SConnection *connection,
+                                                NDhcp4Incoming *message,
+                                                bool broadcast) {
+        uint8_t type;
+        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_DISCOVER:
+                message->userdata.type = N_DHCP4_C_MESSAGE_DISCOVER;
+                break;
+        case N_DHCP4_MESSAGE_REQUEST: {
+                struct in_addr server_identifier = {};
+                struct in_addr requested_ip = {};
+
+                r = n_dhcp4_incoming_query_server_identifier(message, &server_identifier);
+                if (r) {
+                        if (r == N_DHCP4_E_UNSET) {
+                                r = n_dhcp4_incoming_query_requested_ip(message, &requested_ip);
+                                if (r) {
+                                        if (r == N_DHCP4_E_UNSET) {
+                                                if (broadcast) {
+                                                        message->userdata.type = N_DHCP4_C_MESSAGE_REBIND;
+                                                } else {
+                                                        message->userdata.type = N_DHCP4_C_MESSAGE_RENEW;
+                                                }
+                                        } else {
+                                                return r;
+                                        }
+                                } else {
+                                        message->userdata.type = N_DHCP4_C_MESSAGE_REBOOT;
+                                }
+                        } else {
+                                return r;
+                        }
+                } else {
+                        if (n_dhcp4_s_connection_owns_ip(connection, server_identifier)) {
+                                message->userdata.type = N_DHCP4_C_MESSAGE_SELECT;
+                        } else {
+                                message->userdata.type = N_DHCP4_C_MESSAGE_IGNORE;
+                        }
+                }
+        }
+                break;
+        case N_DHCP4_MESSAGE_DECLINE:
+                message->userdata.type = N_DHCP4_C_MESSAGE_DECLINE;
+                break;
+        case N_DHCP4_MESSAGE_RELEASE:
+                message->userdata.type = N_DHCP4_C_MESSAGE_RELEASE;
+                break;
+        default:
+                return N_DHCP4_E_UNEXPECTED;
+        }
+
+        return 0;
+}
+
+int n_dhcp4_s_connection_dispatch_io(NDhcp4SConnection *connection, NDhcp4Incoming **messagep) {
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL;
+        struct sockaddr_in dest = {};
+        int r;
+
+        r = n_dhcp4_s_socket_udp_recv(connection->fd_udp,
+                                          connection->buf,
+                                          sizeof(connection->buf),
+                                          &message,
+                                          &dest);
+        if (r)
+                return r;
+
+        r = n_dhcp4_s_connection_verify_incoming(connection,
+                                                 message,
+                                                 dest.sin_addr.s_addr == INADDR_BROADCAST);
+        if (r) {
+                if (r == N_DHCP4_E_MALFORMED || r == N_DHCP4_E_UNEXPECTED) {
+                        *messagep = NULL;
+                        return 0;
+                }
+
+                return -ENOTRECOVERABLE;
+        }
+
+        *messagep = message;
+        message = NULL;
+        return 0;
+}
+
+/*
+ * If the 'giaddr' field in a DHCP message from a client is non-zero,
+ * the server sends any return messages to the 'DHCP server' port on the
+ * BOOTP relay agent whose address appears in 'giaddr'. If the 'giaddr'
+ * field is zero and the 'ciaddr' field is nonzero, then the server
+ * unicasts DHCPOFFER and DHCPACK messages to the address in 'ciaddr'.
+ * If 'giaddr' is zero and 'ciaddr' is zero, and the broadcast bit is
+ * set, then the server broadcasts DHCPOFFER and DHCPACK messages to
+ * 0xffffffff. If the broadcast bit is not set and 'giaddr' is zero and
+ * 'ciaddr' is zero, then the server unicasts DHCPOFFER and DHCPACK
+ * messages to the client's hardware address and 'yiaddr' address.  In
+ * all cases, when 'giaddr' is zero, the server broadcasts any DHCPNAK
+ * messages to 0xffffffff.
+ */
+int n_dhcp4_s_connection_send_reply(NDhcp4SConnection *connection,
+                                    const struct in_addr *server_addr,
+                                    NDhcp4Outgoing *message) {
+        NDhcp4Header *header = n_dhcp4_outgoing_get_header(message);
+        int r;
+
+        if (header->giaddr) {
+                const struct in_addr giaddr = { header->giaddr };
+
+                r = n_dhcp4_s_socket_udp_send(connection->fd_udp,
+                                              server_addr,
+                                              &giaddr,
+                                              message);
+                if (r)
+                        return r;
+        } else if (header->ciaddr) {
+                const struct in_addr ciaddr = { header->ciaddr };
+
+                r = n_dhcp4_s_socket_udp_send(connection->fd_udp,
+                                              server_addr,
+                                              &ciaddr,
+                                              message);
+                if (r)
+                        return r;
+        } else if (header->flags & htons(N_DHCP4_MESSAGE_FLAG_BROADCAST)) {
+                r = n_dhcp4_s_socket_udp_broadcast(connection->fd_udp,
+                                                   server_addr,
+                                                   message);
+                if (r)
+                        return r;
+        } else {
+                r = n_dhcp4_s_socket_packet_send(connection->fd_packet,
+                                                 connection->ifindex,
+                                                 server_addr,
+                                                 header->chaddr,
+                                                 header->hlen,
+                                                 &(struct in_addr){header->yiaddr},
+                                                 N_DHCP4_DSCP_DEFAULT,
+                                                 message);
+                if (r)
+                        return r;
+        }
+
+        return 0;
+}
+
+static void n_dhcp4_s_connection_init_reply_header(NDhcp4SConnection *connection,
+                                                   NDhcp4Header *request,
+                                                   NDhcp4Header *reply) {
+        reply->op = N_DHCP4_OP_BOOTREPLY;
+
+        reply->htype = request->htype;
+        reply->hlen = request->hlen;
+        reply->flags = request->flags;
+        reply->xid = request->xid;
+        reply->ciaddr = request->ciaddr;
+        reply->giaddr = request->giaddr;
+        memcpy(reply->chaddr, request->chaddr, request->hlen);
+}
+
+static int n_dhcp4_s_connection_outgoing_set_yiaddr(NDhcp4Outgoing *message,
+                                                     uint32_t yiaddr,
+                                                     uint32_t lifetime) {
+        uint32_t t1 = lifetime / 2;
+        uint32_t t2 = ((uint64_t)lifetime * 7) / 8;
+        struct in_addr addr = { .s_addr = yiaddr };
+        int r;
+
+        r = n_dhcp4_outgoing_append_lifetime(message, lifetime);
+        if (r)
+                return r;
+
+        r = n_dhcp4_outgoing_append_t1(message, t1);
+        if (r)
+                return r;
+
+        r = n_dhcp4_outgoing_append_t2(message, t2);
+        if (r)
+                return r;
+
+        n_dhcp4_outgoing_set_yiaddr(message, addr);
+
+        return 0;
+}
+
+static int n_dhcp4_s_connection_new_reply(NDhcp4SConnection *connection,
+                                          NDhcp4Outgoing **messagep,
+                                          NDhcp4Incoming *request,
+                                          uint8_t type,
+                                          const struct in_addr *server_address) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
+        uint16_t max_message_size;
+        uint8_t *client_identifier;
+        size_t n_client_identifier;
+        int r;
+
+        r = n_dhcp4_incoming_query_max_message_size(request, &max_message_size);
+        if (r)
+                return r;
+
+        r = n_dhcp4_outgoing_new(&message,
+                                 max_message_size,
+                                 N_DHCP4_OVERLOAD_FILE | N_DHCP4_OVERLOAD_SNAME);
+        if (r)
+                return r;
+
+        n_dhcp4_s_connection_init_reply_header(connection,
+                                               n_dhcp4_incoming_get_header(request),
+                                               n_dhcp4_outgoing_get_header(message));
+
+        r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_MESSAGE_TYPE, &type, sizeof(type));
+        if (r)
+                return r;
+
+        r = n_dhcp4_outgoing_append_server_identifier(message, *server_address);
+        if (r)
+                return r;
+
+        r = n_dhcp4_incoming_query(request,
+                                   N_DHCP4_OPTION_CLIENT_IDENTIFIER,
+                                   &client_identifier,
+                                   &n_client_identifier);
+        if (!r) {
+                r = n_dhcp4_outgoing_append(message,
+                                            N_DHCP4_OPTION_CLIENT_IDENTIFIER,
+                                            client_identifier,
+                                            n_client_identifier);
+                if (r)
+                        return r;
+        } else if (r != N_DHCP4_E_UNSET) {
+                return r;
+        }
+
+        *messagep = message;
+        message = NULL;
+        return 0;
+}
+
+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) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *reply = NULL;
+        int r;
+
+        r = n_dhcp4_s_connection_new_reply(connection,
+                                           &reply,
+                                           request,
+                                           N_DHCP4_MESSAGE_OFFER,
+                                           server_address);
+        if (r)
+                return r;
+
+        r = n_dhcp4_s_connection_outgoing_set_yiaddr(reply,
+                                                     client_address->s_addr,
+                                                     lifetime);
+        if (r)
+                return r;
+
+        *replyp = reply;
+        reply = NULL;
+        return 0;
+}
+
+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) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *reply = NULL;
+        int r;
+
+        r = n_dhcp4_s_connection_new_reply(connection,
+                                           &reply,
+                                           request,
+                                           N_DHCP4_MESSAGE_ACK,
+                                           server_address);
+        if (r)
+                return r;
+
+        r = n_dhcp4_s_connection_outgoing_set_yiaddr(reply,
+                                                     client_address->s_addr,
+                                                     lifetime);
+        if (r)
+                return r;
+
+        *replyp = reply;
+        reply = NULL;
+        return 0;
+}
+
+int n_dhcp4_s_connection_nak_new(NDhcp4SConnection *connection,
+                                 NDhcp4Outgoing **replyp,
+                                 NDhcp4Incoming *request,
+                                 const struct in_addr *server_address) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *reply = NULL;
+        int r;
+
+        r = n_dhcp4_s_connection_new_reply(connection,
+                                           &reply,
+                                           request,
+                                           N_DHCP4_MESSAGE_NAK,
+                                           server_address);
+        if (r)
+                return r;
+
+        /*
+         * The RFC is a bit unclear on how NAK should be sent, on the
+         * one hand it says that they should be unconditionally broadcast
+         * (unless going through a relay agent), on the other, when they
+         * do go through a relay agent, they will not be. We treat them
+         * as any other reply and only broadcast when the broadcast bit
+         * is set.
+         */
+
+        *replyp = reply;
+        reply = NULL;
+        return 0;
+}
+
+void n_dhcp4_s_connection_ip_init(NDhcp4SConnectionIp *ip, struct in_addr addr) {
+        *ip = (NDhcp4SConnectionIp)N_DHCP4_S_CONNECTION_IP_NULL(*ip);
+        ip->ip = addr;
+}
+
+void n_dhcp4_s_connection_ip_deinit(NDhcp4SConnectionIp *ip) {
+        c_assert(!ip->connection);
+        *ip = (NDhcp4SConnectionIp)N_DHCP4_S_CONNECTION_IP_NULL(*ip);
+}
+
+void n_dhcp4_s_connection_ip_link(NDhcp4SConnectionIp *ip, NDhcp4SConnection *connection) {
+        c_assert(!connection->ip);
+        c_assert(!ip->connection);
+
+        connection->ip = ip;
+        ip->connection = connection;
+}
+
+void n_dhcp4_s_connection_ip_unlink(NDhcp4SConnectionIp *ip) {
+        ip->connection->ip = NULL;
+        ip->connection = NULL;
+}
diff --git a/src/n-dhcp4/src/n-dhcp4-s-lease.c b/src/n-dhcp4/src/n-dhcp4-s-lease.c
new file mode 100644
index 00000000..cb8aabf9
--- /dev/null
+++ b/src/n-dhcp4/src/n-dhcp4-s-lease.c
@@ -0,0 +1,103 @@
+/*
+ * XXX
+ */
+
+#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"
+
+/**
+ * n_dhcp4_server_lease_new() - XXX
+ */
+int n_dhcp4_server_lease_new(NDhcp4ServerLease **leasep, NDhcp4Incoming *message) {
+        _c_cleanup_(n_dhcp4_server_lease_unrefp) NDhcp4ServerLease *lease = NULL;
+
+        c_assert(leasep);
+
+        lease = malloc(sizeof(*lease));
+        if (!lease)
+                return -ENOMEM;
+
+        *lease = (NDhcp4ServerLease)N_DHCP4_SERVER_LEASE_NULL(*lease);
+
+        lease->request = message;
+
+        *leasep = lease;
+        lease = NULL;
+        return 0;
+}
+
+static void n_dhcp4_server_lease_free(NDhcp4ServerLease *lease) {
+        c_assert(!lease->server);
+
+        c_list_unlink(&lease->server_link);
+
+        n_dhcp4_incoming_free(lease->request);
+        free(lease);
+}
+
+/**
+ * n_dhcp4_server_lease_ref() - XXX
+ */
+_c_public_ NDhcp4ServerLease *n_dhcp4_server_lease_ref(NDhcp4ServerLease *lease) {
+        if (lease)
+                ++lease->n_refs;
+        return lease;
+}
+
+/**
+ * n_dhcp4_server_lease_unref() - XXX
+ */
+_c_public_ NDhcp4ServerLease *n_dhcp4_server_lease_unref(NDhcp4ServerLease *lease) {
+        if (lease && !--lease->n_refs)
+                n_dhcp4_server_lease_free(lease);
+        return NULL;
+}
+
+/**
+ * n_dhcp4_server_lease_query() - XXX
+ */
+_c_public_ int n_dhcp4_server_lease_query(NDhcp4ServerLease *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;
+        }
+
+        return n_dhcp4_incoming_query(lease->request, option, datap, n_datap);
+}
+
+_c_public_ int n_dhcp4_server_lease_append(NDhcp4ServerLease *lease, uint8_t option, uint8_t *data, size_t n_data) {
+        /* XXX */
+        return -ENOTRECOVERABLE;
+}
+
+_c_public_ int n_dhcp4_server_lease_offer(NDhcp4ServerLease *lease) {
+        /* XXX */
+        return -ENOTRECOVERABLE;
+}
+
+_c_public_ int n_dhcp4_server_lease_ack(NDhcp4ServerLease *lease) {
+        /* XXX */
+        return -ENOTRECOVERABLE;
+}
+
+_c_public_ int n_dhcp4_server_lease_nack(NDhcp4ServerLease *lease) {
+        /* XXX */
+        return -ENOTRECOVERABLE;
+}
diff --git a/src/n-dhcp4/src/n-dhcp4-server.c b/src/n-dhcp4/src/n-dhcp4-server.c
new file mode 100644
index 00000000..7b971eeb
--- /dev/null
+++ b/src/n-dhcp4/src/n-dhcp4-server.c
@@ -0,0 +1,241 @@
+/*
+ * Server Side of the Dynamic Host Configuration Protocol for IPv4
+ *
+ * XXX
+ */
+
+#include <assert.h>
+#include <c-list.h>
+#include <c-stdaux.h>
+#include <errno.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"
+#include "util/packet.h"
+
+/**
+ * n_dhcp4_server_config_new() - XXX
+ */
+_c_public_ int n_dhcp4_server_config_new(NDhcp4ServerConfig **configp) {
+        _c_cleanup_(n_dhcp4_server_config_freep) NDhcp4ServerConfig *config = NULL;
+
+        config = calloc(1, sizeof(*config));
+        if (!config)
+                return -ENOMEM;
+
+        *config = (NDhcp4ServerConfig)N_DHCP4_SERVER_CONFIG_NULL(*config);
+
+        *configp = config;
+        config = NULL;
+        return 0;
+}
+
+/**
+ * n_dhcp4_server_config_free() - XXX
+ */
+_c_public_ NDhcp4ServerConfig *n_dhcp4_server_config_free(NDhcp4ServerConfig *config) {
+        if (!config)
+                return NULL;
+
+        free(config);
+
+        return NULL;
+}
+
+/**
+ * n_dhcp4_server_config_set_ifindex() - XXX
+ */
+_c_public_ void n_dhcp4_server_config_set_ifindex(NDhcp4ServerConfig *config, int ifindex) {
+        config->ifindex = ifindex;
+}
+
+/**
+ * n_dhcp4_s_event_node_new() - XXX
+ */
+int n_dhcp4_s_event_node_new(NDhcp4SEventNode **nodep) {
+        NDhcp4SEventNode *node;
+
+        node = calloc(1, sizeof(*node));
+        if (!node)
+                return -ENOMEM;
+
+        *node = (NDhcp4SEventNode)N_DHCP4_S_EVENT_NODE_NULL(*node);
+
+        *nodep = node;
+        return 0;
+}
+
+/**
+ * n_dhcp4_s_event_node_free() - XXX
+ */
+NDhcp4SEventNode *n_dhcp4_s_event_node_free(NDhcp4SEventNode *node) {
+        if (!node)
+                return NULL;
+
+        c_list_unlink(&node->server_link);
+        free(node);
+
+        return NULL;
+}
+
+/**
+ * n_dhcp4_server_new() - XXX
+ */
+_c_public_ int n_dhcp4_server_new(NDhcp4Server **serverp, NDhcp4ServerConfig *config) {
+        _c_cleanup_(n_dhcp4_server_unrefp) NDhcp4Server *server = NULL;
+        int r;
+
+        c_assert(serverp);
+
+        server = malloc(sizeof(*server));
+        if (!server)
+                return -ENOMEM;
+
+        *server = (NDhcp4Server)N_DHCP4_SERVER_NULL(*server);
+
+        r = n_dhcp4_s_connection_init(&server->connection, config->ifindex);
+        if (r)
+                return r;
+
+        *serverp = server;
+        server = NULL;
+        return 0;
+}
+
+static void n_dhcp4_server_free(NDhcp4Server *server) {
+        NDhcp4SEventNode *node, *t_node;
+
+        c_list_for_each_entry_safe(node, t_node, &server->event_list, server_link)
+                n_dhcp4_s_event_node_free(node);
+
+        free(server);
+}
+
+/**
+ * n_dhcp4_server_ref() - XXX
+ */
+_c_public_ NDhcp4Server *n_dhcp4_server_ref(NDhcp4Server *server) {
+        if (server)
+                ++server->n_refs;
+        return server;
+}
+
+/**
+ * n_dhcp4_server_unref() - XXX
+ */
+_c_public_ NDhcp4Server *n_dhcp4_server_unref(NDhcp4Server *server) {
+        if (server && !--server->n_refs)
+                n_dhcp4_server_free(server);
+        return NULL;
+}
+
+/**
+ * n_dhcp4_server_raise() - XXX
+ */
+int n_dhcp4_server_raise(NDhcp4Server *server, NDhcp4SEventNode **nodep, unsigned int event) {
+        NDhcp4SEventNode *node;
+        int r;
+
+        r = n_dhcp4_s_event_node_new(&node);
+        if (r)
+                return r;
+
+        node->event.event = event;
+        c_list_link_tail(&server->event_list, &node->server_link);
+
+        if (nodep)
+                *nodep = node;
+        return 0;
+}
+
+/**
+ * n_dhcp4_server_get_fd() - XXX
+ */
+_c_public_ void n_dhcp4_server_get_fd(NDhcp4Server *server, int *fdp) {
+        n_dhcp4_s_connection_get_fd(&server->connection, fdp);
+}
+
+/**
+ * n_dhcp4_server_dispatch() - XXX
+ */
+_c_public_ int n_dhcp4_server_dispatch(NDhcp4Server *server) {
+        int r;
+
+        for (unsigned int i = 0; i < 128; ++i) {
+                _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL;
+
+                r = n_dhcp4_s_connection_dispatch_io(&server->connection, &message);
+                if (r) {
+                        if (r == N_DHCP4_E_AGAIN)
+                                return 0;
+                        return r;
+                }
+        }
+
+        return N_DHCP4_E_PREEMPTED;
+}
+
+/**
+ * n_dhcp4_server_pop_event() - XXX
+ */
+_c_public_ int n_dhcp4_server_pop_event(NDhcp4Server *server, NDhcp4ServerEvent **eventp) {
+        NDhcp4SEventNode *node, *t_node;
+
+        c_list_for_each_entry_safe(node, t_node, &server->event_list, server_link) {
+                if (node->is_public) {
+                        n_dhcp4_s_event_node_free(node);
+                        continue;
+                }
+
+                node->is_public = true;
+                *eventp = &node->event;
+                return 0;
+        }
+
+        *eventp = NULL;
+        return 0;
+}
+
+/**
+ * n_dhcp4_server_add_ip() - XXX
+ */
+_c_public_ int n_dhcp4_server_add_ip(NDhcp4Server *server, NDhcp4ServerIp **ipp, struct in_addr addr) {
+        _c_cleanup_(n_dhcp4_server_ip_freep) NDhcp4ServerIp *ip = NULL;
+
+        /* XXX: support more than one address */
+        if (server->connection.ip)
+                return -EBUSY;
+
+        ip = malloc(sizeof(*ip));
+        if (!ip)
+                return -ENOMEM;
+
+        *ip = (NDhcp4ServerIp)N_DHCP4_SERVER_IP_NULL(*ip);
+
+        n_dhcp4_s_connection_ip_init(&ip->ip, addr);
+        n_dhcp4_s_connection_ip_link(&ip->ip, &server->connection);
+
+        *ipp = ip;
+        ip = NULL;
+        return 0;
+}
+
+/**
+ * n_dhcp4_server_ip_free() - XXX
+ */
+_c_public_ NDhcp4ServerIp *n_dhcp4_server_ip_free(NDhcp4ServerIp *ip) {
+        if (!ip)
+                return NULL;
+
+        n_dhcp4_s_connection_ip_unlink(&ip->ip);
+        n_dhcp4_s_connection_ip_deinit(&ip->ip);
+
+        free(ip);
+        return NULL;
+}
diff --git a/src/n-dhcp4/src/n-dhcp4.h b/src/n-dhcp4/src/n-dhcp4.h
index f6c87a8d..f3cf4210 100644
--- a/src/n-dhcp4/src/n-dhcp4.h
+++ b/src/n-dhcp4/src/n-dhcp4.h
@@ -161,6 +161,7 @@ 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);
+int n_dhcp4_client_probe_release(NDhcp4ClientProbe *probe);
 
 /* client leases */
 
diff --git a/src/n-dhcp4/src/test-api.c b/src/n-dhcp4/src/test-api.c
new file mode 100644
index 00000000..fac33008
--- /dev/null
+++ b/src/n-dhcp4/src/test-api.c
@@ -0,0 +1,157 @@
+/*
+ * API Visibility Tests
+ * This verifies the visibility and availability of the exported API.
+ */
+
+#undef NDEBUG
+#include <assert.h>
+#include <stdlib.h>
+#include "n-dhcp4.h"
+
+static void test_api_constants(void) {
+        assert(1 + N_DHCP4_CLIENT_START_DELAY_RFC2131);
+
+        assert(1 + _N_DHCP4_E_SUCCESS);
+        assert(1 + N_DHCP4_E_PREEMPTED);
+        assert(1 + N_DHCP4_E_INTERNAL);
+        assert(1 + N_DHCP4_E_INVALID_IFINDEX);
+        assert(1 + N_DHCP4_E_INVALID_TRANSPORT);
+        assert(1 + N_DHCP4_E_INVALID_ADDRESS);
+        assert(1 + N_DHCP4_E_INVALID_CLIENT_ID);
+        assert(1 + N_DHCP4_E_DUPLICATE_OPTION);
+        assert(1 + N_DHCP4_E_UNSET);
+        assert(1 + _N_DHCP4_E_N);
+
+        assert(1 + N_DHCP4_TRANSPORT_ETHERNET);
+        assert(1 + N_DHCP4_TRANSPORT_INFINIBAND);
+        assert(1 + _N_DHCP4_TRANSPORT_N);
+
+        assert(1 + N_DHCP4_CLIENT_EVENT_DOWN);
+        assert(1 + N_DHCP4_CLIENT_EVENT_OFFER);
+        assert(1 + N_DHCP4_CLIENT_EVENT_GRANTED);
+        assert(1 + N_DHCP4_CLIENT_EVENT_RETRACTED);
+        assert(1 + N_DHCP4_CLIENT_EVENT_EXTENDED);
+        assert(1 + N_DHCP4_CLIENT_EVENT_EXPIRED);
+        assert(1 + N_DHCP4_CLIENT_EVENT_CANCELLED);
+        assert(1 + _N_DHCP4_CLIENT_EVENT_N);
+
+        assert(1 + N_DHCP4_SERVER_EVENT_DOWN);
+        assert(1 + N_DHCP4_SERVER_EVENT_DISCOVER);
+        assert(1 + N_DHCP4_SERVER_EVENT_REQUEST);
+        assert(1 + N_DHCP4_SERVER_EVENT_RENEW);
+        assert(1 + N_DHCP4_SERVER_EVENT_DECLINE);
+        assert(1 + N_DHCP4_SERVER_EVENT_RELEASE);
+        assert(1 + _N_DHCP4_SERVER_EVENT_N);
+}
+
+static void test_api_types(void) {
+        assert(sizeof(NDhcp4ClientConfig*) > 0);
+        assert(sizeof(NDhcp4ClientProbeConfig*) > 0);
+        assert(sizeof(NDhcp4Client*) > 0);
+        assert(sizeof(NDhcp4ClientEvent) > 0);
+        assert(sizeof(NDhcp4ClientProbe*) > 0);
+        assert(sizeof(NDhcp4ClientLease*) > 0);
+        assert(sizeof(NDhcp4Server*) > 0);
+        assert(sizeof(NDhcp4ServerConfig*) > 0);
+        assert(sizeof(NDhcp4ServerEvent) > 0);
+        assert(sizeof(NDhcp4ServerIp*) > 0);
+        assert(sizeof(NDhcp4ServerLease*) > 0);
+}
+
+static void test_api_functions(void) {
+        void *fns[] = {
+                (void *)n_dhcp4_client_config_new,
+                (void *)n_dhcp4_client_config_free,
+                (void *)n_dhcp4_client_config_freep,
+                (void *)n_dhcp4_client_config_freev,
+                (void *)n_dhcp4_client_config_set_ifindex,
+                (void *)n_dhcp4_client_config_set_transport,
+                (void *)n_dhcp4_client_config_set_request_broadcast,
+                (void *)n_dhcp4_client_config_set_mac,
+                (void *)n_dhcp4_client_config_set_broadcast_mac,
+                (void *)n_dhcp4_client_config_set_client_id,
+
+                (void *)n_dhcp4_client_probe_config_new,
+                (void *)n_dhcp4_client_probe_config_free,
+                (void *)n_dhcp4_client_probe_config_freep,
+                (void *)n_dhcp4_client_probe_config_freev,
+                (void *)n_dhcp4_client_probe_config_set_inform_only,
+                (void *)n_dhcp4_client_probe_config_set_init_reboot,
+                (void *)n_dhcp4_client_probe_config_set_requested_ip,
+                (void *)n_dhcp4_client_probe_config_set_start_delay,
+                (void *)n_dhcp4_client_probe_config_request_option,
+                (void *)n_dhcp4_client_probe_config_append_option,
+
+                (void *)n_dhcp4_client_new,
+                (void *)n_dhcp4_client_ref,
+                (void *)n_dhcp4_client_unref,
+                (void *)n_dhcp4_client_unrefp,
+                (void *)n_dhcp4_client_unrefv,
+                (void *)n_dhcp4_client_get_fd,
+                (void *)n_dhcp4_client_dispatch,
+                (void *)n_dhcp4_client_pop_event,
+                (void *)n_dhcp4_client_update_mtu,
+                (void *)n_dhcp4_client_probe,
+
+                (void *)n_dhcp4_client_probe_free,
+                (void *)n_dhcp4_client_probe_freep,
+                (void *)n_dhcp4_client_probe_freev,
+                (void *)n_dhcp4_client_probe_get_userdata,
+                (void *)n_dhcp4_client_probe_set_userdata,
+
+                (void *)n_dhcp4_client_lease_ref,
+                (void *)n_dhcp4_client_lease_unref,
+                (void *)n_dhcp4_client_lease_unrefp,
+                (void *)n_dhcp4_client_lease_unrefv,
+                (void *)n_dhcp4_client_lease_get_yiaddr,
+                (void *)n_dhcp4_client_lease_get_siaddr,
+                (void *)n_dhcp4_client_lease_get_lifetime,
+                (void *)n_dhcp4_client_lease_get_server_identifier,
+                (void *)n_dhcp4_client_lease_get_file,
+                (void *)n_dhcp4_client_lease_query,
+                (void *)n_dhcp4_client_lease_select,
+                (void *)n_dhcp4_client_lease_accept,
+                (void *)n_dhcp4_client_lease_decline,
+
+                (void *)n_dhcp4_server_config_new,
+                (void *)n_dhcp4_server_config_free,
+                (void *)n_dhcp4_server_config_freep,
+                (void *)n_dhcp4_server_config_freev,
+                (void *)n_dhcp4_server_config_set_ifindex,
+
+                (void *)n_dhcp4_server_new,
+                (void *)n_dhcp4_server_ref,
+                (void *)n_dhcp4_server_unref,
+                (void *)n_dhcp4_server_unrefp,
+                (void *)n_dhcp4_server_unrefv,
+                (void *)n_dhcp4_server_get_fd,
+                (void *)n_dhcp4_server_dispatch,
+                (void *)n_dhcp4_server_pop_event,
+                (void *)n_dhcp4_server_add_ip,
+
+                (void *)n_dhcp4_server_ip_free,
+                (void *)n_dhcp4_server_ip_freep,
+                (void *)n_dhcp4_server_ip_freev,
+
+                (void *)n_dhcp4_server_lease_ref,
+                (void *)n_dhcp4_server_lease_unref,
+                (void *)n_dhcp4_server_lease_unrefp,
+                (void *)n_dhcp4_server_lease_unrefv,
+                (void *)n_dhcp4_server_lease_query,
+                (void *)n_dhcp4_server_lease_append,
+                (void *)n_dhcp4_server_lease_offer,
+                (void *)n_dhcp4_server_lease_ack,
+                (void *)n_dhcp4_server_lease_nack,
+        };
+        size_t i;
+
+        for (i = 0; i < sizeof(fns) / sizeof(*fns); ++i)
+                assert(!!fns[i]);
+}
+
+int main(int argc, char **argv) {
+        test_api_constants();
+        test_api_types();
+        test_api_functions();
+        return 0;
+}
diff --git a/src/n-dhcp4/src/test-connection.c b/src/n-dhcp4/src/test-connection.c
new file mode 100644
index 00000000..98bd2aca
--- /dev/null
+++ b/src/n-dhcp4/src/test-connection.c
@@ -0,0 +1,390 @@
+/*
+ * Tests for DHCP4 Client Connections
+ */
+
+#undef NDEBUG
+#include <assert.h>
+#include <c-stdaux.h>
+#include <endian.h>
+#include <errno.h>
+#include <poll.h>
+#include <linux/if_packet.h>
+#include <net/if_arp.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/epoll.h>
+#include "n-dhcp4-private.h"
+#include "test.h"
+#include "util/link.h"
+#include "util/netns.h"
+#include "util/packet.h"
+
+static void test_poll_client(int efd, unsigned int u32) {
+        struct epoll_event event = {};
+        int r;
+
+        r = epoll_wait(efd, &event, 1, -1);
+        c_assert(r == 1);
+        c_assert(event.events == EPOLLIN);
+        c_assert(event.data.u32 == u32);
+}
+
+static void test_poll_server(int fd) {
+        struct pollfd pfd = { .fd = fd, .events = POLLIN };
+        int r;
+
+        r = poll(&pfd, 1, -1);
+        c_assert(r == 1);
+        c_assert(pfd.revents == POLLIN);
+}
+
+static void test_s_connection_init(int netns, NDhcp4SConnection *connection, int ifindex) {
+        int r, oldns;
+
+        netns_get(&oldns);
+        netns_set(netns);
+
+        r = n_dhcp4_s_connection_init(connection, ifindex);
+        c_assert(!r);
+
+        netns_set(oldns);
+}
+
+static void test_c_connection_listen(int netns, NDhcp4CConnection *connection) {
+        int r, oldns;
+
+        netns_get(&oldns);
+        netns_set(netns);
+
+        r = n_dhcp4_c_connection_listen(connection);
+        c_assert(!r);
+
+        netns_set(oldns);
+}
+
+static void test_c_connection_connect(int netns,
+                                      NDhcp4CConnection *connection,
+                                      const struct in_addr *client,
+                                      const struct in_addr *server) {
+        int r, oldns;
+
+        netns_get(&oldns);
+        netns_set(netns);
+
+        r = n_dhcp4_c_connection_connect(connection, client, server);
+        c_assert(!r);
+
+        netns_set(oldns);
+}
+
+static void test_server_receive(NDhcp4SConnection *connection, uint8_t expected_type, NDhcp4Incoming **messagep) {
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL;
+        uint8_t received_type;
+        int r, fd;
+
+        n_dhcp4_s_connection_get_fd(connection, &fd);
+        test_poll_server(fd);
+
+        r = n_dhcp4_s_connection_dispatch_io(connection, &message);
+        c_assert(!r);
+        c_assert(message);
+
+        r = n_dhcp4_incoming_query_message_type(message, &received_type);
+        c_assert(!r);
+        c_assert(received_type == expected_type);
+
+        if (messagep) {
+                *messagep = message;
+                message = NULL;
+        }
+}
+
+static void test_client_receive(NDhcp4CConnection *connection, uint8_t expected_type, NDhcp4Incoming **messagep) {
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL;
+        uint8_t received_type;
+        int r;
+
+        test_poll_client(connection->fd_epoll, N_DHCP4_CLIENT_EPOLL_IO);
+
+        r = n_dhcp4_c_connection_dispatch_io(connection, &message);
+        c_assert(!r);
+        c_assert(message);
+
+        r = n_dhcp4_incoming_query_message_type(message, &received_type);
+        c_assert(!r);
+        c_assert(received_type == expected_type);
+
+        if (messagep) {
+                *messagep = message;
+                message = NULL;
+        }
+}
+
+static void test_discover(NDhcp4SConnection *connection_server,
+                          NDhcp4CConnection *connection_client,
+                          const struct in_addr *addr_server,
+                          const struct in_addr *addr_client,
+                          NDhcp4Incoming **offerp) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request_out = NULL;
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *request_in = NULL;
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *reply_out = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_discover_new(connection_client, &request_out);
+        c_assert(!r);
+
+        r = n_dhcp4_c_connection_start_request(connection_client, request_out, 0);
+        c_assert(!r);
+        request_out = NULL;
+
+        test_server_receive(connection_server, N_DHCP4_MESSAGE_DISCOVER, &request_in);
+
+        r = n_dhcp4_s_connection_offer_new(connection_server, &reply_out, request_in, addr_server, addr_client, 60);
+        c_assert(!r);
+
+        r = n_dhcp4_s_connection_send_reply(connection_server, addr_server, reply_out);
+        c_assert(!r);
+
+        test_client_receive(connection_client, N_DHCP4_MESSAGE_OFFER, offerp);
+}
+
+static void test_select(NDhcp4SConnection *connection_server,
+                        NDhcp4CConnection *connection_client,
+                        NDhcp4Incoming *offer,
+                        const struct in_addr *addr_server,
+                        const struct in_addr *addr_client) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request_out = NULL;
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *request_in = NULL;
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *reply = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_select_new(connection_client, &request_out, offer);
+        c_assert(!r);
+
+        r = n_dhcp4_c_connection_start_request(connection_client, request_out, 0);
+        c_assert(!r);
+        request_out = NULL;
+
+        test_server_receive(connection_server, N_DHCP4_MESSAGE_REQUEST, &request_in);
+
+        r = n_dhcp4_s_connection_ack_new(connection_server, &reply, request_in, addr_server, addr_client, 60);
+        c_assert(!r);
+
+        r = n_dhcp4_s_connection_send_reply(connection_server, addr_server, reply);
+        c_assert(!r);
+
+        test_client_receive(connection_client, N_DHCP4_MESSAGE_ACK, NULL);
+}
+
+static void test_reboot(NDhcp4SConnection *connection_server,
+                        NDhcp4CConnection *connection_client,
+                        const struct in_addr *addr_server,
+                        const struct in_addr *addr_client,
+                        NDhcp4Incoming **ackp) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request_out = NULL;
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *request_in = NULL;
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *reply = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_reboot_new(connection_client, &request_out, addr_server);
+        c_assert(!r);
+
+        r = n_dhcp4_c_connection_start_request(connection_client, request_out, 0);
+        c_assert(!r);
+        request_out = NULL;
+
+        test_server_receive(connection_server, N_DHCP4_MESSAGE_REQUEST, &request_in);
+
+        r = n_dhcp4_s_connection_ack_new(connection_server, &reply, request_in, addr_server, addr_client, 60);
+        c_assert(!r);
+
+        r = n_dhcp4_s_connection_send_reply(connection_server, addr_server, reply);
+        c_assert(!r);
+
+        test_client_receive(connection_client, N_DHCP4_MESSAGE_ACK, ackp);
+}
+
+static void test_decline(NDhcp4SConnection *connection_server,
+                         NDhcp4CConnection *connection_client,
+                         NDhcp4Incoming *ack) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request_out = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_decline_new(connection_client, &request_out, ack, "No thanks.");
+        c_assert(!r);
+
+        r = n_dhcp4_c_connection_start_request(connection_client, request_out, 0);
+        c_assert(!r);
+        request_out = NULL;
+
+        test_server_receive(connection_server, N_DHCP4_MESSAGE_DECLINE, NULL);
+}
+
+
+static void test_renew(NDhcp4SConnection *connection_server,
+                       NDhcp4CConnection *connection_client,
+                       const struct in_addr *addr_server,
+                       const struct in_addr *addr_client) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request_out = NULL;
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *request_in = NULL;
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *reply = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_renew_new(connection_client, &request_out);
+        c_assert(!r);
+
+        r = n_dhcp4_c_connection_start_request(connection_client, request_out, 0);
+        c_assert(!r);
+        request_out = NULL;
+
+        test_server_receive(connection_server, N_DHCP4_MESSAGE_REQUEST, &request_in);
+
+        r = n_dhcp4_s_connection_ack_new(connection_server, &reply, request_in, addr_server, addr_client, 60);
+        c_assert(!r);
+
+        r = n_dhcp4_s_connection_send_reply(connection_server, addr_server, reply);
+        c_assert(!r);
+
+        test_client_receive(connection_client, N_DHCP4_MESSAGE_ACK, NULL);
+}
+
+static void test_rebind(NDhcp4SConnection *connection_server,
+                        NDhcp4CConnection *connection_client,
+                        const struct in_addr *addr_server,
+                        const struct in_addr *addr_client) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request_out = NULL;
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *request_in = NULL;
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *reply = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_rebind_new(connection_client, &request_out);
+        c_assert(!r);
+
+        r = n_dhcp4_c_connection_start_request(connection_client, request_out, 0);
+        c_assert(!r);
+        request_out = NULL;
+
+        test_server_receive(connection_server, N_DHCP4_MESSAGE_REQUEST, &request_in);
+
+        r = n_dhcp4_s_connection_ack_new(connection_server, &reply, request_in, addr_server, addr_client, 60);
+        c_assert(!r);
+
+        r = n_dhcp4_s_connection_send_reply(connection_server, addr_server, reply);
+        c_assert(!r);
+
+        test_client_receive(connection_client, N_DHCP4_MESSAGE_ACK, NULL);
+}
+
+static void test_release(NDhcp4SConnection *connection_server,
+                         NDhcp4CConnection *connection_client,
+                         const struct in_addr *addr_server,
+                         const struct in_addr *addr_client) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request_out = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_release_new(connection_client, &request_out, "Shutting down!");
+        c_assert(!r);
+
+        r = n_dhcp4_c_connection_start_request(connection_client, request_out, 0);
+        c_assert(!r);
+        request_out = NULL;
+
+        test_server_receive(connection_server, N_DHCP4_MESSAGE_RELEASE, NULL);
+}
+
+static void test_connection(void) {
+        const struct in_addr addr_server = (struct in_addr){ htonl(10 << 24 | 1) };
+        const struct in_addr addr_client = (struct in_addr){ htonl(10 << 24 | 2) };
+        _c_cleanup_(netns_closep) int ns_server = -1, ns_client = -1;
+        _c_cleanup_(link_deinit) Link link_server = LINK_NULL(link_server);
+        _c_cleanup_(link_deinit) Link link_client = LINK_NULL(link_client);
+        _c_cleanup_(c_closep) int efd_client = -1;
+        int r;
+
+        /* setup */
+
+        netns_new(&ns_server);
+        netns_new(&ns_client);
+
+        link_new_veth(&link_server, &link_client, ns_server, ns_client);
+        link_add_ip4(&link_server, &addr_server, 8);
+
+        efd_client = epoll_create1(EPOLL_CLOEXEC);
+        c_assert(efd_client >= 0);
+
+        /* test connections */
+        {
+                _c_cleanup_(n_dhcp4_client_config_freep) NDhcp4ClientConfig *client_config = NULL;
+                _c_cleanup_(n_dhcp4_client_probe_config_freep) NDhcp4ClientProbeConfig *probe_config = NULL;
+                NDhcp4SConnection connection_server = N_DHCP4_S_CONNECTION_NULL(connection_server);
+                NDhcp4SConnectionIp connection_server_ip = N_DHCP4_S_CONNECTION_IP_NULL(connection_server_ip);
+                NDhcp4CConnection connection_client = N_DHCP4_C_CONNECTION_NULL(connection_client);
+                _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *offer = NULL;
+                _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *ack = NULL;
+                NDhcp4LogQueue log_queue = N_DHCP4_LOG_QUEUE_NULL_DEFUNCT();
+
+                test_s_connection_init(ns_server, &connection_server, link_server.ifindex);
+                n_dhcp4_s_connection_ip_init(&connection_server_ip, addr_server);
+                n_dhcp4_s_connection_ip_link(&connection_server_ip, &connection_server);
+
+                r = n_dhcp4_client_config_new(&client_config);
+                c_assert(!r);
+
+                n_dhcp4_client_config_set_ifindex(client_config, link_client.ifindex);
+                n_dhcp4_client_config_set_transport(client_config, N_DHCP4_TRANSPORT_ETHERNET);
+                n_dhcp4_client_config_set_request_broadcast(client_config, false);
+                n_dhcp4_client_config_set_mac(client_config, link_client.mac.ether_addr_octet, ETH_ALEN);
+                n_dhcp4_client_config_set_broadcast_mac(client_config,
+                                                        (const uint8_t[]){
+                                                                0xff, 0xff, 0xff,
+                                                                0xff, 0xff, 0xff,
+                                                        },
+                                                        ETH_ALEN);
+                r = n_dhcp4_client_config_set_client_id(client_config,
+                                                        (void *)"client-id",
+                                                        strlen("client-id"));
+                c_assert(!r);
+
+                r = n_dhcp4_client_probe_config_new(&probe_config);
+                c_assert(!r);
+
+                r = n_dhcp4_c_connection_init(&connection_client,
+                                              client_config,
+                                              probe_config,
+                                              &log_queue,
+                                              efd_client);
+                c_assert(!r);
+                test_c_connection_listen(ns_client, &connection_client);
+
+                test_discover(&connection_server, &connection_client, &addr_server, &addr_client, &offer);
+                test_select(&connection_server, &connection_client, offer, &addr_server, &addr_client);
+                test_reboot(&connection_server, &connection_client, &addr_server, &addr_client, &ack);
+                test_rebind(&connection_server, &connection_client, &addr_server, &addr_client);
+                test_decline(&connection_server, &connection_client, ack);
+
+                link_add_ip4(&link_client, &addr_client, 8);
+                test_c_connection_connect(ns_client, &connection_client, &addr_client, &addr_server);
+
+                test_renew(&connection_server, &connection_client, &addr_server, &addr_client);
+                test_release(&connection_server, &connection_client, &addr_server, &addr_client);
+
+                n_dhcp4_c_connection_deinit(&connection_client);
+                n_dhcp4_s_connection_ip_unlink(&connection_server_ip);
+                n_dhcp4_s_connection_ip_deinit(&connection_server_ip);
+                n_dhcp4_s_connection_deinit(&connection_server);
+        }
+
+        /* teardown */
+
+        link_del_ip4(&link_client, &addr_client, 8);
+        link_del_ip4(&link_server, &addr_server, 8);
+}
+
+int main(int argc, char **argv) {
+        test_setup();
+
+        test_connection();
+
+        return 0;
+}
diff --git a/src/n-dhcp4/src/test-message.c b/src/n-dhcp4/src/test-message.c
new file mode 100644
index 00000000..6d4200f7
--- /dev/null
+++ b/src/n-dhcp4/src/test-message.c
@@ -0,0 +1,159 @@
+/*
+ * Tests for DHCP4 Message Handling
+ */
+
+#undef NDEBUG
+#include <assert.h>
+#include <c-stdaux.h>
+#include <endian.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "n-dhcp4-private.h"
+
+static void test_outgoing(void) {
+        NDhcp4Outgoing *outgoing;
+        int r;
+
+        /* verify basic NEW/FREE */
+
+        outgoing = NULL;
+        r = n_dhcp4_outgoing_new(&outgoing, 0, 0);
+        c_assert(!r);
+        c_assert(outgoing);
+
+        outgoing = n_dhcp4_outgoing_free(outgoing);
+        c_assert(!outgoing);
+}
+
+static void test_incoming(void) {
+        NDhcp4Incoming *incoming;
+        struct {
+                NDhcp4Header header;
+                uint8_t sname[64];
+                uint8_t file[128];
+                uint32_t magic;
+                uint8_t options[1024];
+        } m;
+        uint8_t *v;
+        size_t l;
+        int r;
+
+        /* verify that messages must be at least the size of the header */
+
+        r = n_dhcp4_incoming_new(&incoming, NULL, 0);
+        c_assert(r == N_DHCP4_E_MALFORMED);
+
+        r = n_dhcp4_incoming_new(&incoming, NULL, sizeof(m.header) + 64 + 128 + 3);
+        c_assert(r == N_DHCP4_E_MALFORMED);
+
+        /* verify that magic must be set */
+
+        memset(&m, 0, sizeof(m));
+        r = n_dhcp4_incoming_new(&incoming, &m, sizeof(m));
+        c_assert(r == N_DHCP4_E_MALFORMED);
+
+        /* verify basic NEW/FREE */
+
+        memset(&m, 0, sizeof(m));
+        m.magic = htobe32(N_DHCP4_MESSAGE_MAGIC);
+        incoming = NULL;
+        r = n_dhcp4_incoming_new(&incoming, &m, sizeof(m.header) + 64 + 128 + 4);
+        c_assert(!r);
+        c_assert(incoming);
+
+        incoming = n_dhcp4_incoming_free(incoming);
+        c_assert(!incoming);
+
+        /* verify that PAD is properly handled */
+
+        r = n_dhcp4_incoming_new(&incoming, &m, sizeof(m));
+        c_assert(!r);
+        incoming = n_dhcp4_incoming_free(incoming);
+
+        /* verify that SNAME/FILE are only looked at if OVERLOAD is set */
+
+        m.sname[0] = 1;
+        m.sname[1] = 0;
+        m.file[0] = 2;
+        m.file[1] = 0;
+
+        r = n_dhcp4_incoming_new(&incoming, &m, sizeof(m));
+        c_assert(!r);
+        r = n_dhcp4_incoming_query(incoming, 1, NULL, NULL);
+        c_assert(r == N_DHCP4_E_UNSET);
+        r = n_dhcp4_incoming_query(incoming, 2, NULL, NULL);
+        c_assert(r == N_DHCP4_E_UNSET);
+        incoming = n_dhcp4_incoming_free(incoming);
+
+        m.options[0] = N_DHCP4_OPTION_OVERLOAD;
+        m.options[1] = 1;
+        m.options[2] = 0;
+
+        r = n_dhcp4_incoming_new(&incoming, &m, sizeof(m));
+        c_assert(!r);
+        r = n_dhcp4_incoming_query(incoming, 1, NULL, NULL);
+        c_assert(r == N_DHCP4_E_UNSET);
+        r = n_dhcp4_incoming_query(incoming, 2, NULL, NULL);
+        c_assert(r == N_DHCP4_E_UNSET);
+        incoming = n_dhcp4_incoming_free(incoming);
+
+        m.options[0] = N_DHCP4_OPTION_OVERLOAD;
+        m.options[1] = 1;
+        m.options[2] = N_DHCP4_OVERLOAD_SNAME;
+
+        r = n_dhcp4_incoming_new(&incoming, &m, sizeof(m));
+        c_assert(!r);
+        r = n_dhcp4_incoming_query(incoming, 1, NULL, NULL);
+        c_assert(r == 0);
+        r = n_dhcp4_incoming_query(incoming, 2, NULL, NULL);
+        c_assert(r == N_DHCP4_E_UNSET);
+        incoming = n_dhcp4_incoming_free(incoming);
+
+        m.options[0] = N_DHCP4_OPTION_OVERLOAD;
+        m.options[1] = 1;
+        m.options[2] = N_DHCP4_OVERLOAD_FILE;
+
+        r = n_dhcp4_incoming_new(&incoming, &m, sizeof(m));
+        c_assert(!r);
+        r = n_dhcp4_incoming_query(incoming, 1, NULL, NULL);
+        c_assert(r == N_DHCP4_E_UNSET);
+        r = n_dhcp4_incoming_query(incoming, 2, NULL, NULL);
+        c_assert(r == 0);
+        incoming = n_dhcp4_incoming_free(incoming);
+
+        m.options[0] = N_DHCP4_OPTION_OVERLOAD;
+        m.options[1] = 1;
+        m.options[2] = N_DHCP4_OVERLOAD_FILE | N_DHCP4_OVERLOAD_SNAME;
+
+        r = n_dhcp4_incoming_new(&incoming, &m, sizeof(m));
+        c_assert(!r);
+        r = n_dhcp4_incoming_query(incoming, 1, NULL, NULL);
+        c_assert(r == 0);
+        r = n_dhcp4_incoming_query(incoming, 2, NULL, NULL);
+        c_assert(r == 0);
+        incoming = n_dhcp4_incoming_free(incoming);
+
+        /* verify basic concatenation */
+
+        m.options[3] = 1;
+        m.options[4] = 1;
+        m.options[5] = 0xef;
+        m.sname[1] = 1;
+        m.sname[2] = 0xcf;
+
+        r = n_dhcp4_incoming_new(&incoming, &m, sizeof(m));
+        c_assert(!r);
+        r = n_dhcp4_incoming_query(incoming, 1, &v, &l);
+        c_assert(r == 0);
+        c_assert(l == 2);
+        c_assert(v[0] == 0xef && v[1] == 0xcf);
+        incoming = n_dhcp4_incoming_free(incoming);
+}
+
+int main(int argc, char **argv) {
+        test_outgoing();
+        test_incoming();
+        return 0;
+}
diff --git a/src/n-dhcp4/src/test-run-client.c b/src/n-dhcp4/src/test-run-client.c
new file mode 100644
index 00000000..259a9608
--- /dev/null
+++ b/src/n-dhcp4/src/test-run-client.c
@@ -0,0 +1,713 @@
+/*
+ * DHCP Client Runner
+ *
+ * This test implements a DHCP client. It takes parameters via the command-line
+ * and runs a DHCP client. It is mainly meant for testing, as such it allows
+ * tweaking that an exported DHCP client should not provide.
+ */
+
+#include <assert.h>
+#include <c-stdaux.h>
+#include <errno.h>
+#include <getopt.h>
+#include <net/if.h>
+#include <netinet/ether.h>
+#include <netinet/in.h>
+#include <poll.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "n-dhcp4.h"
+#include "n-dhcp4-private.h"
+#include "test.h"
+
+typedef struct Manager Manager;
+
+enum {
+        _MAIN_SUCCESS,
+        MAIN_EXIT,
+        MAIN_FAILED,
+};
+
+struct Manager {
+        NDhcp4Client *client;
+        NDhcp4ClientProbe *probe;
+};
+
+#define MANAGER_NULL(_x) {}
+
+static struct ether_addr        main_arg_broadcast_mac = {};
+static bool                     main_arg_broadcast_mac_set = false;
+static uint8_t*                 main_arg_client_id = NULL;
+static size_t                   main_arg_n_client_id = 0;
+static int                      main_arg_ifindex = 0;
+static struct in_addr           main_arg_requested_ip = { INADDR_ANY };
+static long long int            main_arg_requested_lifetime = -1;
+static uint8_t                  main_arg_requested_parameters[UINT8_MAX] = {};
+static size_t                   main_arg_n_requested_parameters = 0;
+static struct ether_addr        main_arg_mac = {};
+static bool                     main_arg_mac_set = false;
+static bool                     main_arg_request_broadcast = false;
+static bool                     main_arg_test = false;
+
+static Manager *manager_free(Manager *manager) {
+        if (!manager)
+                return NULL;
+
+        n_dhcp4_client_probe_free(manager->probe);
+        n_dhcp4_client_unref(manager->client);
+        free(manager);
+
+        return NULL;
+}
+
+static void manager_freep(Manager **manager) {
+        manager_free(*manager);
+}
+
+static int manager_new(Manager **managerp) {
+        _c_cleanup_(n_dhcp4_client_config_freep) NDhcp4ClientConfig *config = NULL;
+        _c_cleanup_(manager_freep) Manager *manager = NULL;
+        int r;
+
+        manager = malloc(sizeof(*manager));
+        if (!manager)
+                return -ENOMEM;
+
+        *manager = (Manager)MANAGER_NULL(*manager);
+
+        r = n_dhcp4_client_config_new(&config);
+        if (r)
+                return r;
+
+        n_dhcp4_client_config_set_broadcast_mac(config,
+                                                &main_arg_broadcast_mac.ether_addr_octet[0],
+                                                sizeof(main_arg_broadcast_mac.ether_addr_octet));
+        n_dhcp4_client_config_set_mac(config,
+                                      &main_arg_mac.ether_addr_octet[0],
+                                      sizeof(main_arg_mac.ether_addr_octet));
+        n_dhcp4_client_config_set_client_id(config,
+                                            main_arg_client_id,
+                                            main_arg_n_client_id);
+        n_dhcp4_client_config_set_ifindex(config, main_arg_ifindex);
+        n_dhcp4_client_config_set_request_broadcast(config, main_arg_request_broadcast);
+        n_dhcp4_client_config_set_transport(config, N_DHCP4_TRANSPORT_ETHERNET);
+
+        r = n_dhcp4_client_new(&manager->client, config);
+        if (r)
+                return r;
+
+        *managerp = manager;
+        manager = NULL;
+        return 0;
+}
+
+static int manager_lease_get_dns(NDhcp4ClientLease *lease, struct in_addr *dns) {
+        uint8_t *data;
+        size_t n_data;
+        int r;
+
+        r = n_dhcp4_client_lease_query(lease, N_DHCP4_OPTION_DOMAIN_NAME_SERVER, &data, &n_data);
+        if (r)
+                return r;
+
+        if (n_data < sizeof(dns->s_addr))
+                return N_DHCP4_E_MALFORMED;
+
+        memcpy(&dns->s_addr, data, sizeof(dns->s_addr));
+
+        return 0;
+}
+
+static int manager_lease_get_router(NDhcp4ClientLease *lease, struct in_addr *router) {
+        uint8_t *data;
+        size_t n_data;
+        int r;
+
+        r = n_dhcp4_client_lease_query(lease, N_DHCP4_OPTION_ROUTER, &data, &n_data);
+        if (r)
+                return r;
+
+        if (n_data < sizeof(router->s_addr))
+                return N_DHCP4_E_MALFORMED;
+
+        memcpy(&router->s_addr, data, sizeof(router->s_addr));
+
+        return 0;
+}
+
+static int manager_lease_get_subnetmask(NDhcp4ClientLease *lease, struct in_addr *mask) {
+        uint8_t *data;
+        size_t n_data;
+        int r;
+
+        r = n_dhcp4_client_lease_query(lease, N_DHCP4_OPTION_SUBNET_MASK, &data, &n_data);
+        if (r)
+                return r;
+
+        if (n_data != sizeof(mask->s_addr))
+                return N_DHCP4_E_MALFORMED;
+
+        memcpy(&mask->s_addr, data, sizeof(mask->s_addr));
+
+        return 0;
+}
+
+static int manager_lease_get_prefix(NDhcp4ClientLease *lease, unsigned int *prefixp) {
+        struct in_addr mask = {};
+        unsigned int postfix;
+        int r;
+
+        r = manager_lease_get_subnetmask(lease, &mask);
+        if (r)
+                return r;
+
+        postfix =__builtin_ctz(ntohl(mask.s_addr));
+        c_assert(postfix <= 32);
+
+        if (postfix < 32) {
+                if ((~ntohl(mask.s_addr)) >> postfix != 0)
+                        return N_DHCP4_E_MALFORMED;
+        }
+
+        *prefixp = 32 - postfix;
+        return 0;
+}
+
+static int manager_check(Manager *manager, NDhcp4ClientLease *lease) {
+        int r;
+
+        r = n_dhcp4_client_lease_query(lease, N_DHCP4_OPTION_ROUTER, NULL, NULL);
+        if (r) {
+                fprintf(stderr, "No router\n");
+                return r;
+        }
+
+        r = n_dhcp4_client_lease_query(lease, N_DHCP4_OPTION_SUBNET_MASK, NULL, NULL);
+        if (r) {
+                fprintf(stderr, "No subnet mask\n");
+                return r;
+        }
+
+        return r;
+}
+
+static int manager_add(Manager *manager, NDhcp4ClientLease *lease) {
+        char *p, ifname[IF_NAMESIZE + 1] = {};
+        struct in_addr router = {}, yiaddr = {}, dns = {};
+        unsigned int prefix;
+        uint64_t lifetime;
+        int r;
+
+        n_dhcp4_client_lease_get_yiaddr(lease, &yiaddr);
+        n_dhcp4_client_lease_get_lifetime(lease, &lifetime);
+
+        r = manager_lease_get_router(lease, &router);
+        if (r)
+                return r;
+
+        r = manager_lease_get_prefix(lease, &prefix);
+        if (r)
+                return r;
+
+        p = if_indextoname(main_arg_ifindex, ifname);
+        c_assert(p);
+
+        if (lifetime == UINT64_MAX) {
+                r = asprintf(&p, "ip addr add %s/%u dev %s preferred_lft forever valid_lft forever", inet_ntoa(yiaddr), prefix, ifname);
+                c_assert(r >= 0);
+        } else {
+                r = asprintf(&p, "ip addr add %s/%u dev %s preferred_lft %llu valid_lft %llu", inet_ntoa(yiaddr), prefix, ifname, lifetime / 1000000000ULL, lifetime / 1000000000ULL);
+                c_assert(r >= 0);
+        }
+        r = system(p);
+        c_assert(r == 0);
+        free(p);
+
+        r = asprintf(&p, "ip route add %s/32 dev %s", inet_ntoa(router), ifname);
+        c_assert(r >= 0);
+        r = system(p);
+        c_assert(r == 0);
+        free(p);
+
+        r = asprintf(&p, "ip route add default via %s dev %s", inet_ntoa(router), ifname);
+        c_assert(r >= 0);
+        r = system(p);
+        c_assert(r == 0);
+        free(p);
+
+        r = manager_lease_get_dns(lease, &dns);
+        if (r) {
+                if (r != N_DHCP4_E_UNSET)
+                        return r;
+        } else {
+                fprintf(stderr, "DNS: %s\n", inet_ntoa(dns));
+        }
+
+        return 0;
+}
+
+static int manager_dispatch(Manager *manager) {
+        NDhcp4ClientEvent *event;
+        int r;
+
+        r = n_dhcp4_client_dispatch(manager->client);
+        if (r) {
+                if (r != N_DHCP4_E_PREEMPTED) {
+                        /*
+                         * We are level-triggered, so we do not need to react
+                         * to preemption. We simply continue the mainloop.
+                         */
+                        return r;
+                }
+        }
+
+        for (;;) {
+                r = n_dhcp4_client_pop_event(manager->client, &event);
+                if (r)
+                        return r;
+
+                if (!event)
+                        break;
+
+                switch (event->event) {
+                case N_DHCP4_CLIENT_EVENT_DOWN:
+                        fprintf(stderr, "DOWN\n");
+
+                        break;
+
+                case N_DHCP4_CLIENT_EVENT_OFFER:
+                        fprintf(stderr, "OFFER\n");
+
+                        r = manager_check(manager, event->granted.lease);
+                        if (r) {
+                                if (r == N_DHCP4_E_UNSET) {
+                                        fprintf(stderr, "Missing mandatory option, ignoring lease.\n");
+                                } else {
+                                        return r;
+                                }
+                        } else {
+                                r = n_dhcp4_client_lease_select(event->offer.lease);
+                                if (r)
+                                        return r;
+                        }
+
+                        break;
+
+                case N_DHCP4_CLIENT_EVENT_GRANTED:
+                        fprintf(stderr, "GRANTED\n");
+
+                        r = manager_add(manager, event->granted.lease);
+                        if (r) {
+                                if (r == N_DHCP4_E_UNSET) {
+                                        fprintf(stderr, "Missing mandatory option, declining lease.\n");
+
+                                        r = n_dhcp4_client_lease_decline(event->granted.lease, "Missing mandatory option.");
+                                        if (r)
+                                                return r;
+                                } else {
+                                        return r;
+                                }
+                        } else {
+                                r = n_dhcp4_client_lease_accept(event->granted.lease);
+                                if (r)
+                                        return r;
+                        }
+
+                        break;
+
+                case N_DHCP4_CLIENT_EVENT_RETRACTED:
+                        fprintf(stderr, "RETRACTED\n");
+
+                        break;
+
+                case N_DHCP4_CLIENT_EVENT_EXTENDED:
+                        fprintf(stderr, "EXTENDED\n");
+
+                        break;
+
+                case N_DHCP4_CLIENT_EVENT_EXPIRED:
+                        fprintf(stderr, "EXPIRED\n");
+
+                        break;
+
+                case N_DHCP4_CLIENT_EVENT_CANCELLED:
+                        fprintf(stderr, "CANCELLED\n");
+
+                        break;
+
+                default:
+                        fprintf(stderr, "Unexpected event: %u\n", event->event);
+
+                        break;
+                }
+        }
+
+        return 0;
+}
+
+static int manager_run(Manager *manager) {
+        _c_cleanup_(n_dhcp4_client_probe_config_freep) NDhcp4ClientProbeConfig *config = NULL;
+        int r;
+
+        r = n_dhcp4_client_probe_config_new(&config);
+        if (r)
+                return r;
+
+        /*
+         * Let's speed up our tests, while still making sure the code-path
+         * for the deferrment is actually tested (so don't set it to zero).
+         */
+        n_dhcp4_client_probe_config_set_start_delay(config, 10);
+
+        n_dhcp4_client_probe_config_set_requested_ip(config, main_arg_requested_ip);
+
+        if (main_arg_n_requested_parameters > 0) {
+                for (unsigned int i = 0; i < main_arg_n_requested_parameters; ++i)
+                        n_dhcp4_client_probe_config_request_option(config, main_arg_requested_parameters[i]);
+        } else {
+                n_dhcp4_client_probe_config_request_option(config, N_DHCP4_OPTION_ROUTER);
+                n_dhcp4_client_probe_config_request_option(config, N_DHCP4_OPTION_SUBNET_MASK);
+                n_dhcp4_client_probe_config_request_option(config, N_DHCP4_OPTION_DOMAIN_NAME_SERVER);
+        }
+
+        if (main_arg_requested_lifetime >= 0) {
+                uint32_t lifetime = ntohl(main_arg_requested_lifetime);
+
+                r = n_dhcp4_client_probe_config_append_option(config, N_DHCP4_OPTION_IP_ADDRESS_LEASE_TIME, &lifetime, sizeof(lifetime));
+                if (r)
+                        return r;
+        }
+
+        r = n_dhcp4_client_probe(manager->client, &manager->probe, config);
+        if (r)
+                return r;
+
+        /*
+         * The test-suite runs this with the --test argument. So far, we do not
+         * perform any fancy runtime tests, but simply exit the main-loop
+         * immediately. We can add more elaborate tests in the future.
+         */
+        if (main_arg_test)
+                return 0;
+
+        for (;;) {
+                struct pollfd pfds[] = {
+                        { .fd = -1, .events = POLLIN },
+                };
+                size_t i;
+                int n;
+
+                n_dhcp4_client_get_fd(manager->client, &pfds[0].fd);
+
+                n = poll(pfds, sizeof(pfds) / sizeof(*pfds), -1);
+                if (n < 0)
+                        return -errno;
+
+                for (i = 0; i < (size_t)n; ++i) {
+                        if (pfds[i].revents & ~POLLIN)
+                                return -ENOTRECOVERABLE;
+
+                        if (!(pfds[i].revents & POLLIN))
+                                continue;
+
+                        r = manager_dispatch(manager);
+                        if (r)
+                                return r;
+                }
+        }
+
+        return 0;
+}
+
+static int run(void) {
+        _c_cleanup_(manager_freep) Manager *manager = NULL;
+        int r;
+
+        r = manager_new(&manager);
+        if (r)
+                return r;
+
+        return manager_run(manager);
+}
+
+static void print_help(void) {
+        printf("%s [GLOBALS...] ...\n\n"
+               "DHCP Test Client\n\n"
+               "  -h --help                            Show this help\n"
+               "     --test                            Run as part of the test suite\n"
+               "     --ifindex IDX                     Index of interface to run on\n"
+               "     --mac HEX                         Hardware address to use\n"
+               "     --broadcast-mac HEX               Broadcast hardware address to use\n"
+               "     --requested-ip IP                 Requested IP address\n"
+               "     --requested-lifetime SECS         Requested lease lifetime in seconds\n"
+               "     --requested-parameters P1,P2,...  Requested parameters\n"
+               "     --client-id HEX                   Client Identifier to use\n"
+               , program_invocation_short_name);
+}
+
+static int setup_test(void) {
+        test_setup();
+
+        /* --broadcast-mac */
+        {
+                main_arg_broadcast_mac_set = true;
+        }
+
+        /* --ifindex */
+        {
+                main_arg_ifindex = 1;
+        }
+
+        /* --mac */
+        {
+                main_arg_mac_set = true;
+        }
+
+        return 0;
+}
+
+static int parse_hexstr(const char *in, uint8_t **outp, size_t *n_outp) {
+        _c_cleanup_(c_freep) uint8_t *out = NULL;
+        size_t i, n_in, n_out;
+
+        n_in = strlen(in);
+        n_out = (n_in + 1) / 2;
+
+        out = malloc(n_out);
+        if (!out)
+                return -ENOMEM;
+
+        for (i = 0; i < n_in; ++i) {
+                uint8_t v = 0;
+
+                switch (in[i]) {
+                case '0'...'9':
+                        v = in[i] - '0';
+                        break;
+                case 'a'...'f':
+                        v = in[i] - 'a' + 0xa;
+                        break;
+                case 'A'...'F':
+                        v = in[i] - 'A' + 0xa;
+                        break;
+                }
+
+                if (i % 2) {
+                        out[i / 2] <<= 4;
+                        out[i / 2] |= v;
+                } else {
+                        out[i / 2] = v;
+                }
+        }
+
+        *outp = out;
+        out = NULL;
+        *n_outp = n_out;
+        return 0;
+}
+
+static int parse_argv(int argc, char **argv) {
+        enum {
+                _ARG_0 = 0x100,
+                ARG_BROADCAST_MAC,
+                ARG_CLIENT_ID,
+                ARG_IFINDEX,
+                ARG_MAC,
+                ARG_REQUEST_BROADCAST,
+                ARG_REQUESTED_IP,
+                ARG_REQUESTED_LIFETIME,
+                ARG_REQUESTED_PARAMETERS,
+                ARG_TEST,
+        };
+        static const struct option options[] = {
+                { "help",                       no_argument,            NULL,   'h'                             },
+                { "broadcast-mac",              required_argument,      NULL,   ARG_BROADCAST_MAC               },
+                { "client-id",                  required_argument,      NULL,   ARG_CLIENT_ID                   },
+                { "ifindex",                    required_argument,      NULL,   ARG_IFINDEX                     },
+                { "mac",                        required_argument,      NULL,   ARG_MAC                         },
+                { "request-broadcast",          no_argument,            NULL,   ARG_REQUEST_BROADCAST           },
+                { "requested-ip",               required_argument,      NULL,   ARG_REQUESTED_IP                },
+                { "requested-lifetime",         required_argument,      NULL,   ARG_REQUESTED_LIFETIME          },
+                { "requested-parameters",       required_argument,      NULL,   ARG_REQUESTED_PARAMETERS        },
+                { "test",                       no_argument,            NULL,   ARG_TEST                        },
+                {}
+        };
+        struct ether_addr *addr;
+        long long int lli;
+        size_t n;
+        void *t;
+        int r, c;
+
+        /*
+         * Most of the argument-parsers are short-and-dirty hacks to make the
+         * conversions work. This is sufficient for a test-client, but needs
+         * proper error-checking if done outside of tests.
+         */
+
+        while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) {
+                switch (c) {
+                case 'h':
+                        print_help();
+                        return MAIN_EXIT;
+
+                case ARG_BROADCAST_MAC:
+                        addr = ether_aton_r(optarg, &main_arg_broadcast_mac);
+                        if (!addr) {
+                                fprintf(stderr,
+                                        "%s: invalid broadcast mac address -- '%s'\n",
+                                        program_invocation_name,
+                                        optarg);
+                                return MAIN_FAILED;
+                        }
+
+                        main_arg_broadcast_mac_set = true;
+                        break;
+
+                case ARG_CLIENT_ID:
+                        r = parse_hexstr(optarg, (uint8_t **)&t, &n);
+                        if (r)
+                                return r;
+
+                        free(main_arg_client_id);
+                        main_arg_client_id = t;
+                        main_arg_n_client_id = n;
+                        break;
+
+                case ARG_IFINDEX:
+                        main_arg_ifindex = atoi(optarg);
+                        break;
+
+                case ARG_MAC:
+                        addr = ether_aton_r(optarg, &main_arg_mac);
+                        if (!addr) {
+                                fprintf(stderr,
+                                        "%s: invalid mac address -- '%s'\n",
+                                        program_invocation_name,
+                                        optarg);
+                                return MAIN_FAILED;
+                        }
+
+                        main_arg_mac_set = true;
+                        break;
+
+                case ARG_REQUEST_BROADCAST:
+                        main_arg_request_broadcast = true;
+                        break;
+
+                case ARG_REQUESTED_IP:
+                        r = inet_pton(AF_INET, optarg, &main_arg_requested_ip);
+                        if (r != 1) {
+                                fprintf(stderr,
+                                        "%s: invalid requested IP -- '%s'\n",
+                                        program_invocation_name,
+                                        optarg);
+                                return MAIN_FAILED;
+                        }
+                        break;
+
+                case ARG_REQUESTED_LIFETIME:
+                        lli = atoll(optarg);
+                        if (lli < 0 || lli > UINT32_MAX) {
+                                fprintf(stderr,
+                                        "%s: invalid requested lifetime -- '%s'\n",
+                                        program_invocation_name,
+                                        optarg);
+                                return MAIN_FAILED;
+                        }
+                        main_arg_requested_lifetime = lli;
+                        break;
+
+                case ARG_REQUESTED_PARAMETERS:
+                        for (const char *param = optarg; param; param = strchr(param, ',') ? strchr(param, ',')  + 1 : NULL) {
+                                c_assert(main_arg_n_requested_parameters <= UINT8_MAX);
+
+                                lli = atoll(param);
+                                if (lli < 0 || lli > UINT8_MAX) {
+                                        fprintf(stderr,
+                                                "%s: invalid requested parameters -- '%s'\n",
+                                                program_invocation_name,
+                                                optarg);
+                                        return MAIN_FAILED;
+                                }
+                                main_arg_requested_parameters[main_arg_n_requested_parameters++] = lli;
+                        }
+                        break;
+
+                case ARG_TEST:
+                        r = setup_test();
+                        if (r)
+                                return r;
+
+                        main_arg_test = true;
+                        break;
+
+                case '?':
+                        /* getopt_long() prints warning */
+                        return MAIN_FAILED;
+
+                default:
+                        return -ENOTRECOVERABLE;
+                }
+        }
+
+        if (optind != argc) {
+                fprintf(stderr,
+                        "%s: invalid arguments -- '%s'\n",
+                        program_invocation_name,
+                        argv[optind]);
+                return MAIN_FAILED;
+        }
+
+        if (!main_arg_broadcast_mac_set ||
+            !main_arg_ifindex ||
+            !main_arg_mac_set) {
+                fprintf(stderr,
+                        "%s: required arguments: broadcast-mac, ifindex, mac\n",
+                        program_invocation_name);
+                return MAIN_FAILED;
+        }
+
+        return 0;
+}
+
+int main(int argc, char **argv) {
+        int r;
+
+        /* --client-id */
+        {
+                uint8_t *b;
+                size_t n;
+
+                n = strlen("client-id");
+                b = malloc(n);
+                c_assert(b);
+                memcpy(b, "client-id", n);
+
+                free(main_arg_client_id);
+                main_arg_client_id = b;
+                main_arg_n_client_id = n;
+        }
+
+        r = parse_argv(argc, argv);
+        if (r)
+                goto exit;
+
+        r = run();
+
+exit:
+        if (r == MAIN_EXIT) {
+                r = 0;
+        } else if (r < 0) {
+                errno = -r;
+                fprintf(stderr, "Failed with system errno %d: %m\n", r);
+                r = 127;
+        } else if (r > 0) {
+                fprintf(stderr, "Failed with internal error %d\n", r);
+        }
+
+        free(main_arg_client_id);
+
+        return r;
+}
diff --git a/src/n-dhcp4/src/test-socket.c b/src/n-dhcp4/src/test-socket.c
new file mode 100644
index 00000000..2b9303af
--- /dev/null
+++ b/src/n-dhcp4/src/test-socket.c
@@ -0,0 +1,317 @@
+/*
+ * Tests for DHCP4 Socket Helpers
+ */
+
+#undef NDEBUG
+#include <assert.h>
+#include <c-stdaux.h>
+#include <endian.h>
+#include <errno.h>
+#include <poll.h>
+#include <linux/if_packet.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "n-dhcp4-private.h"
+#include "test.h"
+#include "util/link.h"
+#include "util/netns.h"
+#include "util/packet.h"
+
+static void test_poll(int sk) {
+        int r;
+
+        r = poll(&(struct pollfd){ .fd = sk, .events = POLLIN }, 1, -1);
+        c_assert(r == 1);
+}
+
+static void test_client_packet_socket_new(Link *link, int *skp) {
+        int r, oldns;
+
+        netns_get(&oldns);
+        netns_set(link->netns);
+
+        r = n_dhcp4_c_socket_packet_new(skp, link->ifindex);
+        c_assert(r >= 0);
+
+        netns_set(oldns);
+}
+
+static void test_client_udp_socket_new(Link *link,
+                                       int *skp,
+                                       const struct in_addr *addr_client,
+                                       const struct in_addr *addr_server) {
+        int r, oldns;
+
+        netns_get(&oldns);
+        netns_set(link->netns);
+
+        r = n_dhcp4_c_socket_udp_new(skp, link->ifindex, addr_client, addr_server, N_DHCP4_DSCP_DEFAULT);
+        c_assert(r >= 0);
+
+        netns_set(oldns);
+}
+
+static void test_server_packet_socket_new(Link *link, int *skp) {
+        int r, oldns;
+
+        netns_get(&oldns);
+        netns_set(link->netns);
+
+        r = n_dhcp4_s_socket_packet_new(skp);
+        c_assert(r >= 0);
+
+        netns_set(oldns);
+}
+
+static void test_server_udp_socket_new(Link *link, int *skp) {
+        int r, oldns;
+
+        netns_get(&oldns);
+        netns_set(link->netns);
+
+        r = n_dhcp4_s_socket_udp_new(skp, link->ifindex);
+        c_assert(r >= 0);
+
+        netns_set(oldns);
+}
+
+static void test_client_server_packet(Link *link_server, Link *link_client) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *outgoing = NULL;
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *incoming = NULL;
+        _c_cleanup_(c_closep) int sk_server = -1, sk_client = -1;
+        uint8_t buf[UINT16_MAX];
+        struct sockaddr_in dest = {};
+        int r;
+
+        test_server_udp_socket_new(link_server, &sk_server);
+        test_client_packet_socket_new(link_client, &sk_client);
+
+        r = n_dhcp4_outgoing_new(&outgoing, 0, 0);
+        c_assert(!r);
+        n_dhcp4_outgoing_get_header(outgoing)->op = N_DHCP4_OP_BOOTREQUEST;
+
+        r = n_dhcp4_c_socket_packet_send(sk_client,
+                                         link_client->ifindex,
+                                         (const unsigned char[]){0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
+                                         ETH_ALEN,
+                                         -1,
+                                         outgoing);
+        c_assert(!r);
+
+        test_poll(sk_server);
+
+        r = n_dhcp4_s_socket_udp_recv(sk_server, buf, sizeof(buf), &incoming, &dest);
+        c_assert(!r);
+        c_assert(incoming);
+        c_assert(dest.sin_family == AF_INET);
+        c_assert(dest.sin_port == htons(N_DHCP4_NETWORK_SERVER_PORT));
+        c_assert(dest.sin_addr.s_addr == INADDR_BROADCAST);
+}
+
+static void test_client_server_udp(Link *link_server, Link *link_client) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *outgoing = NULL;
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *incoming = NULL;
+        _c_cleanup_(c_closep) int sk_server = -1, sk_client = -1;
+        struct in_addr addr_server = (struct in_addr){ htonl(10 << 24 | 1) };
+        struct in_addr addr_client = (struct in_addr){ htonl(10 << 24 | 2) };
+        uint8_t buf[UINT16_MAX];
+        struct sockaddr_in dest = {};
+        int r;
+
+        /* setup */
+
+        link_add_ip4(link_server, &addr_server, 8);
+        link_add_ip4(link_client, &addr_client, 8);
+
+        /* test communication */
+
+        test_server_udp_socket_new(link_server, &sk_server);
+        test_client_udp_socket_new(link_client, &sk_client, &addr_client, &addr_server);
+
+        r = n_dhcp4_outgoing_new(&outgoing, 0, 0);
+        c_assert(!r);
+        n_dhcp4_outgoing_get_header(outgoing)->op = N_DHCP4_OP_BOOTREQUEST;
+
+        r = n_dhcp4_c_socket_udp_send(sk_client, outgoing);
+        c_assert(!r);
+
+        test_poll(sk_server);
+
+        r = n_dhcp4_s_socket_udp_recv(sk_server, buf, sizeof(buf), &incoming, &dest);
+        c_assert(!r);
+        c_assert(incoming);
+        c_assert(dest.sin_family == AF_INET);
+        c_assert(dest.sin_port == htons(N_DHCP4_NETWORK_SERVER_PORT));
+        c_assert(dest.sin_addr.s_addr == addr_server.s_addr);
+
+        /* teardown */
+
+        link_del_ip4(link_client, &addr_client, 8);
+        link_del_ip4(link_server, &addr_server, 8);
+}
+
+static void test_server_client_packet(Link *link_server, Link *link_client) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *outgoing = NULL;
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *incoming1 = NULL, *incoming2 = NULL;
+        _c_cleanup_(c_closep) int sk_server = -1, sk_client = -1;
+        struct in_addr addr_client = (struct in_addr){ htonl(10 << 24 | 2) };
+        struct in_addr addr_server = (struct in_addr){ htonl(10 << 24 | 1) };
+        uint8_t buf[UINT16_MAX];
+        int r;
+
+        /* setup */
+
+        link_add_ip4(link_server, &addr_server, 8);
+
+        /* test communication */
+
+        test_server_packet_socket_new(link_server, &sk_server);
+        test_client_packet_socket_new(link_client, &sk_client);
+
+        r = n_dhcp4_outgoing_new(&outgoing, 0, 0);
+        c_assert(!r);
+        n_dhcp4_outgoing_get_header(outgoing)->op = N_DHCP4_OP_BOOTREPLY;
+
+        r = n_dhcp4_s_socket_packet_send(sk_server,
+                                         link_server->ifindex,
+                                         &addr_server,
+                                         link_client->mac.ether_addr_octet,
+                                         ETH_ALEN,
+                                         &addr_client,
+                                         N_DHCP4_DSCP_DEFAULT,
+                                         outgoing);
+        c_assert(!r);
+        r = n_dhcp4_s_socket_packet_send(sk_server,
+                                         link_server->ifindex,
+                                         &addr_server,
+                                         (const unsigned char[]){
+                                                0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+                                         },
+                                         ETH_ALEN,
+                                         &addr_client,
+                                         N_DHCP4_DSCP_DEFAULT,
+                                         outgoing);
+        c_assert(!r);
+
+        test_poll(sk_client);
+
+        r = n_dhcp4_c_socket_packet_recv(sk_client, buf, sizeof(buf), &incoming1);
+        c_assert(!r);
+        c_assert(incoming1);
+
+        test_poll(sk_client);
+
+        r = n_dhcp4_c_socket_packet_recv(sk_client, buf, sizeof(buf), &incoming2);
+        c_assert(!r);
+        c_assert(incoming2);
+
+        /* teardown */
+
+        link_del_ip4(link_server, &addr_server, 8);
+}
+
+static void test_server_client_udp(Link *link_server, Link *link_client) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *outgoing = NULL;
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *incoming = NULL;
+        _c_cleanup_(c_closep) int sk_server = -1, sk_client = -1;
+        struct in_addr addr_client = (struct in_addr){ htonl(10 << 24 | 2) };
+        struct in_addr addr_server = (struct in_addr){ htonl(10 << 24 | 1) };
+        uint8_t buf[UINT16_MAX];
+        int r;
+
+        /* setup */
+
+        link_add_ip4(link_server, &addr_server, 8);
+        link_add_ip4(link_client, &addr_client, 8);
+
+        /* test communication */
+
+        test_server_udp_socket_new(link_server, &sk_server);
+        test_client_udp_socket_new(link_client, &sk_client, &addr_client, &addr_server);
+
+        r = n_dhcp4_outgoing_new(&outgoing, 0, 0);
+        c_assert(!r);
+        n_dhcp4_outgoing_get_header(outgoing)->op = N_DHCP4_OP_BOOTREPLY;
+
+        r = n_dhcp4_s_socket_udp_send(sk_server,
+                                      &addr_server,
+                                      &addr_client,
+                                      outgoing);
+        c_assert(!r);
+
+        test_poll(sk_client);
+
+        r = n_dhcp4_c_socket_udp_recv(sk_client, buf, sizeof(buf), &incoming);
+        c_assert(!r);
+        c_assert(incoming);
+
+        /* teardown */
+
+        link_del_ip4(link_client, &addr_client, 8);
+        link_del_ip4(link_server, &addr_server, 8);
+}
+
+static void test_sockets(void) {
+        _c_cleanup_(netns_closep) int ns_server = -1, ns_client = -1;
+        _c_cleanup_(link_deinit) Link link_server = LINK_NULL(link_server);
+        _c_cleanup_(link_deinit) Link link_client = LINK_NULL(link_client);
+
+        /* setup */
+
+        netns_new(&ns_server);
+        netns_new(&ns_client);
+        link_new_veth(&link_server, &link_client, ns_server, ns_client);
+
+        /* communication tests */
+
+        test_client_server_packet(&link_server, &link_client);
+        test_client_server_udp(&link_server, &link_client);
+        test_server_client_packet(&link_server, &link_client);
+        test_server_client_udp(&link_server, &link_client);
+}
+
+static void test_multiple_servers(void) {
+        _c_cleanup_(netns_closep) int netns = -1;
+        _c_cleanup_(link_deinit) Link link_server = LINK_NULL(link_server);
+        _c_cleanup_(link_deinit) Link link_client = LINK_NULL(link_client);
+        int r, oldns;
+
+        /* setup */
+
+        netns_new(&netns);
+        link_new_veth(&link_server, &link_client, netns, netns);
+
+        /* test multiple server UDP sockets on the same machine */
+
+        netns_get(&oldns);
+        netns_set(netns);
+        {
+                _c_cleanup_(c_closep) int sk1 = -1, sk2 = -1;
+
+                /*
+                 * DHCP servers have to bind to a fixed port, so you cannot run
+                 * two servers on the same interface. It must be possible to
+                 * run them on separate interfaces, though.
+                 */
+
+                r = n_dhcp4_s_socket_udp_new(&sk1, link_server.ifindex);
+                c_assert(r >= 0);
+
+                r = n_dhcp4_s_socket_udp_new(&sk2, link_server.ifindex);
+                c_assert(r == -EADDRINUSE);
+
+                r = n_dhcp4_s_socket_udp_new(&sk2, link_client.ifindex);
+                c_assert(r >= 0);
+        }
+        netns_set(oldns);
+}
+
+int main(int argc, char **argv) {
+        test_setup();
+
+        test_sockets();
+        test_multiple_servers();
+
+        return 0;
+}
diff --git a/src/n-dhcp4/src/test.h b/src/n-dhcp4/src/test.h
new file mode 100644
index 00000000..b5acd14b
--- /dev/null
+++ b/src/n-dhcp4/src/test.h
@@ -0,0 +1,107 @@
+#pragma once
+
+/*
+ * Test Helpers
+ * Bunch of helpers to setup the environment for networking tests. This
+ * includes net-namespace setups, veth setups, and more.
+ */
+
+#undef NDEBUG
+#include <assert.h>
+#include <c-stdaux.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <net/ethernet.h>
+#include <netinet/in.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/mount.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+static inline void test_raise_memlock(void) {
+        const size_t wanted = 64 * 1024 * 1024;
+        struct rlimit get, set;
+        int r;
+
+        r = getrlimit(RLIMIT_MEMLOCK, &get);
+        c_assert(!r);
+
+        /* try raising limit to @wanted */
+        set.rlim_cur = wanted;
+        set.rlim_max = (wanted > get.rlim_max) ? wanted : get.rlim_max;
+        r = setrlimit(RLIMIT_MEMLOCK, &set);
+        if (r) {
+                c_assert(errno == EPERM);
+
+                /* not privileged to raise limit, so maximize soft limit */
+                set.rlim_cur = get.rlim_max;
+                set.rlim_max = get.rlim_max;
+                r = setrlimit(RLIMIT_MEMLOCK, &set);
+                c_assert(!r);
+        }
+}
+
+static inline void test_unshare_user_namespace(void) {
+        uid_t euid;
+        gid_t egid;
+        int r, fd;
+
+        /*
+         * Enter a new user namespace as root:root.
+         */
+
+        euid = geteuid();
+        egid = getegid();
+
+        r = unshare(CLONE_NEWUSER);
+        c_assert(r >= 0);
+
+        fd = open("/proc/self/uid_map", O_WRONLY);
+        c_assert(fd >= 0);
+        r = dprintf(fd, "0 %d 1\n", euid);
+        c_assert(r >= 0);
+        close(fd);
+
+        fd = open("/proc/self/setgroups", O_WRONLY);
+        c_assert(fd >= 0);
+        r = dprintf(fd, "deny");
+        c_assert(r >= 0);
+        close(fd);
+
+        fd = open("/proc/self/gid_map", O_WRONLY);
+        c_assert(fd >= 0);
+        r = dprintf(fd, "0 %d 1\n", egid);
+        c_assert(r >= 0);
+        close(fd);
+}
+
+static inline void test_setup(void) {
+        int r;
+
+        /*
+         * Move into a new network and mount namespace both associated
+         * with a new user namespace where the current eUID is mapped to
+         * 0. Then create a private instance of /run/netns. This ensures
+         * that any network devices or network namespaces are private to
+         * the test process.
+         */
+
+        test_raise_memlock();
+        test_unshare_user_namespace();
+
+        r = unshare(CLONE_NEWNET | CLONE_NEWNS);
+        c_assert(r >= 0);
+
+        r = mount(NULL, "/", NULL, MS_PRIVATE | MS_REC, NULL);
+        c_assert(r >= 0);
+
+        r = mount(NULL, "/run", "tmpfs", 0, NULL);
+        c_assert(r >= 0);
+
+        r = mkdir("/run/netns", 0755);
+        c_assert(r >= 0);
+}
diff --git a/src/n-dhcp4/src/util/link.c b/src/n-dhcp4/src/util/link.c
new file mode 100644
index 00000000..54d9a5c3
--- /dev/null
+++ b/src/n-dhcp4/src/util/link.c
@@ -0,0 +1,287 @@
+/*
+ * Link Management
+ *
+ * This is for our test-infrastructure only! It is not meant to be used outside
+ * of unit-tests.
+ */
+
+#include <arpa/inet.h>
+#include <assert.h>
+#include <c-stdaux.h>
+#include <net/ethernet.h>
+#include <net/if.h>
+#include <netinet/in.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <unistd.h>
+#include "link.h"
+#include "netns.h"
+#include "socket.h"
+
+/**
+ * link_deinit() - deinitialize link
+ * @link:               link to operate on
+ *
+ * This deinitializes a link and clears it. Once this call returns the link is
+ * cleared to LINK_NULL().
+ *
+ * It is safe to call this on LINK_NULL(), in which case it is a no-op. It is
+ * thus also safe to call this multiple times on the same link.
+ */
+void link_deinit(Link *link) {
+        netns_close(link->netns);
+        *link = (Link)LINK_NULL(*link);
+}
+
+static void link_query(int netns, const char *name, int *ifindexp, struct ether_addr *macp) {
+        int oldns;
+
+        netns_get(&oldns);
+        {
+                struct ifreq ifr = {};
+                size_t n_name;
+                int r, s;
+
+                netns_set(netns);
+
+                n_name = strlen(name);
+                c_assert(n_name <= IF_NAMESIZE);
+
+                if (ifindexp) {
+                        *ifindexp = if_nametoindex(name);
+                        c_assert(*ifindexp > 0);
+                }
+
+                if (macp) {
+                        s = socket(AF_INET, SOCK_DGRAM, 0);
+                        c_assert(s >= 0);
+
+                        memcpy(ifr.ifr_name, name, n_name);
+                        r = ioctl(s, SIOCGIFHWADDR, &ifr);
+                        c_assert(r >= 0);
+
+                        memcpy(macp->ether_addr_octet, ifr.ifr_hwaddr.sa_data, ETH_ALEN);
+
+                        close(s);
+                }
+        }
+        netns_set(oldns);
+}
+
+static void link_move(const char *ifname, int netns) {
+        char *p;
+        int r;
+
+        r = asprintf(&p, "ip link set %s up netns ns-test", ifname);
+        c_assert(r > 0);
+
+        netns_pin(netns, "ns-test");
+        r = system(p);
+        c_assert(r == 0);
+        netns_unpin("ns-test");
+
+        free(p);
+}
+
+/**
+ * link_new_veth() - create new veth pair
+ * @veth_parentp:               output argument for new veth parent
+ * @veth_childp:                output argument for new veth child
+ * @netns_parent:               target namespace for the parent
+ * @netns_child:                target namespace for the child
+ *
+ * This creates a new veth pair in the specified namespaces.
+ */
+void link_new_veth(Link *veth_parentp, Link *veth_childp, int netns_parent, int netns_child) {
+        int oldns;
+
+        netns_get(&oldns);
+        {
+                int r;
+
+                /*
+                 * Temporarily enter a new network namespace to make sure the
+                 * interface names are fresh.
+                 */
+                netns_set_anonymous();
+
+                r = system("ip link add veth-parent type veth peer name veth-child");
+                c_assert(r == 0);
+                r = system("ip link set veth-parent up addrgenmode none");
+                c_assert(r == 0);
+                r = system("ip link set veth-child up addrgenmode none");
+                c_assert(r == 0);
+
+                link_move("veth-parent", netns_parent);
+                link_move("veth-child", netns_child);
+        }
+        netns_set(oldns);
+
+        netns_new_dup(&veth_parentp->netns, netns_parent);
+        netns_new_dup(&veth_childp->netns, netns_child);
+        link_query(netns_parent, "veth-parent", &veth_parentp->ifindex, &veth_parentp->mac);
+        link_query(netns_child, "veth-child", &veth_childp->ifindex, &veth_childp->mac);
+
+        /*
+         * XXX: After moving a link both its name and ifindex might have
+         *      changed. Hence, link_query() might check the wrong interface.
+         *      One way to fix this would be to rename the interfaces after
+         *      they have been moved and queried based on their final ifindex.
+         *      This way, we reserve the internal names for the constructor,
+         *      and guarantee the final names will never conflict (disallowing
+         *      parallel calls to this function).
+         */
+}
+
+/**
+ * link_new_bridge() - create new bridge
+ * @bridgep:                    output argument for the new bridge
+ * @netns:                      target network namespace
+ *
+ * This creates a new bridge interface in the specified target network
+ * namespace.
+ */
+void link_new_bridge(Link *bridgep, int netns) {
+        int oldns;
+
+        netns_get(&oldns);
+        {
+                int r;
+
+                netns_set(netns);
+
+                r = system("ip link add test-bridge type bridge");
+                c_assert(r == 0);
+                r = system("ip link set test-bridge up addrgenmode none");
+                c_assert(r == 0);
+        }
+        netns_set(oldns);
+
+        netns_new_dup(&bridgep->netns, netns);
+        link_query(netns, "test-bridge", &bridgep->ifindex, &bridgep->mac);
+}
+
+/**
+ * link_add_ip4() - add IPv4 address to the specified link
+ * @link:                       link to operate on
+ * @addr:                       address to add
+ * @prefix:                     address prefix length
+ *
+ * This adds the specified IPv4 address to the given link.
+ */
+void link_add_ip4(Link *link, const struct in_addr *addr, unsigned int prefix) {
+        int oldns;
+
+        netns_get(&oldns);
+        {
+                char *p, ifname[IF_NAMESIZE + 1] = {};
+                int r;
+
+                netns_set(link->netns);
+
+                p = if_indextoname(link->ifindex, ifname);
+                c_assert(p);
+                r = asprintf(&p, "ip addr add %s/%u dev %s", inet_ntoa(*addr), prefix, ifname);
+                c_assert(r >= 0);
+                r = system(p);
+                c_assert(r == 0);
+                free(p);
+        }
+        netns_set(oldns);
+}
+
+/**
+ * link_del_ip4() - delete IPv4 address from the specified link
+ * @link:                       link to operate on
+ * @addr:                       address to delete
+ * @prefix:                     address prefix length
+ *
+ * This deletes the specified IPv4 address from the given link.
+ */
+void link_del_ip4(Link *link, const struct in_addr *addr, unsigned int prefix) {
+        int oldns;
+
+        netns_get(&oldns);
+        {
+                char *p, ifname[IF_NAMESIZE + 1] = {};
+                int r;
+
+                netns_set(link->netns);
+
+                p = if_indextoname(link->ifindex, ifname);
+                c_assert(p);
+                r = asprintf(&p, "ip addr del %s/%u dev %s", inet_ntoa(*addr), prefix, ifname);
+                c_assert(r >= 0);
+                r = system(p);
+                c_assert(r == 0);
+                free(p);
+        }
+        netns_set(oldns);
+}
+
+/**
+ * link_set_master() - change the bridge master of an interface
+ * @link:                       link to operate on
+ * @if_master:                  bridge to set as master
+ *
+ * This sets @if_master as the new master bridge of @link. The specified bridge
+ * must be in the same network namespace as @link.
+ */
+void link_set_master(Link *link, int if_master) {
+        int oldns;
+
+        netns_get(&oldns);
+        {
+                char *p, ifname_master[IF_NAMESIZE + 1] = {}, ifname[IF_NAMESIZE + 1] = {};
+                int r;
+
+                netns_set(link->netns);
+
+                p = if_indextoname(link->ifindex, ifname);
+                c_assert(p);
+                p = if_indextoname(if_master, ifname_master);
+                c_assert(p);
+                r = asprintf(&p, "ip link set %s master %s", ifname, ifname_master);
+                c_assert(r > 0);
+                r = system(p);
+                c_assert(r == 0);
+                free(p);
+        }
+        netns_set(oldns);
+}
+
+/**
+ * link_socket() - create socket for link
+ * @link:               link to operate on
+ * @socketp:            output argument for new socket
+ * @family:             socket family to create socket in
+ * @type:               socket type to create socket as
+ *
+ * This creates a socket of the protocol family @family via socket(2), but
+ * makes sure to create it in the network-namespace where @link resides.
+ * Furthermore, the socket is bound to the link specified in @link.
+ *
+ * The new socket is returned in @socketp.
+ */
+void link_socket(Link *link, int *socketp, int family, int type) {
+        int oldns;
+
+        netns_get(&oldns);
+        {
+                int r, fd;
+
+                netns_set(link->netns);
+
+                fd = socket(family, type, 0);
+                c_assert(fd >= 0);
+
+                r = socket_bind_if(fd, link->ifindex);
+                c_assert(!r);
+
+                *socketp = fd;
+        }
+        netns_set(oldns);
+}
diff --git a/src/n-dhcp4/src/util/link.h b/src/n-dhcp4/src/util/link.h
new file mode 100644
index 00000000..cd0ad6f8
--- /dev/null
+++ b/src/n-dhcp4/src/util/link.h
@@ -0,0 +1,38 @@
+#pragma once
+
+/*
+ * Link Management
+ *
+ * This utility provides easy access to network links. It is meant for testing
+ * purposes only and relies on call-outs to ip(1). A proper implementation
+ * should rather use netlink directly to interact with the kernel.
+ *
+ * Furthermore, for simplification this is limited to ethernet links.
+ */
+
+#include <c-stdaux.h>
+#include <net/ethernet.h>
+#include <netinet/in.h>
+#include <stdlib.h>
+
+typedef struct Link Link;
+
+struct Link {
+        int netns;
+        int ifindex;
+        struct ether_addr mac;
+};
+
+#define LINK_NULL(_x) {                                                         \
+                .netns = -1,                                                    \
+        }
+
+void link_deinit(Link *link);
+
+void link_new_veth(Link *veth_parentp, Link *veth_childp, int netns_parent, int netns_child);
+void link_new_bridge(Link *bridgep, int netns);
+
+void link_add_ip4(Link *link, const struct in_addr *addr, unsigned int prefix);
+void link_del_ip4(Link *link, const struct in_addr *addr, unsigned int prefix);
+void link_set_master(Link *link, int if_master);
+void link_socket(Link *link, int *socketp, int family, int type);
diff --git a/src/n-dhcp4/src/util/netns.c b/src/n-dhcp4/src/util/netns.c
new file mode 100644
index 00000000..0b7b32e4
--- /dev/null
+++ b/src/n-dhcp4/src/util/netns.c
@@ -0,0 +1,165 @@
+/*
+ * Network Namespaces
+ *
+ * This is meant for testing-purposes only. It is not meant to be used outside
+ * of our unit-tests!
+ */
+
+#include <assert.h>
+#include <c-stdaux.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <unistd.h>
+#include "netns.h"
+
+/**
+ * netns_new() - create a new network namespace
+ * @netnsp:             output argument to store netns fd
+ *
+ * This creates a new network namespace and returns a netns fd that refers to
+ * the new network namespace. Note that there is no native API to create an
+ * anonymous network namespace, so this call has to temporarily switch to a new
+ * network namespace (using unshare(2)). This temporary switch does not affect
+ * any other threads or processes, however, it can be observed by other
+ * processes.
+ */
+void netns_new(int *netnsp) {
+        int r, oldns;
+
+        netns_get(&oldns);
+
+        r = unshare(CLONE_NEWNET);
+        c_assert(r >= 0);
+
+        netns_get(netnsp);
+        netns_set(oldns);
+}
+
+/**
+ * netns_new_dup() - duplicate network namespace descriptor
+ * @newnsp:             output argument for duplicated descriptor
+ * @netns:              netns descriptor to duplicate
+ *
+ * This duplicates the network namespace file descriptor. The duplicate still
+ * refers to the same network namespace, but is an independent file descriptor.
+ */
+void netns_new_dup(int *newnsp, int netns) {
+        *newnsp = fcntl(netns, F_DUPFD_CLOEXEC, 0);
+        c_assert(*newnsp >= 0);
+}
+
+/**
+ * netns_close() - destroy a network namespace descriptor
+ * @netns:              netns to operate on, or <0
+ *
+ * This closes the given network namespace descriptor. If @netns is negative,
+ * this is a no-op.
+ *
+ * Return: -1 is returned.
+ */
+int netns_close(int netns) {
+        return c_close(netns);
+}
+
+/**
+ * netns_get() - retrieve the current network namespace
+ * @netnsp:             output argument to store netns fd
+ *
+ * This retrieves a file-descriptor to the current network namespace.
+ */
+void netns_get(int *netnsp) {
+        *netnsp = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC);
+        c_assert(*netnsp >= 0);
+}
+
+/**
+ * setns_set() - change the current network namespace
+ * @netns:              netns to set
+ *
+ * This changes the current network namespace to the netns given by the
+ * file-descriptor @netns.
+ */
+void netns_set(int netns) {
+        int r;
+
+        r = setns(netns, CLONE_NEWNET);
+        c_assert(r >= 0);
+}
+
+/**
+ * netns_set_anonymous() - enter an anonymous network namespace
+ *
+ * This is a helper that creates a new network namespace, enters it, and then
+ * forgets about it.
+ */
+void netns_set_anonymous(void) {
+        int r;
+
+        r = unshare(CLONE_NEWNET);
+        c_assert(r >= 0);
+}
+
+/**
+ * netns_pin() - pin network namespace in file-system
+ * @netns:              netns to pin
+ * @name:               name to pin netns under
+ *
+ * This pins the network namespace given as @netns in the file-system as
+ * `/run/netns/@name`. It is the responsibility of the caller to guarantee
+ * @name is not used by anyone else in parallel. This function will abort if
+ * @name is already in use.
+ *
+ * The namespace in `/run/netns/` is compatible with the namespace provided by
+ * the ip(1) tool, and can be used to pass network namespaces to invocations of
+ * ip(1).
+ */
+void netns_pin(int netns, const char *name) {
+        char *fd_path, *netns_path;
+        int r, fd;
+
+        r = asprintf(&fd_path, "/proc/self/fd/%d", netns);
+        c_assert(r >= 0);
+
+        r = asprintf(&netns_path, "/run/netns/%s", name);
+        c_assert(r >= 0);
+
+        fd = open(netns_path, O_RDONLY|O_CLOEXEC|O_CREAT|O_EXCL, 0);
+        c_assert(fd >= 0);
+        close(fd);
+
+        r = mount(fd_path, netns_path, "none", MS_BIND, NULL);
+        c_assert(r >= 0);
+
+        free(netns_path);
+        free(fd_path);
+}
+
+/**
+ * netns_unpin() - unpin network namespace from file-system
+ * @name:               name to unpin
+ *
+ * This removes a network namespace pin from the file-system. It expects the
+ * pin to be located at `/run/netns/@name`. This function aborts if the pin
+ * does not exist.
+ *
+ * See netns_pin() for ways to create such pins.
+ */
+void netns_unpin(const char *name) {
+        char *netns_path;
+        int r;
+
+        r = asprintf(&netns_path, "/run/netns/%s", name);
+        c_assert(r >= 0);
+
+        r = umount2(netns_path, MNT_DETACH);
+        c_assert(r >= 0);
+
+        r = unlink(netns_path);
+        c_assert(r >= 0);
+
+        free(netns_path);
+}
diff --git a/src/n-dhcp4/src/util/netns.h b/src/n-dhcp4/src/util/netns.h
new file mode 100644
index 00000000..02e2e010
--- /dev/null
+++ b/src/n-dhcp4/src/util/netns.h
@@ -0,0 +1,27 @@
+#pragma once
+
+/*
+ * Network Namespaces
+ *
+ * The netns utility provides an object-based API to network namespaces. It is
+ * meant for testing purposes only.
+ */
+
+#include <c-stdaux.h>
+#include <stdlib.h>
+
+void netns_new(int *netnsp);
+void netns_new_dup(int *newnsp, int netns);
+int netns_close(int netns);
+
+void netns_get(int *netnsp);
+void netns_set(int netns);
+void netns_set_anonymous(void);
+
+void netns_pin(int netns, const char *name);
+void netns_unpin(const char *name);
+
+static inline void netns_closep(int *netns) {
+        if (*netns >= 0)
+                netns_close(*netns);
+}
diff --git a/src/n-dhcp4/src/util/test-packet.c b/src/n-dhcp4/src/util/test-packet.c
new file mode 100644
index 00000000..44dbc300
--- /dev/null
+++ b/src/n-dhcp4/src/util/test-packet.c
@@ -0,0 +1,411 @@
+/*
+ * Packet Socket Tests
+ */
+
+#undef NDEBUG
+#include <assert.h>
+#include <c-stdaux.h>
+#include <errno.h>
+#include <net/if_arp.h>
+#include <stdlib.h>
+#include <string.h>
+#include "n-dhcp4-private.h"
+#include "link.h"
+#include "netns.h"
+#include "packet.h"
+#include "test.h"
+
+typedef struct Blob {
+        uint16_t checksum;
+        uint8_t data[128];
+} Blob;
+
+static void test_checksum_one(Blob *blob, size_t size) {
+        uint16_t checksum;
+
+        /*
+         * The only important property of the internet-checksum is that if the
+         * target blob is amended with its own checksum, the checksum
+         * calculation will become 0. So here we simply calculate the checksum
+         * with a dummy 0 in place, then put the checksum in and verify that
+         * the resulting checksum becomes 0.
+         */
+
+        blob->checksum = 0;
+        blob->checksum = packet_internet_checksum((uint8_t*)blob, size);
+
+        checksum = packet_internet_checksum((uint8_t*)blob, size);
+        c_assert(!checksum);
+}
+
+static void test_checksum_udp_one(Blob *blob, size_t size) {
+        uint16_t checksum;
+
+        /*
+         * Like test_checksum_one(), here we calculate the target checksum,
+         * then place it in the source blob and calculate the checksum again.
+         * We expect it to be 0 in the end (i.e., pass the checksum test).
+         *
+         * Unlike the generic version, we must pass dummy UDP data into the
+         * helpers and also avoid a 0 checksum in the original source.
+         */
+
+        checksum = packet_internet_checksum_udp(&(struct in_addr){ htonl((10 << 24) | 2)},
+                                                &(struct in_addr){ htonl((10 << 24) | 1)},
+                                                67,
+                                                68,
+                                                blob->data,
+                                                sizeof(blob->data),
+                                                0);
+        checksum = checksum ?: 0xffff;
+        checksum = packet_internet_checksum_udp(&(struct in_addr){ htonl((10 << 24) | 2)},
+                                                &(struct in_addr){ htonl((10 << 24) | 1)},
+                                                67,
+                                                68,
+                                                blob->data,
+                                                sizeof(blob->data),
+                                                checksum);
+        c_assert(!checksum);
+}
+
+/*
+ * This generates some pseudo-random bytes and verifies that
+ * packet_internet_checksum{,_udp}() correctly calculates the checksum on this
+ * random-data.
+ */
+static void test_checksum(void) {
+        Blob blob = {};
+
+        /* fill @blob.data with some pseudo-random bytes */
+        for (size_t i = 0; i < sizeof(blob.data); ++i)
+                blob.data[i] = i ^ (i >> 8) ^ (i >> 16) ^ (i >> 24);
+
+        /* take chunks of @blob.data and verify their checksum */
+        for (size_t j = 0; j < sizeof(uint64_t); ++j) {
+                for (uint32_t i = 0; i <= 0xffff; ++i) {
+                        blob.data[0] = i & 0xff;
+                        blob.data[1] = i >> 8;
+                        test_checksum_one(&blob, sizeof(blob) - j);
+                        test_checksum_udp_one(&blob, sizeof(blob) - j);
+                }
+        }
+}
+
+static void test_new_packet_socket(Link *link, int *skp) {
+        struct sockaddr_ll addr = {
+                .sll_family = AF_PACKET,
+                .sll_protocol = htons(ETH_P_IP),
+                .sll_ifindex = link->ifindex,
+        };
+        int r, on = 1;
+
+        link_socket(link, skp, AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC);
+
+        r = setsockopt(*skp, SOL_PACKET, PACKET_AUXDATA, &on, sizeof(on));
+        c_assert(r >= 0);
+
+        r = bind(*skp, (struct sockaddr*)&addr, sizeof(addr));
+        c_assert(r >= 0);
+}
+
+static void test_packet_unicast(int ifindex, int sk, void *buf, size_t n_buf,
+                                const struct sockaddr_in *paddr_src,
+                                const struct sockaddr_in *paddr_dst,
+                                const struct ether_addr *haddr_dst) {
+        struct packet_sockaddr_ll addr = {
+                .sll_family = AF_PACKET,
+                .sll_protocol = htons(ETH_P_IP),
+                .sll_ifindex = ifindex,
+                .sll_halen = ETH_ALEN,
+        };
+        size_t len;
+        int r;
+
+        memcpy(addr.sll_addr, haddr_dst, ETH_ALEN);
+
+        r = packet_sendto_udp(sk, buf, n_buf, &len, paddr_src, &addr, paddr_dst, N_DHCP4_DSCP_DEFAULT);
+        c_assert(!r);
+        c_assert(len == n_buf);
+}
+
+static void test_packet_broadcast(int ifindex, int sk, void *buf, size_t n_buf,
+                                  const struct sockaddr_in *paddr_src,
+                                  const struct sockaddr_in *paddr_dst) {
+        struct packet_sockaddr_ll addr = {
+                .sll_family = AF_PACKET,
+                .sll_protocol = htons(ETH_P_IP),
+                .sll_ifindex = ifindex,
+                .sll_halen = ETH_ALEN,
+        };
+        size_t len;
+        int r;
+
+        memcpy(addr.sll_addr, (unsigned char[]){ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, }, ETH_ALEN);
+
+        r = packet_sendto_udp(sk, buf, n_buf, &len, paddr_src, &addr, paddr_dst, N_DHCP4_DSCP_DEFAULT);
+        c_assert(!r);
+        c_assert(len == n_buf);
+}
+
+static void test_packet_packet(Link *link_src,
+                               Link *link_dst,
+                               const struct sockaddr_in *paddr_src,
+                               const struct sockaddr_in *paddr_dst) {
+        _c_cleanup_(c_closep) int sk_src = -1, sk_dst = -1;
+        uint8_t buf[1024];
+        size_t len;
+        int r;
+
+        link_socket(link_src, &sk_src, AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC);
+        test_new_packet_socket(link_dst, &sk_dst);
+
+        test_packet_unicast(link_src->ifindex, sk_src, buf, sizeof(buf) - 1, paddr_src, paddr_dst, &link_dst->mac);
+        test_packet_broadcast(link_src->ifindex, sk_src, buf, sizeof(buf) - 1, paddr_src, paddr_dst);
+
+        r = packet_recv_udp(sk_dst, buf, sizeof(buf), &len);
+        c_assert(!r);
+        c_assert(len == (ssize_t)sizeof(buf) - 1);
+
+        r = packet_recv_udp(sk_dst, buf, sizeof(buf), &len);
+        c_assert(!r);
+        c_assert(len == (ssize_t)sizeof(buf) - 1);
+}
+
+static void test_packet_udp(Link *link_src,
+                            Link *link_dst,
+                            const struct sockaddr_in *paddr_src,
+                            const struct sockaddr_in *paddr_dst) {
+        _c_cleanup_(c_closep) int sk_src = -1, sk_dst = -1;
+        uint8_t buf[1024];
+        ssize_t len;
+        int r;
+
+        link_socket(link_src, &sk_src, AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC);
+        link_socket(link_dst, &sk_dst, AF_INET, SOCK_DGRAM | SOCK_CLOEXEC);
+        link_add_ip4(link_dst, &paddr_dst->sin_addr, 8);
+
+        r = bind(sk_dst, (struct sockaddr*)paddr_dst, sizeof(*paddr_dst));
+        c_assert(r >= 0);
+
+        test_packet_unicast(link_src->ifindex, sk_src, buf, sizeof(buf) - 1, paddr_src, paddr_dst, &link_dst->mac);
+        test_packet_broadcast(link_src->ifindex, sk_src, buf, sizeof(buf) - 1, paddr_src, paddr_dst);
+
+        len = recv(sk_dst, buf, sizeof(buf), 0);
+        c_assert(len == (ssize_t)sizeof(buf) - 1);
+
+        len = recv(sk_dst, buf, sizeof(buf), 0);
+        c_assert(len == (ssize_t)sizeof(buf) - 1);
+
+        link_del_ip4(link_dst, &paddr_dst->sin_addr, 8);
+}
+
+static void test_udp_packet(Link *link_src,
+                            Link *link_dst,
+                            const struct sockaddr_in *paddr_src,
+                            const struct sockaddr_in *paddr_dst) {
+        _c_cleanup_(c_closep) int sk_src = -1, sk_dst = -1;
+        uint8_t buf[1024];
+        ssize_t slen;
+        size_t len;
+        int r;
+
+        link_socket(link_src, &sk_src, AF_INET, SOCK_DGRAM | SOCK_CLOEXEC);
+        test_new_packet_socket(link_dst, &sk_dst);
+        link_add_ip4(link_src, &paddr_src->sin_addr, 8);
+        link_add_ip4(link_dst, &paddr_dst->sin_addr, 8);
+
+        slen = sendto(sk_src, buf, sizeof(buf) - 1, 0,
+                      (struct sockaddr*)paddr_dst, sizeof(*paddr_dst));
+        c_assert(slen == (ssize_t)sizeof(buf) - 1);
+
+        r = packet_recv_udp(sk_dst, buf, sizeof(buf), &len);
+        c_assert(!r);
+        c_assert(len == (ssize_t)sizeof(buf) - 1);
+
+        link_del_ip4(link_dst, &paddr_dst->sin_addr, 8);
+        link_del_ip4(link_src, &paddr_src->sin_addr, 8);
+}
+
+static void test_udp_udp(Link *link_src,
+                         Link *link_dst,
+                         const struct sockaddr_in *paddr_src,
+                         const struct sockaddr_in *paddr_dst) {
+        _c_cleanup_(c_closep) int sk_src = -1, sk_dst = -1;
+        uint8_t buf[1024];
+        ssize_t len;
+        int r;
+
+        link_socket(link_src, &sk_src, AF_INET, SOCK_DGRAM | SOCK_CLOEXEC);
+        link_socket(link_dst, &sk_dst, AF_INET, SOCK_DGRAM | SOCK_CLOEXEC);
+        link_add_ip4(link_src, &paddr_src->sin_addr, 8);
+        link_add_ip4(link_dst, &paddr_dst->sin_addr, 8);
+
+        r = bind(sk_dst, (struct sockaddr*)paddr_dst, sizeof(*paddr_dst));
+        c_assert(r >= 0);
+
+        len = sendto(sk_src, buf, sizeof(buf) - 1, 0,
+                     (struct sockaddr*)paddr_dst, sizeof(*paddr_dst));
+        c_assert(len == (ssize_t)sizeof(buf) - 1);
+
+        len = recv(sk_dst, buf, sizeof(buf), 0);
+        c_assert(len == (ssize_t)sizeof(buf) - 1);
+
+        link_del_ip4(link_dst, &paddr_dst->sin_addr, 8);
+        link_del_ip4(link_src, &paddr_src->sin_addr, 8);
+}
+
+static void test_shutdown(Link *link_src,
+                          Link *link_dst,
+                          const struct sockaddr_in *paddr_src,
+                          const struct sockaddr_in *paddr_dst) {
+        _c_cleanup_(c_closep) int sk_src = -1, sk_dst1 = -1, sk_dst2 = -1;
+        uint8_t buf[1024];
+        ssize_t slen;
+        size_t len;
+        int r;
+
+        link_socket(link_src, &sk_src, AF_INET, SOCK_DGRAM | SOCK_CLOEXEC);
+        test_new_packet_socket(link_dst, &sk_dst1);
+        link_add_ip4(link_src, &paddr_src->sin_addr, 8);
+        link_add_ip4(link_dst, &paddr_dst->sin_addr, 8);
+
+        /* 1 - send only to the packet socket */
+        slen = sendto(sk_src, buf, sizeof(buf), 0,
+                     (struct sockaddr*)paddr_dst, sizeof(*paddr_dst));
+        c_assert(slen == (ssize_t)sizeof(buf));
+
+        /* create a UDP socket */
+        link_socket(link_dst, &sk_dst2, AF_INET, SOCK_DGRAM | SOCK_CLOEXEC);
+
+        r = bind(sk_dst2, (struct sockaddr*)paddr_dst, sizeof(*paddr_dst));
+        c_assert(r >= 0);
+
+        /* 2 - send to both sockets */
+        slen = sendto(sk_src, buf, sizeof(buf), 0,
+                     (struct sockaddr*)paddr_dst, sizeof(*paddr_dst));
+        c_assert(slen == (ssize_t)sizeof(buf));
+
+        /* shut down the packet socket */
+        r = packet_shutdown(sk_dst1);
+        c_assert(r >= 0);
+
+        /* 3 - send only to the UDP socket */
+        slen = sendto(sk_src, buf, sizeof(buf), 0,
+                     (struct sockaddr*)paddr_dst, sizeof(*paddr_dst));
+        c_assert(slen == (ssize_t)sizeof(buf));
+
+        /* receive 1 and 2 on the packet socket */
+        r = packet_recv_udp(sk_dst1, buf, sizeof(buf), &len);
+        c_assert(!r);
+        c_assert(len == (ssize_t)sizeof(buf));
+        r = packet_recv_udp(sk_dst1, buf, sizeof(buf), &len);
+        c_assert(!r);
+        c_assert(len == (ssize_t)sizeof(buf));
+
+        /* make sure there is nothing more pending on the packet socket */
+        slen = recv(sk_dst1, buf, sizeof(buf), MSG_DONTWAIT);
+        c_assert(slen < 0);
+        c_assert(errno == EAGAIN);
+
+        /* receive 2 and 3 on the UDP socket */
+        slen = recv(sk_dst2, buf, sizeof(buf), 0);
+        c_assert(slen == (ssize_t)sizeof(buf));
+        slen = recv(sk_dst2, buf, sizeof(buf), 0);
+        c_assert(slen == (ssize_t)sizeof(buf));
+
+        /* make sure there is nothing more pending on the UDP socket */
+        slen = recv(sk_dst1, buf, sizeof(buf), MSG_DONTWAIT);
+        c_assert(slen < 0);
+        c_assert(errno == EAGAIN);
+
+        link_del_ip4(link_dst, &paddr_dst->sin_addr, 8);
+        link_del_ip4(link_src, &paddr_src->sin_addr, 8);
+}
+
+static void test_ip_hdr(Link *link_src,
+                        Link *link_dst,
+                        const struct sockaddr_in *paddr_src,
+                        const struct sockaddr_in *paddr_dst) {
+        _c_cleanup_(c_closep) int sk_src = -1, sk_dst = -1;
+        uint8_t ipopts[5] = { 1, 1, 1, 1, 1 };
+        uint8_t buf[1024];
+        ssize_t slen;
+        size_t len;
+        int r;
+
+        /*
+         * This test sends a packet from a UDP socket to a packet socket, but
+         * appends 5-bytes of IPOPT_NOOP ip-options. With this we verify our
+         * packet socket correctly skips additional ip-options and does not
+         * interpret the ip-header as a fixed size header.
+         */
+
+        link_socket(link_src, &sk_src, AF_INET, SOCK_DGRAM | SOCK_CLOEXEC);
+        test_new_packet_socket(link_dst, &sk_dst);
+        link_add_ip4(link_src, &paddr_src->sin_addr, 8);
+        link_add_ip4(link_dst, &paddr_dst->sin_addr, 8);
+
+        r = setsockopt(sk_src, IPPROTO_IP, IP_OPTIONS, ipopts, sizeof(ipopts));
+        c_assert(r >= 0);
+
+        slen = sendto(sk_src, buf, sizeof(buf) - 1, 0,
+                      (struct sockaddr*)paddr_dst, sizeof(*paddr_dst));
+        c_assert(slen == (ssize_t)sizeof(buf) - 1);
+
+        r = packet_recv_udp(sk_dst, buf, sizeof(buf), &len);
+        c_assert(!r);
+        c_assert(len == (ssize_t)sizeof(buf) - 1);
+
+        link_del_ip4(link_dst, &paddr_dst->sin_addr, 8);
+        link_del_ip4(link_src, &paddr_src->sin_addr, 8);
+}
+
+/*
+ * This test verifies that we can send packets from/to packet/udp sockets. It
+ * tests all combinations: packet->packet, packet->udp, udp->packet, udp->udp
+ *
+ * Furthermore, this test checks for some of the behavioural properties of our
+ * packet socket helpers.
+ */
+static void test_packet(void) {
+        _c_cleanup_(netns_closep) int ns_src = -1, ns_dst = -1;
+        _c_cleanup_(link_deinit) Link link_src = LINK_NULL(link_src);
+        _c_cleanup_(link_deinit) Link link_dst = LINK_NULL(link_dst);
+        struct sockaddr_in paddr_src = {
+                .sin_family = AF_INET,
+                .sin_addr = (struct in_addr){ htonl(10<<24 | 1) },
+                .sin_port = htons(10),
+        };
+        struct sockaddr_in paddr_dst = {
+                .sin_family = AF_INET,
+                .sin_addr = (struct in_addr){ htonl(10<<24 | 2) },
+                .sin_port = htons(11),
+        };
+
+        /* setup */
+
+        netns_new(&ns_src);
+        netns_new(&ns_dst);
+        link_new_veth(&link_src, &link_dst, ns_src, ns_dst);
+
+        /* communication tests */
+
+        test_packet_packet(&link_src, &link_dst, &paddr_src, &paddr_dst);
+        test_packet_udp(&link_src, &link_dst, &paddr_src, &paddr_dst);
+        test_udp_packet(&link_src, &link_dst, &paddr_src, &paddr_dst);
+        test_udp_udp(&link_src, &link_dst, &paddr_src, &paddr_dst);
+
+        /* behavior tests */
+
+        test_shutdown(&link_src, &link_dst, &paddr_src, &paddr_dst);
+        test_ip_hdr(&link_src, &link_dst, &paddr_src, &paddr_dst);
+}
+
+int main(int argc, char **argv) {
+        test_setup();
+
+        test_checksum();
+        test_packet();
+
+        return 0;
+}
diff --git a/src/n-dhcp4/subprojects/c-list b/src/n-dhcp4/subprojects/c-list
new file mode 120000
index 00000000..4e274698
--- /dev/null
+++ b/src/n-dhcp4/subprojects/c-list
@@ -0,0 +1 @@
+../../c-list
\ No newline at end of file
diff --git a/src/n-dhcp4/subprojects/c-siphash b/src/n-dhcp4/subprojects/c-siphash
new file mode 120000
index 00000000..70d68818
--- /dev/null
+++ b/src/n-dhcp4/subprojects/c-siphash
@@ -0,0 +1 @@
+../../c-siphash
\ No newline at end of file