about summary refs log tree commit diff
path: root/shared
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2019-07-31 10:51:42 +0200
committerMichael Biebl <biebl@debian.org>2019-07-31 10:51:42 +0200
commit2e5fa45ddfbb5cffa1e78221f1cea706e2f298af (patch)
tree86f69d36c56de3074280456eddc854a780b8e04b /shared
parent85563b7fc7ec2cd21e38debb9b28db342e2e8e7c (diff)
New upstream version 1.19.90 upstream/1.19.90
Diffstat (limited to 'shared')
-rw-r--r--shared/c-rbtree/src/c-rbtree-private.h7
-rw-r--r--shared/c-rbtree/src/c-rbtree.c83
-rw-r--r--shared/c-siphash/src/c-siphash.c11
-rw-r--r--shared/c-stdaux/src/c-stdaux.h546
-rw-r--r--shared/meson.build92
-rw-r--r--shared/n-acd/src/n-acd-bpf-fallback.c1
-rw-r--r--shared/n-acd/src/n-acd-bpf.c1
-rw-r--r--shared/n-acd/src/n-acd-private.h20
-rw-r--r--shared/n-acd/src/n-acd-probe.c135
-rw-r--r--shared/n-acd/src/n-acd.c264
-rw-r--r--shared/n-acd/src/n-acd.h2
-rw-r--r--shared/n-acd/src/util/timer.c14
-rw-r--r--shared/n-acd/src/util/timer.h1
-rw-r--r--shared/n-dhcp4/src/n-dhcp4-c-connection.c1150
-rw-r--r--shared/n-dhcp4/src/n-dhcp4-c-lease.c356
-rw-r--r--shared/n-dhcp4/src/n-dhcp4-c-probe.c1203
-rw-r--r--shared/n-dhcp4/src/n-dhcp4-client.c839
-rw-r--r--shared/n-dhcp4/src/n-dhcp4-incoming.c431
-rw-r--r--shared/n-dhcp4/src/n-dhcp4-outgoing.c372
-rw-r--r--shared/n-dhcp4/src/n-dhcp4-private.h688
-rw-r--r--shared/n-dhcp4/src/n-dhcp4-socket.c657
-rw-r--r--shared/n-dhcp4/src/n-dhcp4.h286
-rw-r--r--shared/n-dhcp4/src/util/packet.c456
-rw-r--r--shared/n-dhcp4/src/util/packet.h61
-rw-r--r--shared/n-dhcp4/src/util/socket.c121
-rw-r--r--shared/n-dhcp4/src/util/socket.h11
-rw-r--r--shared/nm-default.h21
-rw-r--r--shared/nm-glib-aux/nm-c-list.h103
-rw-r--r--shared/nm-glib-aux/nm-dbus-aux.c69
-rw-r--r--shared/nm-glib-aux/nm-dbus-aux.h102
-rw-r--r--shared/nm-glib-aux/nm-dedup-multi.c1
-rw-r--r--shared/nm-glib-aux/nm-dedup-multi.h1
-rw-r--r--shared/nm-glib-aux/nm-enum-utils.c1
-rw-r--r--shared/nm-glib-aux/nm-enum-utils.h1
-rw-r--r--shared/nm-glib-aux/nm-glib.h13
-rw-r--r--shared/nm-glib-aux/nm-hash-utils.c39
-rw-r--r--shared/nm-glib-aux/nm-hash-utils.h26
-rw-r--r--shared/nm-glib-aux/nm-io-utils.c24
-rw-r--r--shared/nm-glib-aux/nm-io-utils.h5
-rw-r--r--shared/nm-glib-aux/nm-jansson.h101
-rw-r--r--shared/nm-glib-aux/nm-json-aux.c149
-rw-r--r--shared/nm-glib-aux/nm-json-aux.h83
-rw-r--r--shared/nm-glib-aux/nm-keyfile-aux.c413
-rw-r--r--shared/nm-glib-aux/nm-keyfile-aux.h78
-rw-r--r--shared/nm-glib-aux/nm-logging-fwd.h27
-rw-r--r--shared/nm-glib-aux/nm-macros-internal.h159
-rw-r--r--shared/nm-glib-aux/nm-obj.h1
-rw-r--r--shared/nm-glib-aux/nm-random-utils.c1
-rw-r--r--shared/nm-glib-aux/nm-random-utils.h1
-rw-r--r--shared/nm-glib-aux/nm-secret-utils.c1
-rw-r--r--shared/nm-glib-aux/nm-secret-utils.h1
-rw-r--r--shared/nm-glib-aux/nm-shared-utils.c371
-rw-r--r--shared/nm-glib-aux/nm-shared-utils.h82
-rw-r--r--shared/nm-glib-aux/nm-time-utils.c38
-rw-r--r--shared/nm-glib-aux/nm-time-utils.h25
-rw-r--r--shared/nm-glib-aux/nm-value-type.h208
-rw-r--r--shared/nm-libnm-core-aux/nm-dispatcher-api.h6
-rw-r--r--shared/nm-libnm-core-aux/nm-libnm-core-aux.c373
-rw-r--r--shared/nm-libnm-core-aux/nm-libnm-core-aux.h54
-rw-r--r--shared/nm-libnm-core-intern/nm-common-macros.h1
-rw-r--r--shared/nm-libnm-core-intern/nm-ethtool-utils.c2
-rw-r--r--shared/nm-libnm-core-intern/nm-ethtool-utils.h1
-rw-r--r--shared/nm-meta-setting.c180
-rw-r--r--shared/nm-meta-setting.h4
-rw-r--r--shared/nm-std-aux/c-list-util.c1
-rw-r--r--shared/nm-std-aux/c-list-util.h1
-rw-r--r--shared/nm-std-aux/nm-dbus-compat.h1
-rw-r--r--shared/nm-test-libnm-utils.h28
-rw-r--r--shared/nm-test-utils-impl.c140
-rw-r--r--shared/nm-udev-aux/nm-udev-utils.c1
-rw-r--r--shared/nm-udev-aux/nm-udev-utils.h1
-rw-r--r--shared/nm-utils/nm-compat.c1
-rw-r--r--shared/nm-utils/nm-compat.h1
-rw-r--r--shared/nm-utils/nm-test-utils.h104
-rw-r--r--shared/nm-utils/nm-vpn-editor-plugin-call.h1
-rw-r--r--shared/nm-utils/nm-vpn-plugin-macros.h2
-rw-r--r--shared/nm-utils/nm-vpn-plugin-utils.c2
-rw-r--r--shared/nm-utils/nm-vpn-plugin-utils.h2
-rw-r--r--shared/nm-utils/tests/test-shared-general.c105
-rw-r--r--shared/nm-version-macros.h6
-rw-r--r--shared/nm-version-macros.h.in2
-rw-r--r--shared/systemd/nm-logging-stub.c7
-rw-r--r--shared/systemd/nm-sd-utils-shared.c13
-rw-r--r--shared/systemd/nm-sd-utils-shared.h5
-rw-r--r--shared/systemd/sd-adapt-shared/format-util.h3
-rw-r--r--shared/systemd/sd-adapt-shared/nm-sd-adapt-shared.h38
-rw-r--r--shared/systemd/sd-adapt-shared/strxcpyx.h3
-rw-r--r--shared/systemd/src/basic/alloc-util.c25
-rw-r--r--shared/systemd/src/basic/alloc-util.h6
-rw-r--r--shared/systemd/src/basic/env-file.c3
-rw-r--r--shared/systemd/src/basic/env-util.c14
-rw-r--r--shared/systemd/src/basic/env-util.h7
-rw-r--r--shared/systemd/src/basic/errno-util.h19
-rw-r--r--shared/systemd/src/basic/escape.c73
-rw-r--r--shared/systemd/src/basic/escape.h6
-rw-r--r--shared/systemd/src/basic/extract-word.c4
-rw-r--r--shared/systemd/src/basic/extract-word.h2
-rw-r--r--shared/systemd/src/basic/fileio.c131
-rw-r--r--shared/systemd/src/basic/fileio.h7
-rw-r--r--shared/systemd/src/basic/format-util.c72
-rw-r--r--shared/systemd/src/basic/format-util.h83
-rw-r--r--shared/systemd/src/basic/fs-util.c128
-rw-r--r--shared/systemd/src/basic/fs-util.h8
-rw-r--r--shared/systemd/src/basic/hash-funcs.c3
-rw-r--r--shared/systemd/src/basic/hash-funcs.h1
-rw-r--r--shared/systemd/src/basic/hashmap.c39
-rw-r--r--shared/systemd/src/basic/hashmap.h4
-rw-r--r--shared/systemd/src/basic/hexdecoct.c25
-rw-r--r--shared/systemd/src/basic/hexdecoct.h5
-rw-r--r--shared/systemd/src/basic/in-addr-util.c30
-rw-r--r--shared/systemd/src/basic/in-addr-util.h2
-rw-r--r--shared/systemd/src/basic/io-util.c84
-rw-r--r--shared/systemd/src/basic/io-util.h17
-rw-r--r--shared/systemd/src/basic/log.h27
-rw-r--r--shared/systemd/src/basic/macro.h24
-rw-r--r--shared/systemd/src/basic/memory-util.h15
-rw-r--r--shared/systemd/src/basic/parse-util.c63
-rw-r--r--shared/systemd/src/basic/parse-util.h6
-rw-r--r--shared/systemd/src/basic/path-util.c151
-rw-r--r--shared/systemd/src/basic/path-util.h37
-rw-r--r--shared/systemd/src/basic/process-util.c280
-rw-r--r--shared/systemd/src/basic/process-util.h12
-rw-r--r--shared/systemd/src/basic/random-util.c191
-rw-r--r--shared/systemd/src/basic/random-util.h4
-rw-r--r--shared/systemd/src/basic/set.h20
-rw-r--r--shared/systemd/src/basic/siphash24.h1
-rw-r--r--shared/systemd/src/basic/socket-util.c33
-rw-r--r--shared/systemd/src/basic/socket-util.h3
-rw-r--r--shared/systemd/src/basic/string-util.c50
-rw-r--r--shared/systemd/src/basic/string-util.h26
-rw-r--r--shared/systemd/src/basic/strv.c69
-rw-r--r--shared/systemd/src/basic/strv.h5
-rw-r--r--shared/systemd/src/basic/strxcpyx.c118
-rw-r--r--shared/systemd/src/basic/strxcpyx.h14
-rw-r--r--shared/systemd/src/basic/time-util.c28
-rw-r--r--shared/systemd/src/basic/time-util.h14
-rw-r--r--shared/systemd/src/basic/tmpfile-util.c19
-rw-r--r--shared/systemd/src/basic/umask-util.h20
-rw-r--r--shared/systemd/src/basic/utf8.c76
-rw-r--r--shared/systemd/src/basic/utf8.h5
-rw-r--r--shared/systemd/src/shared/dns-domain.c1387
-rw-r--r--shared/systemd/src/shared/dns-domain.h114
142 files changed, 13327 insertions, 1325 deletions
diff --git a/shared/c-rbtree/src/c-rbtree-private.h b/shared/c-rbtree/src/c-rbtree-private.h
index 25b9ba01..c9befbf3 100644
--- a/shared/c-rbtree/src/c-rbtree-private.h
+++ b/shared/c-rbtree/src/c-rbtree-private.h
@@ -6,16 +6,11 @@
  * which are used by our test-suite.
  */
 
+#include <c-stdaux.h>
 #include <stddef.h>
 #include "c-rbtree.h"
 
 /*
- * Macros
- */
-
-#define _public_ __attribute__((__visibility__("default")))
-
-/*
  * Nodes
  */
 
diff --git a/shared/c-rbtree/src/c-rbtree.c b/shared/c-rbtree/src/c-rbtree.c
index 31d74300..aacdcc29 100644
--- a/shared/c-rbtree/src/c-rbtree.c
+++ b/shared/c-rbtree/src/c-rbtree.c
@@ -24,11 +24,11 @@
  */
 
 #include <assert.h>
+#include <c-stdaux.h>
 #include <stdalign.h>
 #include <stddef.h>
-
-#include "c-rbtree-private.h"
 #include "c-rbtree.h"
+#include "c-rbtree-private.h"
 
 /*
  * We use alignas(8) to enforce 64bit alignment of structure fields. This is
@@ -53,7 +53,7 @@ static_assert(alignof(CRBTree) >= 8, "Invalid CRBTree alignment");
  *
  * Return: Pointer to leftmost child, or NULL.
  */
-_public_ CRBNode *c_rbnode_leftmost(CRBNode *n) {
+_c_public_ CRBNode *c_rbnode_leftmost(CRBNode *n) {
         if (n)
                 while (n->left)
                         n = n->left;
@@ -72,7 +72,7 @@ _public_ CRBNode *c_rbnode_leftmost(CRBNode *n) {
  *
  * Return: Pointer to rightmost child, or NULL.
  */
-_public_ CRBNode *c_rbnode_rightmost(CRBNode *n) {
+_c_public_ CRBNode *c_rbnode_rightmost(CRBNode *n) {
         if (n)
                 while (n->right)
                         n = n->right;
@@ -94,7 +94,7 @@ _public_ CRBNode *c_rbnode_rightmost(CRBNode *n) {
  *
  * Return: Pointer to left-deepest child, or NULL.
  */
-_public_ CRBNode *c_rbnode_leftdeepest(CRBNode *n) {
+_c_public_ CRBNode *c_rbnode_leftdeepest(CRBNode *n) {
         if (n) {
                 for (;;) {
                         if (n->left)
@@ -123,7 +123,7 @@ _public_ CRBNode *c_rbnode_leftdeepest(CRBNode *n) {
  *
  * Return: Pointer to right-deepest child, or NULL.
  */
-_public_ CRBNode *c_rbnode_rightdeepest(CRBNode *n) {
+_c_public_ CRBNode *c_rbnode_rightdeepest(CRBNode *n) {
         if (n) {
                 for (;;) {
                         if (n->right)
@@ -149,7 +149,7 @@ _public_ CRBNode *c_rbnode_rightdeepest(CRBNode *n) {
  *
  * Return: Pointer to next node, or NULL.
  */
-_public_ CRBNode *c_rbnode_next(CRBNode *n) {
+_c_public_ CRBNode *c_rbnode_next(CRBNode *n) {
         CRBNode *p;
 
         if (!c_rbnode_is_linked(n))
@@ -175,7 +175,7 @@ _public_ CRBNode *c_rbnode_next(CRBNode *n) {
  *
  * Return: Pointer to previous node, or NULL.
  */
-_public_ CRBNode *c_rbnode_prev(CRBNode *n) {
+_c_public_ CRBNode *c_rbnode_prev(CRBNode *n) {
         CRBNode *p;
 
         if (!c_rbnode_is_linked(n))
@@ -209,7 +209,7 @@ _public_ CRBNode *c_rbnode_prev(CRBNode *n) {
  *
  * Return: Pointer to next node, or NULL.
  */
-_public_ CRBNode *c_rbnode_next_postorder(CRBNode *n) {
+_c_public_ CRBNode *c_rbnode_next_postorder(CRBNode *n) {
         CRBNode *p;
 
         if (!c_rbnode_is_linked(n))
@@ -253,7 +253,7 @@ _public_ CRBNode *c_rbnode_next_postorder(CRBNode *n) {
  *
  * Return: Pointer to previous node in post-order, or NULL.
  */
-_public_ CRBNode *c_rbnode_prev_postorder(CRBNode *n) {
+_c_public_ CRBNode *c_rbnode_prev_postorder(CRBNode *n) {
         CRBNode *p;
 
         if (!c_rbnode_is_linked(n))
@@ -283,8 +283,8 @@ _public_ CRBNode *c_rbnode_prev_postorder(CRBNode *n) {
  *
  * Return: Pointer to first node, or NULL.
  */
-_public_ CRBNode *c_rbtree_first(CRBTree *t) {
-        assert(t);
+_c_public_ CRBNode *c_rbtree_first(CRBTree *t) {
+        c_assert(t);
         return c_rbnode_leftmost(t->root);
 }
 
@@ -299,8 +299,8 @@ _public_ CRBNode *c_rbtree_first(CRBTree *t) {
  *
  * Return: Pointer to last node, or NULL.
  */
-_public_ CRBNode *c_rbtree_last(CRBTree *t) {
-        assert(t);
+_c_public_ CRBNode *c_rbtree_last(CRBTree *t) {
+        c_assert(t);
         return c_rbnode_rightmost(t->root);
 }
 
@@ -319,8 +319,8 @@ _public_ CRBNode *c_rbtree_last(CRBTree *t) {
  *
  * Return: Pointer to first node in post-order, or NULL.
  */
-_public_ CRBNode *c_rbtree_first_postorder(CRBTree *t) {
-        assert(t);
+_c_public_ CRBNode *c_rbtree_first_postorder(CRBTree *t) {
+        c_assert(t);
         return c_rbnode_leftdeepest(t->root);
 }
 
@@ -338,8 +338,8 @@ _public_ CRBNode *c_rbtree_first_postorder(CRBTree *t) {
  *
  * Return: Pointer to last node in post-order, or NULL.
  */
-_public_ CRBNode *c_rbtree_last_postorder(CRBTree *t) {
-        assert(t);
+_c_public_ CRBNode *c_rbtree_last_postorder(CRBTree *t) {
+        c_assert(t);
         return t->root;
 }
 
@@ -452,15 +452,14 @@ static inline void c_rbnode_swap_child(CRBNode *old, CRBNode *new) {
  * Note that this operates in O(1) time. Only the root-entry is updated to
  * point to the new tree-root.
  */
-_public_ void c_rbtree_move(CRBTree *to, CRBTree *from) {
+_c_public_ void c_rbtree_move(CRBTree *to, CRBTree *from) {
         CRBTree *t;
 
-        assert(!to->root);
+        c_assert(!to->root);
 
         if (from->root) {
                 t = c_rbnode_pop_root(from->root);
-                assert(t == from);
-                (void)t;
+                c_assert(t == from);
 
                 to->root = from->root;
                 from->root = NULL;
@@ -487,10 +486,10 @@ static inline void c_rbtree_paint_terminal(CRBNode *n) {
         g = c_rbnode_parent(p);
         gg = c_rbnode_parent(g);
 
-        assert(c_rbnode_is_red(p));
-        assert(c_rbnode_is_black(g));
-        assert(p == g->left || !g->left || c_rbnode_is_black(g->left));
-        assert(p == g->right || !g->right || c_rbnode_is_black(g->right));
+        c_assert(c_rbnode_is_red(p));
+        c_assert(c_rbnode_is_black(g));
+        c_assert(p == g->left || !g->left || c_rbnode_is_black(g->left));
+        c_assert(p == g->right || !g->right || c_rbnode_is_black(g->right));
 
         if (p == g->left) {
                 if (n == p->right) {
@@ -674,11 +673,11 @@ static inline void c_rbtree_paint(CRBNode *n) {
  * In most cases you are better off using c_rbtree_add(). See there for details
  * how tree-insertion works.
  */
-_public_ void c_rbnode_link(CRBNode *p, CRBNode **l, CRBNode *n) {
-        assert(p);
-        assert(l);
-        assert(n);
-        assert(l == &p->left || l == &p->right);
+_c_public_ void c_rbnode_link(CRBNode *p, CRBNode **l, CRBNode *n) {
+        c_assert(p);
+        c_assert(l);
+        c_assert(n);
+        c_assert(l == &p->left || l == &p->right);
 
         c_rbnode_set_parent_and_flags(n, p, C_RBNODE_RED);
         c_rbtree_store(&n->left, NULL);
@@ -739,12 +738,12 @@ _public_ void c_rbnode_link(CRBNode *p, CRBNode **l, CRBNode *n) {
  * than c_rbnode_unlink_stale()). In those cases, you should validate that a
  * node is unlinked before you call c_rbtree_add().
  */
-_public_ void c_rbtree_add(CRBTree *t, CRBNode *p, CRBNode **l, CRBNode *n) {
-        assert(t);
-        assert(l);
-        assert(n);
-        assert(!p || l == &p->left || l == &p->right);
-        assert(p || l == &t->root);
+_c_public_ void c_rbtree_add(CRBTree *t, CRBNode *p, CRBNode **l, CRBNode *n) {
+        c_assert(t);
+        c_assert(l);
+        c_assert(n);
+        c_assert(!p || l == &p->left || l == &p->right);
+        c_assert(p || l == &t->root);
 
         c_rbnode_set_parent_and_flags(n, p, C_RBNODE_RED);
         c_rbtree_store(&n->left, NULL);
@@ -796,7 +795,7 @@ static inline void c_rbnode_rebalance_terminal(CRBNode *p, CRBNode *previous) {
                                  * Note that the parent must be red, otherwise
                                  * it must have been handled by our caller.
                                  */
-                                assert(c_rbnode_is_red(p));
+                                c_assert(c_rbnode_is_red(p));
                                 c_rbnode_set_parent_and_flags(s, p, c_rbnode_flags(s) | C_RBNODE_RED);
                                 c_rbnode_set_parent_and_flags(p, c_rbnode_parent(p), c_rbnode_flags(p) & ~C_RBNODE_RED);
                                 return;
@@ -856,7 +855,7 @@ static inline void c_rbnode_rebalance_terminal(CRBNode *p, CRBNode *previous) {
                 if (!x || c_rbnode_is_black(x)) {
                         y = s->right;
                         if (!y || c_rbnode_is_black(y)) {
-                                assert(c_rbnode_is_red(p));
+                                c_assert(c_rbnode_is_red(p));
                                 c_rbnode_set_parent_and_flags(s, p, c_rbnode_flags(s) | C_RBNODE_RED);
                                 c_rbnode_set_parent_and_flags(p, c_rbnode_parent(p), c_rbnode_flags(p) & ~C_RBNODE_RED);
                                 return;
@@ -963,11 +962,11 @@ static inline void c_rbnode_rebalance(CRBNode *n) {
  * This does *NOT* reset @n to being unlinked. If you need this, use
  * c_rbtree_unlink().
  */
-_public_ void c_rbnode_unlink_stale(CRBNode *n) {
+_c_public_ void c_rbnode_unlink_stale(CRBNode *n) {
         CRBTree *t;
 
-        assert(n);
-        assert(c_rbnode_is_linked(n));
+        c_assert(n);
+        c_assert(c_rbnode_is_linked(n));
 
         /*
          * There are three distinct cases during node removal of a tree:
diff --git a/shared/c-siphash/src/c-siphash.c b/shared/c-siphash/src/c-siphash.c
index 5cea6f2b..fae3abad 100644
--- a/shared/c-siphash/src/c-siphash.c
+++ b/shared/c-siphash/src/c-siphash.c
@@ -11,12 +11,11 @@
  * C_siphash_finalize_Y() can be easily provided, if required.
  */
 
+#include <c-stdaux.h>
 #include <stddef.h>
 #include <stdint.h>
 #include "c-siphash.h"
 
-#define _public_ __attribute__((__visibility__("default")))
-
 static inline uint64_t c_siphash_read_le64(const uint8_t bytes[8]) {
         return  ((uint64_t) bytes[0]) |
                (((uint64_t) bytes[1]) <<  8) |
@@ -68,7 +67,7 @@ static inline void c_siphash_sipround(CSipHash *state) {
  * Right now, only SipHash24 is supported. Other SipHash parameters can be
  * easily added if required.
  */
-_public_ void c_siphash_init(CSipHash *state, const uint8_t seed[16]) {
+_c_public_ void c_siphash_init(CSipHash *state, const uint8_t seed[16]) {
         uint64_t k0, k1;
 
         k0 = c_siphash_read_le64(seed);
@@ -105,7 +104,7 @@ _public_ void c_siphash_init(CSipHash *state, const uint8_t seed[16]) {
  * Note that this implementation works best when used with chunk-sizes of
  * multiples of 64bit (8-bytes). This is not a requirement, though.
  */
-_public_ void c_siphash_append(CSipHash *state, const uint8_t *bytes, size_t n_bytes) {
+_c_public_ void c_siphash_append(CSipHash *state, const uint8_t *bytes, size_t n_bytes) {
         const uint8_t *end = bytes + n_bytes;
         size_t left = state->n_bytes & 7;
         uint64_t m;
@@ -195,7 +194,7 @@ _public_ void c_siphash_append(CSipHash *state, const uint8_t *bytes, size_t n_b
  *
  * Return: 64bit hash value
  */
-_public_ uint64_t c_siphash_finalize(CSipHash *state) {
+_c_public_ uint64_t c_siphash_finalize(CSipHash *state) {
         uint64_t b;
 
         b = state->padding | (((uint64_t) state->n_bytes) << 56);
@@ -236,7 +235,7 @@ _public_ uint64_t c_siphash_finalize(CSipHash *state) {
  *
  * Return: 64bit hash value
  */
-_public_ uint64_t c_siphash_hash(const uint8_t seed[16], const uint8_t *bytes, size_t n_bytes) {
+_c_public_ uint64_t c_siphash_hash(const uint8_t seed[16], const uint8_t *bytes, size_t n_bytes) {
         CSipHash state;
 
         c_siphash_init(&state, seed);
diff --git a/shared/c-stdaux/src/c-stdaux.h b/shared/c-stdaux/src/c-stdaux.h
new file mode 100644
index 00000000..08d155ce
--- /dev/null
+++ b/shared/c-stdaux/src/c-stdaux.h
@@ -0,0 +1,546 @@
+#pragma once
+
+/*
+ * Auxiliary macros and functions for the C standard library
+ *
+ * The `c-stdaux.h` header contains a collection of auxiliary macros and helper
+ * functions around the functionality provided by the different C standard
+ * library implementations, as well as other specifications implemented by
+ * them.
+ *
+ * Most of the helpers provided here provide aliases for common library and
+ * compiler features. Furthermore, several helpers simply provide other calling
+ * conventions than their standard counterparts (e.g., they allow for NULL to
+ * be passed with an object length of 0 where it makes sense to accept empty
+ * input).
+ *
+ * The namespace used by this project is:
+ *
+ *  * `c_*` for all common C symbols or definitions that behave like proper C
+ *    entities (e.g., macros that protect against double-evaluation would use
+ *    lower-case names)
+ *
+ *  * `C_*` for all constants, as well as macros that may not be safe against
+ *    double evaluation.
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <assert.h>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdalign.h>
+#include <stdarg.h>
+#if 0 /* NM_IGNORED */
+#include <stdatomic.h>
+#endif /* NM_IGNORED */
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdnoreturn.h>
+#include <string.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+/*
+ * Shortcuts for gcc attributes. See GCC manual for details. They're 1-to-1
+ * mappings to the GCC equivalents. No additional magic here. They are
+ * supported by other compilers as well.
+ */
+#define _c_cleanup_(_x) __attribute__((__cleanup__(_x)))
+#define _c_const_ __attribute__((__const__))
+#define _c_deprecated_ __attribute__((__deprecated__))
+#define _c_hidden_ __attribute__((__visibility__("hidden")))
+#define _c_likely_(_x) (__builtin_expect(!!(_x), 1))
+#define _c_packed_ __attribute__((__packed__))
+#define _c_printf_(_a, _b) __attribute__((__format__(printf, _a, _b)))
+#define _c_public_ __attribute__((__visibility__("default")))
+#define _c_pure_ __attribute__((__pure__))
+#define _c_sentinel_ __attribute__((__sentinel__))
+#define _c_unlikely_(_x) (__builtin_expect(!!(_x), 0))
+#define _c_unused_ __attribute__((__unused__))
+
+/**
+ * C_EXPR_ASSERT() - create expression with assertion
+ * @_expr:              expression to evaluate to
+ * @_assertion:         arbitrary assertion
+ * @_message:           message associated with the assertion
+ *
+ * This macro simply evaluates to @_expr. That is, it can be used in any
+ * context that expects an expression like @_expr. Additionally, it takes an
+ * assertion as @_assertion and evaluates it through _Static_assert(), using
+ * @_message as debug message.
+ *
+ * The _Static_assert() builtin of C11 is defined as statement and thus cannot
+ * be used in expressions. This macro circumvents this restriction.
+ *
+ * Return: Evaluates to @_expr.
+ */
+#define C_EXPR_ASSERT(_expr, _assertion, _message)                      \
+        /* indentation and line-split to get better diagnostics */      \
+        (__builtin_choose_expr(                                         \
+                !!(1 + 0 * sizeof(                                      \
+                        struct {                                        \
+_Static_assert(_assertion, _message); \
+                        }                                               \
+                )),                                                     \
+                (_expr),                                                \
+                ((void)0)                                               \
+        ))
+
+/**
+ * C_STRINGIFY() - stringify a token, but evaluate it first
+ * @_x:         token to evaluate and stringify
+ *
+ * Return: Evaluates to a constant string literal
+ */
+#define C_STRINGIFY(_x) C_INTERNAL_STRINGIFY(_x)
+#define C_INTERNAL_STRINGIFY(_x) #_x
+
+/**
+ * C_CONCATENATE() - concatenate two tokens, but evaluate them first
+ * @_x:         first token
+ * @_y:         second token
+ *
+ * Return: Evaluates to a constant identifier
+ */
+#define C_CONCATENATE(_x, _y) C_INTERNAL_CONCATENATE(_x, _y)
+#define C_INTERNAL_CONCATENATE(_x, _y) _x ## _y
+
+/**
+ * C_EXPAND() - expand a tuple to a series of its values
+ * @_x:         tuple to expand
+ *
+ * Return: Evaluates to the expanded tuple
+ */
+#define C_EXPAND(_x) C_INTERNAL_EXPAND _x
+#define C_INTERNAL_EXPAND(...) __VA_ARGS__
+
+/**
+ * C_VAR() - generate unique variable name
+ * @_x:         name of variable, optional
+ * @_uniq:      unique prefix, usually provided by __COUNTER__, optional
+ *
+ * This macro shall be used to generate unique variable names, that will not be
+ * shadowed by recursive macro invocations. It is effectively a
+ * C_CONCATENATE of both arguments, but also provides a globally separated
+ * prefix and makes the code better readable.
+ *
+ * The second argument is optional. If not given, __LINE__ is implied, and as
+ * such the macro will generate the same identifier if used multiple times on
+ * the same code-line (or within a macro). This should be used if recursive
+ * calls into the macro are not expected. In fact, no argument is necessary in
+ * this case, as a mere `C_VAR` will evaluate to a valid variable name.
+ *
+ * This helper may be used by macro implementations that might reasonable well
+ * be called in a stacked fasion, like:
+ *
+ *     c_max(foo, c_max(bar, baz))
+ *
+ * Such a stacked call of c_max() might cause compiler warnings of shadowed
+ * variables in the definition of c_max(). By using C_VAR(), such warnings
+ * can be silenced as each evaluation of c_max() uses unique variable names.
+ *
+ * Return: This evaluates to a constant identifier.
+ */
+#define C_VAR(...) C_INTERNAL_VAR(__VA_ARGS__, 2, 1)
+#define C_INTERNAL_VAR(_x, _uniq, _num, ...) C_VAR ## _num (_x, _uniq)
+#define C_VAR1(_x, _unused) C_VAR2(_x, C_CONCATENATE(line, __LINE__))
+#define C_VAR2(_x, _uniq) C_CONCATENATE(c_internal_var_unique_, C_CONCATENATE(_uniq, _x))
+
+/**
+ * C_CC_MACRO1() - provide safe environment to a macro
+ * @_call:      macro to call
+ * @_x1:        first argument
+ * @...:        further arguments to forward unmodified to @_call
+ *
+ * This function simplifies the implementation of macros. Whenever you
+ * implement a macro, provide the internal macro name as @_call and its
+ * argument as @_x1. Inside of your internal macro, you...
+ *
+ *  - ...are safe against multiple evaluation errors, since C_CC_MACRO1 will
+ *       store the initial parameters in temporary variables.
+ *
+ *  - ...support constant folding, as C_CC_MACRO1 takes care to invoke your
+ *       macro with the original values, if they are compile-time constant.
+ *
+ *  - ...have unique variable names for recursive callers and will not run into
+ *       variable-shadowing-warnings accidentally.
+ *
+ *  - ...have properly typed arguments as C_CC_MACRO1 stores the original
+ *       arguments in an `__auto_type` temporary variable.
+ *
+ * Return: Result of @_call is returned.
+ */
+#define C_CC_MACRO1(_call, _x1, ...) C_INTERNAL_CC_MACRO1(_call, __COUNTER__, (_x1), ## __VA_ARGS__)
+#define C_INTERNAL_CC_MACRO1(_call, _x1q, _x1, ...)                     \
+        __builtin_choose_expr(                                          \
+                __builtin_constant_p(_x1),                              \
+                _call(_x1, ## __VA_ARGS__),                             \
+                __extension__ ({                                        \
+                        const __auto_type C_VAR(X1, _x1q) = (_x1);      \
+                        _call(C_VAR(X1, _x1q), ## __VA_ARGS__);         \
+                }))
+
+/**
+ * C_CC_MACRO2() - provide safe environment to a macro
+ * @_call:      macro to call
+ * @_x1:        first argument
+ * @_x2:        second argument
+ * @...:        further arguments to forward unmodified to @_call
+ *
+ * This is the 2-argument equivalent of C_CC_MACRO1().
+ *
+ * Return: Result of @_call is returned.
+ */
+#define C_CC_MACRO2(_call, _x1, _x2, ...) C_INTERNAL_CC_MACRO2(_call, __COUNTER__, (_x1), __COUNTER__, (_x2), ## __VA_ARGS__)
+#define C_INTERNAL_CC_MACRO2(_call, _x1q, _x1, _x2q, _x2, ...)                          \
+        __builtin_choose_expr(                                                          \
+                (__builtin_constant_p(_x1) && __builtin_constant_p(_x2)),               \
+                _call((_x1), (_x2), ## __VA_ARGS__),                                    \
+                __extension__ ({                                                        \
+                        const __auto_type C_VAR(X1, _x1q) = (_x1);                      \
+                        const __auto_type C_VAR(X2, _x2q) = (_x2);                      \
+                        _call(C_VAR(X1, _x1q), C_VAR(X2, _x2q), ## __VA_ARGS__);        \
+                }))
+
+/**
+ * C_CC_MACRO3() - provide safe environment to a macro
+ * @_call:      macro to call
+ * @_x1:        first argument
+ * @_x2:        second argument
+ * @_x3:        third argument
+ * @...:        further arguments to forward unmodified to @_call
+ *
+ * This is the 3-argument equivalent of C_CC_MACRO1().
+ *
+ * Return: Result of @_call is returned.
+ */
+#define C_CC_MACRO3(_call, _x1, _x2, _x3, ...) C_INTERNAL_CC_MACRO3(_call, __COUNTER__, (_x1), __COUNTER__, (_x2), __COUNTER__, (_x3), ## __VA_ARGS__)
+#define C_INTERNAL_CC_MACRO3(_call, _x1q, _x1, _x2q, _x2, _x3q, _x3, ...)                               \
+        __builtin_choose_expr(                                                                          \
+                (__builtin_constant_p(_x1) && __builtin_constant_p(_x2) && __builtin_constant_p(_x3)),  \
+                _call((_x1), (_x2), (_x3), ## __VA_ARGS__),                                             \
+                __extension__ ({                                                                        \
+                        const __auto_type C_VAR(X1, _x1q) = (_x1);                                      \
+                        const __auto_type C_VAR(X2, _x2q) = (_x2);                                      \
+                        const __auto_type C_VAR(X3, _x3q) = (_x3);                                      \
+                        _call(C_VAR(X1, _x1q), C_VAR(X2, _x2q), C_VAR(X3, _x3q), ## __VA_ARGS__);       \
+                }))
+
+/**
+ * C_ARRAY_SIZE() - calculate number of array elements at compile time
+ * @_x:         array to calculate size of
+ *
+ * Return: Evaluates to a constant integer expression.
+ */
+#define C_ARRAY_SIZE(_x)                                                \
+        C_EXPR_ASSERT(sizeof(_x) / sizeof((_x)[0]),                     \
+               /*                                                       \
+                * Verify that `_x' is an array, not a pointer. Rely on  \
+                * `&_x[0]' degrading arrays to pointers.                \
+                */                                                      \
+                !__builtin_types_compatible_p(                          \
+                        __typeof__(_x),                                 \
+                        __typeof__(&(*(__typeof__(_x)*)0)[0])           \
+                ),                                                      \
+                "C_ARRAY_SIZE() called with non-array argument"         \
+        )
+
+/**
+ * C_DECIMAL_MAX() - calculate maximum length of the decimal
+ *                   representation of an integer
+ * @_type: integer variable/type
+ *
+ * This calculates the bytes required for the decimal representation of an
+ * integer of the given type. It accounts for a possible +/- prefix, but it
+ * does *NOT* include the trailing terminating zero byte.
+ *
+ * Return: Evaluates to a constant integer expression
+ */
+#define C_DECIMAL_MAX(_arg)                                                             \
+        (_Generic((__typeof__(_arg)){ 0 },                                              \
+                        char: C_INTERNAL_DECIMAL_MAX(sizeof(char)),                     \
+                 signed char: C_INTERNAL_DECIMAL_MAX(sizeof(signed char)),              \
+               unsigned char: C_INTERNAL_DECIMAL_MAX(sizeof(unsigned char)),            \
+                signed short: C_INTERNAL_DECIMAL_MAX(sizeof(signed short)),             \
+              unsigned short: C_INTERNAL_DECIMAL_MAX(sizeof(unsigned short)),           \
+                  signed int: C_INTERNAL_DECIMAL_MAX(sizeof(signed int)),               \
+                unsigned int: C_INTERNAL_DECIMAL_MAX(sizeof(unsigned int)),             \
+                 signed long: C_INTERNAL_DECIMAL_MAX(sizeof(signed long)),              \
+               unsigned long: C_INTERNAL_DECIMAL_MAX(sizeof(unsigned long)),            \
+            signed long long: C_INTERNAL_DECIMAL_MAX(sizeof(signed long long)),         \
+          unsigned long long: C_INTERNAL_DECIMAL_MAX(sizeof(unsigned long long))))
+#define C_INTERNAL_DECIMAL_MAX(_bytes)                                          \
+        C_EXPR_ASSERT(                                                          \
+                1 + ((_bytes) <= 1 ?  3 :                                       \
+                     (_bytes) <= 2 ?  5 :                                       \
+                     (_bytes) <= 4 ? 10 :                                       \
+                                     20),                                       \
+                (_bytes) <= 8,                                                  \
+                "Invalid use of C_INTERNAL_DECIMAL_MAX()"                       \
+        )
+
+/**
+ * c_container_of() - cast a member of a structure out to the containing structure
+ * @_ptr:       pointer to the member or NULL
+ * @_type:      type of the container struct this is embedded in
+ * @_member:    name of the member within the struct
+ *
+ * This uses `offsetof(3)` to turn a pointer to a structure-member into a
+ * pointer to the surrounding structure.
+ *
+ * Return: Pointer to the surrounding object.
+ */
+#define c_container_of(_ptr, _type, _member) C_CC_MACRO1(C_CONTAINER_OF, (_ptr), _type, _member)
+#define C_CONTAINER_OF(_ptr, _type, _member)                                            \
+        __extension__ ({                                                                \
+                /* trigger warning if types do not match */                             \
+                (void)(&((_type *)0)->_member == (_ptr));                               \
+                _ptr ? (_type*)( (char*)_ptr - offsetof(_type, _member) ) : NULL;       \
+        })
+
+/**
+ * c_max() - compute maximum of two values
+ * @_a:         value A
+ * @_b:         value B
+ *
+ * Calculate the maximum of both passed values. Both arguments are evaluated
+ * exactly once, under all circumstances. Furthermore, if both values are
+ * constant expressions, the result will be constant as well.
+ *
+ * The comparison of their values is performed with the types given by the
+ * caller. It is the caller's responsibility to convert them to suitable types
+ * if necessary.
+ *
+ * Return: Maximum of both values is returned.
+ */
+#define c_max(_a, _b) C_CC_MACRO2(C_MAX, (_a), (_b))
+#define C_MAX(_a, _b) ((_a) > (_b) ? (_a) : (_b))
+
+/**
+ * c_min() - compute minimum of two values
+ * @_a:         value A
+ * @_b:         value B
+ *
+ * Calculate the minimum of both passed values. Both arguments are evaluated
+ * exactly once, under all circumstances. Furthermore, if both values are
+ * constant expressions, the result will be constant as well.
+ *
+ * The comparison of their values is performed with the types given by the
+ * caller. It is the caller's responsibility to convert them to suitable types
+ * if necessary.
+ *
+ * Return: Minimum of both values is returned.
+ */
+#define c_min(_a, _b) C_CC_MACRO2(C_MIN, (_a), (_b))
+#define C_MIN(_a, _b) ((_a) < (_b) ? (_a) : (_b))
+
+/**
+ * c_less_by() - calculate clamped difference of two values
+ * @_a:         minuend
+ * @_b:         subtrahend
+ *
+ * Calculate [_a - _b], but clamp the result to 0. Both arguments are evaluated
+ * exactly once, under all circumstances. Furthermore, if both values are
+ * constant expressions, the result will be constant as well.
+ *
+ * The comparison of their values is performed with the types given by the
+ * caller. It is the caller's responsibility to convert them to suitable types
+ * if necessary.
+ *
+ * Return: This computes [_a - _b], if [_a > _b]. Otherwise, 0 is returned.
+ */
+#define c_less_by(_a, _b) C_CC_MACRO2(C_LESS_BY, (_a), (_b))
+#define C_LESS_BY(_a, _b) ((_a) > (_b) ? (_a) - (_b) : 0)
+
+/**
+ * c_clamp() - clamp value to lower and upper boundary
+ * @_x:         value to clamp
+ * @_low:       lower boundary
+ * @_high:      higher boundary
+ *
+ * This clamps @_x to the lower and higher bounds given as @_low and @_high.
+ * All arguments are evaluated exactly once, and yield a constant expression if
+ * all arguments are constant as well.
+ *
+ * The comparison of their values is performed with the types given by the
+ * caller. It is the caller's responsibility to convert them to suitable types
+ * if necessary.
+ *
+ * Return: Clamped integer value.
+ */
+#define c_clamp(_x, _low, _high) C_CC_MACRO3(C_CLAMP, (_x), (_low), (_high))
+#define C_CLAMP(_x, _low, _high) ((_x) > (_high) ? (_high) : (_x) < (_low) ? (_low) : (_x))
+
+/**
+ * c_div_round_up() - calculate integer quotient but round up
+ * @_x:         dividend
+ * @_y:         divisor
+ *
+ * Calculates [x / y] but rounds up the result to the next integer. All
+ * arguments are evaluated exactly once, and yield a constant expression if all
+ * arguments are constant.
+ *
+ * Note:
+ * [(x + y - 1) / y] suffers from an integer overflow, even though the
+ * computation should be possible in the given type. Therefore, we use
+ * [x / y + !!(x % y)]. Note that on most CPUs a division returns both the
+ * quotient and the remainder, so both should be equally fast. Furthermore, if
+ * the divisor is a power of two, the compiler will optimize it, anyway.
+ *
+ * The operationsare performed with the types given by the caller. It is the
+ * caller's responsibility to convert the arguments to suitable types if
+ * necessary.
+ *
+ * Return: The quotient is returned.
+ */
+#define c_div_round_up(_x, _y) C_CC_MACRO2(C_DIV_ROUND_UP, (_x), (_y))
+#define C_DIV_ROUND_UP(_x, _y) ((_x) / (_y) + !!((_x) % (_y)))
+
+/**
+ * c_align_to() - align value to a multiple
+ * @_val:       value to align
+ * @_to:        align to multiple of this
+ *
+ * This aligns @_val to a multiple of @_to. If @_val is already a multiple of
+ * @_to, @_val is returned unchanged. This function operates within the
+ * boundaries of the type of @_val and @_to. Make sure to cast them if needed.
+ *
+ * The arguments of this macro are evaluated exactly once. If both arguments
+ * are a constant expression, this also yields a constant return value.
+ *
+ * Note that @_to must be a power of 2, otherwise the behavior will not match
+ * expectations.
+ *
+ * Return: @_val aligned to a multiple of @_to
+ */
+#define c_align_to(_val, _to) C_CC_MACRO2(C_ALIGN_TO, (_val), (_to))
+#define C_ALIGN_TO(_val, _to) (((_val) + (_to) - 1) & ~((_to) - 1))
+
+/**
+ * c_assert() - runtime assertions
+ * @expr_result:                result of an expression
+ *
+ * This function behaves like the standard `assert(3)` macro. That is, if
+ * `NDEBUG` is defined, it is a no-op. In all other cases it will assert that
+ * the result of the passed expression is true.
+ *
+ * Unlike the standard `assert(3)` macro, this function always evaluates its
+ * argument. This means side-effects will always be evaluated! However, if the
+ * macro is used with constant expressions, the compiler will be able to
+ * optimize it away.
+ */
+#define c_assert(_x) ({                                                         \
+                const _c_unused_ bool c_assert_result = (_x);                   \
+                assert(c_assert_result && #_x);                                 \
+        })
+
+/**
+ * c_errno() - return valid errno
+ *
+ * This helper should be used to shut up gcc if you know 'errno' is valid (ie.,
+ * errno is > 0). Instead of "return -errno;", use
+ * "return -c_errno();" It will suppress bogus gcc warnings in case it assumes
+ * 'errno' might be 0 (or <0) and thus the caller's error-handling might not be
+ * triggered.
+ *
+ * This helper should be avoided whenever possible. However, occasionally we
+ * really want to shut up gcc (especially with static/inline functions). In
+ * those cases, gcc usually cannot deduce that some error paths are guaranteed
+ * to be taken. Hence, making the return value explicit allows gcc to better
+ * optimize the code.
+ *
+ * Note that you really should never use this helper to work around broken libc
+ * calls or syscalls, not setting 'errno' correctly.
+ *
+ * Return: Positive error code is returned.
+ */
+static inline int c_errno(void) {
+        return _c_likely_(errno > 0) ? errno : ENOTRECOVERABLE;
+}
+
+/*
+ * Common Destructors
+ *
+ * Followingly, there're a bunch of common 'static inline' destructors, which
+ * simply call the function that they're named after, but return "INVALID"
+ * instead of "void". This allows direct assignment to any member-field and/or
+ * variable they're defined in, like:
+ *
+ *   foo = c_free(foo);
+ *
+ * or
+ *
+ *   foo->bar = c_close(foo->bar);
+ *
+ * Furthermore, all those destructors can be safely called with the "INVALID"
+ * value as argument, and they will be a no-op.
+ */
+
+static inline void *c_free(void *p) {
+        free(p);
+        return NULL;
+}
+
+static inline int c_close(int fd) {
+        if (fd >= 0)
+                close(fd);
+        return -1;
+}
+
+static inline FILE *c_fclose(FILE *f) {
+        if (f)
+                fclose(f);
+        return NULL;
+}
+
+static inline DIR *c_closedir(DIR *d) {
+        if (d)
+                closedir(d);
+        return NULL;
+}
+
+/*
+ * Common Cleanup Helpers
+ *
+ * A bunch of _c_cleanup_(foobarp) helpers that are used all over the place.
+ * Note that all of those have the "if (IS_INVALID(foobar))" check inline, so
+ * compilers can optimize most of the cleanup-paths in a function. However, if
+ * the function they call already does this _inline_, then it might be skipped.
+ */
+
+#define C_DEFINE_CLEANUP(_type, _func)                                          \
+        static inline void _func ## p(_type *p) {                               \
+                if (*p)                                                         \
+                        _func(*p);                                              \
+        } struct c_internal_trailing_semicolon
+
+#define C_DEFINE_DIRECT_CLEANUP(_type, _func)                                   \
+        static inline void _func ## p(_type *p) {                               \
+                _func(*p);                                                      \
+        } struct c_internal_trailing_semicolon
+
+static inline void c_freep(void *p) {
+        /*
+         * `foobar **` does not coerce to `void **`, so we need `void *` as
+         * argument type, and then we dereference manually.
+         */
+        c_free(*(void **)p);
+}
+
+C_DEFINE_DIRECT_CLEANUP(int, c_close);
+C_DEFINE_CLEANUP(FILE *, c_fclose);
+C_DEFINE_CLEANUP(DIR *, c_closedir);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/shared/meson.build b/shared/meson.build
index ed9bf03f..af903d3c 100644
--- a/shared/meson.build
+++ b/shared/meson.build
@@ -2,8 +2,23 @@ shared_inc = include_directories('.')
 
 ###############################################################################
 
+shared_c_stdaux = static_library(
+    'c-stdaux',
+    c_args: '-std=c11',
+    sources: files('c-stdaux/src/c-stdaux.h'),
+)
+
+shared_c_stdaux_dep = declare_dependency(
+    include_directories: shared_inc,
+)
+
+###############################################################################
+
 shared_c_siphash = static_library(
     'c-siphash',
+    include_directories: [
+        include_directories('c-stdaux/src'),
+    ],
     sources: 'c-siphash/src/c-siphash.c',
 )
 
@@ -17,6 +32,9 @@ shared_c_siphash_dep = declare_dependency(
 shared_c_rbtree = static_library(
     'c-rbtree',
     c_args: '-std=c11',
+    include_directories: [
+        include_directories('c-stdaux/src'),
+    ],
     sources: files('c-rbtree/src/c-rbtree.c',
                    'c-rbtree/src/c-rbtree.h',
                    'c-rbtree/src/c-rbtree-private.h'),
@@ -52,6 +70,7 @@ shared_n_acd = static_library(
         '-Wno-vla',
     ],
     include_directories: [
+        include_directories('c-stdaux/src'),
         include_directories('c-siphash/src'),
         include_directories('c-list/src'),
         include_directories('c-rbtree/src'),
@@ -69,6 +88,43 @@ shared_n_acd_dep = declare_dependency(
 
 ###############################################################################
 
+shared_n_dhcp4 = static_library(
+    'n-dhcp4',
+    sources: files('n-dhcp4/src/n-dhcp4-c-connection.c',
+                   'n-dhcp4/src/n-dhcp4-c-lease.c',
+                   'n-dhcp4/src/n-dhcp4-c-probe.c',
+                   'n-dhcp4/src/n-dhcp4-client.c',
+                   'n-dhcp4/src/n-dhcp4-incoming.c',
+                   'n-dhcp4/src/n-dhcp4-outgoing.c',
+                   'n-dhcp4/src/n-dhcp4-private.h',
+                   'n-dhcp4/src/n-dhcp4-socket.c',
+                   'n-dhcp4/src/n-dhcp4.h',
+                   'n-dhcp4/src/util/packet.c',
+                   'n-dhcp4/src/util/packet.h',
+                   'n-dhcp4/src/util/socket.c',
+                   'n-dhcp4/src/util/socket.h'),
+    c_args: [
+        '-D_GNU_SOURCE',
+        '-Wno-declaration-after-statement',
+        '-Wno-pointer-arith',
+    ],
+    include_directories: [
+        include_directories('c-list/src'),
+        include_directories('c-siphash/src'),
+        include_directories('c-stdaux/src'),
+    ],
+    dependencies: [
+        shared_c_siphash_dep,
+    ],
+)
+
+shared_n_dhcp4_dep = declare_dependency(
+    include_directories: shared_inc,
+    link_with: shared_n_dhcp4,
+)
+
+###############################################################################
+
 version_conf = configuration_data()
 version_conf.set('NM_MAJOR_VERSION', nm_major_version)
 version_conf.set('NM_MINOR_VERSION', nm_minor_version)
@@ -120,11 +176,14 @@ shared_nm_glib_aux_c_args = [
 
 shared_nm_glib_aux = static_library(
     'nm-utils-base',
-    sources: files('nm-glib-aux/nm-dedup-multi.c',
+    sources: files('nm-glib-aux/nm-dbus-aux.c',
+                   'nm-glib-aux/nm-dedup-multi.c',
                    'nm-glib-aux/nm-enum-utils.c',
                    'nm-glib-aux/nm-errno.c',
                    'nm-glib-aux/nm-hash-utils.c',
                    'nm-glib-aux/nm-io-utils.c',
+                   'nm-glib-aux/nm-json-aux.c',
+                   'nm-glib-aux/nm-keyfile-aux.c',
                    'nm-glib-aux/nm-random-utils.c',
                    'nm-glib-aux/nm-secret-utils.c',
                    'nm-glib-aux/nm-shared-utils.c',
@@ -192,6 +251,7 @@ libnm_systemd_shared = static_library(
     'systemd/src/basic/extract-word.c',
     'systemd/src/basic/fd-util.c',
     'systemd/src/basic/fileio.c',
+    'systemd/src/basic/format-util.c',
     'systemd/src/basic/fs-util.c',
     'systemd/src/basic/hash-funcs.c',
     'systemd/src/basic/hashmap.c',
@@ -211,15 +271,18 @@ libnm_systemd_shared = static_library(
     'systemd/src/basic/string-table.c',
     'systemd/src/basic/string-util.c',
     'systemd/src/basic/strv.c',
+    'systemd/src/basic/strxcpyx.c',
     'systemd/src/basic/time-util.c',
     'systemd/src/basic/tmpfile-util.c',
     'systemd/src/basic/utf8.c',
     'systemd/src/basic/util.c',
+    'systemd/src/shared/dns-domain.c',
     'systemd/nm-sd-utils-shared.c',
   ),
   include_directories: include_directories(
     'systemd/sd-adapt-shared',
     'systemd/src/basic',
+    'systemd/src/shared',
   ),
   dependencies: shared_nm_glib_aux_dep,
   c_args: [
@@ -232,6 +295,7 @@ libnm_systemd_shared_dep = declare_dependency(
   include_directories: include_directories(
     'systemd/sd-adapt-shared',
     'systemd/src/basic',
+    'systemd/src/shared',
   ),
   dependencies: [
     shared_nm_glib_aux_dep,
@@ -268,18 +332,22 @@ libnm_systemd_shared_no_logging_dep = declare_dependency(
 
 ###############################################################################
 
-test_shared_general = executable(
-  'nm-utils/tests/test-shared-general',
-  [ 'nm-utils/tests/test-shared-general.c', ],
-  c_args: [
-      '-DNETWORKMANAGER_COMPILATION_TEST',
-      '-DNETWORKMANAGER_COMPILATION=(NM_NETWORKMANAGER_COMPILATION_GLIB|NM_NETWORKMANAGER_COMPILATION_WITH_GLIB_I18N_PROG)',
+exe = executable(
+    'nm-utils/tests/test-shared-general',
+    [ 'nm-utils/tests/test-shared-general.c' ],
+    c_args: [
+        '-DNETWORKMANAGER_COMPILATION_TEST',
+        '-DNETWORKMANAGER_COMPILATION=(NM_NETWORKMANAGER_COMPILATION_GLIB|NM_NETWORKMANAGER_COMPILATION_WITH_GLIB_I18N_PROG)',
+    ],
+    dependencies: [
+        shared_nm_glib_aux_dep,
+        libnm_systemd_shared_no_logging_dep,
+        shared_c_siphash_dep,
     ],
-  dependencies: shared_nm_glib_aux_dep,
-  link_with: shared_c_siphash,
 )
+
 test(
-  'shared/nm-utils/test-shared-general',
-  test_script,
-  args: test_args + [test_shared_general.full_path()]
+    'shared/nm-utils/tests/test-shared-general',
+    test_script,
+    args: test_args + [exe.full_path()]
 )
diff --git a/shared/n-acd/src/n-acd-bpf-fallback.c b/shared/n-acd/src/n-acd-bpf-fallback.c
index 7270cfd9..3cf4eb06 100644
--- a/shared/n-acd/src/n-acd-bpf-fallback.c
+++ b/shared/n-acd/src/n-acd-bpf-fallback.c
@@ -7,6 +7,7 @@
  * See n-acd-bpf.c for documentation.
  */
 
+#include <c-stdaux.h>
 #include <stddef.h>
 #include "n-acd-private.h"
 
diff --git a/shared/n-acd/src/n-acd-bpf.c b/shared/n-acd/src/n-acd-bpf.c
index 771a28ee..57b29ddf 100644
--- a/shared/n-acd/src/n-acd-bpf.c
+++ b/shared/n-acd/src/n-acd-bpf.c
@@ -12,6 +12,7 @@
  * that have already been removed.
  */
 
+#include <c-stdaux.h>
 #include <errno.h>
 #include <inttypes.h>
 #include <linux/bpf.h>
diff --git a/shared/n-acd/src/n-acd-private.h b/shared/n-acd/src/n-acd-private.h
index 3f207912..4583c018 100644
--- a/shared/n-acd/src/n-acd-private.h
+++ b/shared/n-acd/src/n-acd-private.h
@@ -2,6 +2,7 @@
 
 #include <c-list.h>
 #include <c-rbtree.h>
+#include <c-stdaux.h>
 #include <errno.h>
 #include <inttypes.h>
 #include <netinet/if_ether.h>
@@ -13,9 +14,6 @@
 
 typedef struct NAcdEventNode NAcdEventNode;
 
-#define _cleanup_(_x) __attribute__((__cleanup__(_x)))
-#define _public_ __attribute__((__visibility__("default")))
-
 /* This augments the error-codes with internal ones that are never exposed. */
 enum {
         _N_ACD_INTERNAL = _N_ACD_E_N,
@@ -150,23 +148,7 @@ int n_acd_bpf_compile(int *progfdp, int mapfd, struct ether_addr *mac);
 
 /* inline helpers */
 
-static inline int n_acd_errno(void) {
-        /*
-         * Compilers continuously warn about uninitialized variables since they
-         * cannot deduce that `return -errno;` will always be negative. This
-         * small wrapper makes sure compilers figure that out. Use it as
-         * replacement for `errno` read access. Yes, it generates worse code,
-         * but only marginally and only affects slow-paths.
-         */
-        return abs(errno) ? : EIO;
-}
-
 static inline void n_acd_event_node_freep(NAcdEventNode **node) {
         if (*node)
                 n_acd_event_node_free(*node);
 }
-
-static inline void n_acd_closep(int *fdp) {
-        if (*fdp >= 0)
-                close(*fdp);
-}
diff --git a/shared/n-acd/src/n-acd-probe.c b/shared/n-acd/src/n-acd-probe.c
index d4da0fd5..43dd344c 100644
--- a/shared/n-acd/src/n-acd-probe.c
+++ b/shared/n-acd/src/n-acd-probe.c
@@ -1,9 +1,14 @@
 /*
  * IPv4 Address Conflict Detection
+ *
+ * This file implements the probe object. A probe is basically the
+ * state-machine of a single ACD run. It takes an address to probe for, checks
+ * for conflicts and then defends it once configured.
  */
 
 #include <assert.h>
 #include <c-rbtree.h>
+#include <c-stdaux.h>
 #include <endian.h>
 #include <errno.h>
 #include <inttypes.h>
@@ -19,8 +24,8 @@
 #include "n-acd-private.h"
 
 /*
- * These parameters and timing intervals specified in RFC-5227. The original
- * values are:
+ * These parameters and timing intervals are specified in RFC-5227. The
+ * original values are:
  *
  *     PROBE_NUM                                3
  *     PROBE_WAIT                               1s
@@ -66,10 +71,19 @@
 #define N_ACD_RFC_DEFEND_INTERVAL_NSEC          (UINT64_C(10000000000)) /* 10s */
 
 /**
- * XXX
+ * n_acd_probe_config_new() - create probe configuration
+ * @configp:                    output argument for new probe configuration
+ *
+ * This creates a new probe configuration. It will be returned in @configp to
+ * the caller, which upon return fully owns the object.
+ *
+ * A probe configuration collects parameters for probes. It never validates the
+ * input, but this is left to the consumer of the configuration to do.
+ *
+ * Return: 0 on success, negative error code on failure.
  */
-_public_ int n_acd_probe_config_new(NAcdProbeConfig **configp) {
-        _cleanup_(n_acd_probe_config_freep) NAcdProbeConfig *config = NULL;
+_c_public_ int n_acd_probe_config_new(NAcdProbeConfig **configp) {
+        _c_cleanup_(n_acd_probe_config_freep) NAcdProbeConfig *config = NULL;
 
         config = malloc(sizeof(*config));
         if (!config)
@@ -83,9 +97,15 @@ _public_ int n_acd_probe_config_new(NAcdProbeConfig **configp) {
 }
 
 /**
- * XXX
+ * n_acd_probe_config_free() - destroy probe configuration
+ * @config:                     configuration to operate on, or NULL
+ *
+ * This destroys the probe configuration and all associated objects. If @config
+ * is NULL, this is a no-op.
+ *
+ * Return: NULL is returned.
  */
-_public_ NAcdProbeConfig *n_acd_probe_config_free(NAcdProbeConfig *config) {
+_c_public_ NAcdProbeConfig *n_acd_probe_config_free(NAcdProbeConfig *config) {
         if (!config)
                 return NULL;
 
@@ -95,16 +115,45 @@ _public_ NAcdProbeConfig *n_acd_probe_config_free(NAcdProbeConfig *config) {
 }
 
 /**
- * XXX
+ * n_acd_probe_config_set_ip() - set ip property
+ * @config:                     configuration to operate on
+ * @ip:                         ip to set
+ *
+ * This sets the IP property to the value `ip`. The address is copied into the
+ * configuration object. No validation is performed.
+ *
+ * The IP property selects the IP address that a probe checks for. It is the
+ * caller's responsibility to guarantee the address is valid and can be used.
  */
-_public_ void n_acd_probe_config_set_ip(NAcdProbeConfig *config, struct in_addr ip) {
+_c_public_ void n_acd_probe_config_set_ip(NAcdProbeConfig *config, struct in_addr ip) {
         config->ip = ip;
 }
 
 /**
- * XXX
+ * n_acd_probe_config_set_timeout() - set timeout property
+ * @config:                     configuration to operate on
+ * @msecs:                      timeout to set, in milliseconds
+ *
+ * This sets the timeout to use for a conflict detection probe. The
+ * specification default is provided as `N_ACD_TIMEOUT_RFC5227` and corresponds
+ * to 9 seconds.
+ *
+ * If set to 0, conflict detection is skipped and the address is immediately
+ * advertised and defended.
+ *
+ * Depending on the transport used, the API user should select a suitable
+ * timeout. Since `ACD` only operates on the link layer, timeouts in the
+ * hundreds of milliseconds range should be more than enough for any modern
+ * network. Note that increasing this value directly affects the time it takes
+ * to connect to a network, since an address should not be used unless conflict
+ * detection finishes.
+ *
+ * Using the specification default is **discouraged**. It is way too slow and
+ * not appropriate for modern networks.
+ *
+ * Default value is `N_ACD_TIMEOUT_RFC5227`.
  */
-_public_ void n_acd_probe_config_set_timeout(NAcdProbeConfig *config, uint64_t msecs) {
+_c_public_ void n_acd_probe_config_set_timeout(NAcdProbeConfig *config, uint64_t msecs) {
         config->timeout_msecs = msecs;
 }
 
@@ -214,15 +263,14 @@ static void n_acd_probe_unlink(NAcdProbe *probe) {
          */
         if (n_acd_probe_is_unique(probe)) {
                 r = n_acd_bpf_map_remove(probe->acd->fd_bpf_map, &probe->ip);
-                assert(r >= 0);
-                (void)r;
+                c_assert(r >= 0);
                 --probe->acd->n_bpf_map;
         }
         c_rbnode_unlink(&probe->ip_node);
 }
 
 int n_acd_probe_new(NAcdProbe **probep, NAcd *acd, NAcdProbeConfig *config) {
-        _cleanup_(n_acd_probe_freep) NAcdProbe *probe = NULL;
+        _c_cleanup_(n_acd_probe_freep) NAcdProbe *probe = NULL;
         int r;
 
         if (!config->ip.s_addr)
@@ -286,9 +334,20 @@ int n_acd_probe_new(NAcdProbe **probep, NAcd *acd, NAcdProbeConfig *config) {
 }
 
 /**
- * XXX
+ * n_acd_probe_free() - destroy a probe
+ * @probe:                      probe to operate on, or NULL
+ *
+ * This destroys the probe specified by @probe. All operations are immediately
+ * ceded and all associated objects are released.
+ *
+ * If @probe is NULL, this is a no-op.
+ *
+ * This function will flush all events associated with @probe from the event
+ * queue. That is, no events will be returned for this @probe anymore.
+ *
+ * Return: NULL is returned.
  */
-_public_ NAcdProbe *n_acd_probe_free(NAcdProbe *probe) {
+_c_public_ NAcdProbe *n_acd_probe_free(NAcdProbe *probe) {
         NAcdEventNode *node, *t_node;
 
         if (!probe)
@@ -306,7 +365,7 @@ _public_ NAcdProbe *n_acd_probe_free(NAcdProbe *probe) {
 }
 
 int n_acd_probe_raise(NAcdProbe *probe, NAcdEventNode **nodep, unsigned int event) {
-        _cleanup_(n_acd_event_node_freep) NAcdEventNode *node = NULL;
+        _c_cleanup_(n_acd_event_node_freep) NAcdEventNode *node = NULL;
         int r;
 
         r = n_acd_raise(probe->acd, &node, event);
@@ -327,8 +386,8 @@ int n_acd_probe_raise(NAcdProbe *probe, NAcdEventNode **nodep, unsigned int even
                 node->event.conflict.probe = probe;
                 break;
         default:
-                assert(0);
-                return -EIO;
+                c_assert(0);
+                return -ENOTRECOVERABLE;
         }
 
         c_list_link_tail(&probe->event_list, &node->probe_link);
@@ -451,8 +510,8 @@ int n_acd_probe_handle_timeout(NAcdProbe *probe) {
                  * There are no timeouts in these states. If we trigger one,
                  * something is fishy.
                  */
-                assert(0);
-                return -EIO;
+                c_assert(0);
+                return -ENOTRECOVERABLE;
         }
 
         return 0;
@@ -583,31 +642,47 @@ int n_acd_probe_handle_packet(NAcdProbe *probe, struct ether_arp *packet, bool h
                  * We are not listening for packets in these states. If we receive one,
                  * something is fishy.
                  */
-                assert(0);
-                return -EIO;
+                c_assert(0);
+                return -ENOTRECOVERABLE;
         }
 
         return 0;
 }
 
 /**
- * n_acd_probe_set_userdata - XXX
+ * n_acd_probe_set_userdata - set userdata
+ * @probe:                      probe to operate on
+ * @userdata:                   userdata pointer
+ *
+ * This can be used to set a caller-controlled user-data pointer on @probe. The
+ * value of the pointer is never inspected or used by `n-acd` and is fully
+ * under control of the caller.
+ *
+ * The default value is NULL.
  */
-_public_ void n_acd_probe_set_userdata(NAcdProbe *probe, void *userdata) {
+_c_public_ void n_acd_probe_set_userdata(NAcdProbe *probe, void *userdata) {
         probe->userdata = userdata;
 }
 
 /**
- * n_acd_probe_get_userdata - XXX
+ * n_acd_probe_get_userdata - get userdata
+ * @probe:                      probe to operate on
+ *
+ * This queries the userdata pointer that was previously set through
+ * n_acd_probe_set_userdata().
+ *
+ * The default value is NULL.
+ *
+ * Return: The stored userdata pointer is returned.
  */
-_public_ void n_acd_probe_get_userdata(NAcdProbe *probe, void **userdatap) {
+_c_public_ void n_acd_probe_get_userdata(NAcdProbe *probe, void **userdatap) {
         *userdatap = probe->userdata;
 }
 
 /**
  * n_acd_probe_announce() - announce the configured IP address
- * @probe:      probe object
- * @defend:     defence policy
+ * @probe:                      probe to operate on
+ * @defend:                     defence policy
  *
  * Announce the IP address on the local link, and start defending it according
  * to the given policy, which mut be one of N_ACD_DEFEND_ONCE,
@@ -619,7 +694,7 @@ _public_ void n_acd_probe_get_userdata(NAcdProbe *probe, void **userdatap) {
  * Return: 0 on success, N_ACD_E_INVALID_ARGUMENT in case the defence policy
  *         is invalid, negative error code on failure.
  */
-_public_ int n_acd_probe_announce(NAcdProbe *probe, unsigned int defend) {
+_c_public_ int n_acd_probe_announce(NAcdProbe *probe, unsigned int defend) {
         if (defend >= _N_ACD_DEFEND_N)
                 return N_ACD_E_INVALID_ARGUMENT;
 
diff --git a/shared/n-acd/src/n-acd.c b/shared/n-acd/src/n-acd.c
index def56a21..a0a48c58 100644
--- a/shared/n-acd/src/n-acd.c
+++ b/shared/n-acd/src/n-acd.c
@@ -1,11 +1,56 @@
 /*
  * IPv4 Address Conflict Detection
+ *
+ * This file contains the main context initialization and management functions,
+ * as well as a bunch of utilities used through the n-acd modules.
+ */
+
+/**
+ * DOC: IPv4 Address Conflict Detection
+ *
+ * The `n-acd` project implements the IPv4 Address Conflict Detection protocol
+ * as defined in RFC-5227. The protocol originates in the IPv4 Link Local
+ * Address selection but was later on generalized and resulted in `ACD`. The
+ * idea is to use `ARP` to query a link for an address to see whether it
+ * already exists on the network, as well as defending an address that is in
+ * use on a network interface. Furthermore, `ACD` provides passive diagnostics
+ * for administrators, as it will detect address conflicts automatically, which
+ * then can be logged or shown to a user.
+ *
+ * The main context object of `n-acd` is the `NAcd` structure. It is a passive
+ * ref-counted context object which drives `ACD` probes running on it. A
+ * context is specific to a linux network device and transport. If multiple
+ * network devices are used, then separate `NAcd` contexts must be deployed.
+ *
+ * The `NAcdProbe` object drives a single `ACD` state-machine. A probe is
+ * created on an `NAcd` context by providing an address to probe for. The probe
+ * will then raise notifications whether the address conflict detection found
+ * something, or whether the address is ready to be used. Optionally, the probe
+ * will then enter into passive mode and defend the address as long as it is
+ * kept active.
+ *
+ * Note that the `n-acd` project only implements the networking protocol. It
+ * never queries or modifies network interfaces. It completely relies on the
+ * API user to react to notifications and update network interfaces
+ * respectively. `n-acd` uses an event-mechanism on every context object. All
+ * events raise by any probe or operation on a given context will queue all
+ * events on that context object. The event-queue can then be drained by the
+ * API user. All events are properly asynchronous and designed in a way that no
+ * synchronous reaction to any event is required. That is, the events are
+ * carefully designed to allow forwarding via IPC (or even networks) to a
+ * controller that handles them and specifies how to react. Furthermore, none
+ * of the function calls of `n-acd` require synchronous error handling.
+ * Instead, functions only ever return values on fatal errors. Everything else
+ * is queued as events, thus guaranteeing that synchronous handling of return
+ * values is not required. Exceptions are functions that do not affect internal
+ * state or do not have an associated context object.
  */
 
 #include <assert.h>
 #include <c-list.h>
 #include <c-rbtree.h>
 #include <c-siphash.h>
+#include <c-stdaux.h>
 #include <endian.h>
 #include <errno.h>
 #include <inttypes.h>
@@ -55,7 +100,7 @@ static int n_acd_get_random(unsigned int *random) {
 
         r = clock_gettime(CLOCK_MONOTONIC, &ts);
         if (r < 0)
-                return -n_acd_errno();
+                return -c_errno();
 
         c_siphash_append(&hash, (const uint8_t *)&ts.tv_sec, sizeof(ts.tv_sec));
         c_siphash_append(&hash, (const uint8_t *)&ts.tv_nsec, sizeof(ts.tv_nsec));
@@ -76,19 +121,19 @@ static int n_acd_socket_new(int *fdp, int fd_bpf_prog, NAcdConfig *config) {
 
         s = socket(PF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
         if (s < 0) {
-                r = -n_acd_errno();
+                r = -c_errno();
                 goto error;
         }
 
         if (fd_bpf_prog >= 0) {
                 r = setsockopt(s, SOL_SOCKET, SO_ATTACH_BPF, &fd_bpf_prog, sizeof(fd_bpf_prog));
                 if (r < 0)
-                        return -n_acd_errno();
+                        return -c_errno();
         }
 
         r = bind(s, (struct sockaddr *)&address, sizeof(address));
         if (r < 0) {
-                r = -n_acd_errno();
+                r = -c_errno();
                 goto error;
         }
 
@@ -103,10 +148,21 @@ error:
 }
 
 /**
- * XXX
+ * n_acd_config_new() - create configuration object
+ * @configp:                    output argument for new configuration
+ *
+ * This creates a new configuration object and provides it to the caller. The
+ * object is fully owned by the caller upon function return.
+ *
+ * A configuration object is a passive structure that is used to collect
+ * information that is then passed to a constructor or other function. A
+ * configuration never validates the data, but it is up to the consumer of a
+ * configuration to do that.
+ *
+ * Return: 0 on success, negative error code on failure.
  */
-_public_ int n_acd_config_new(NAcdConfig **configp) {
-        _cleanup_(n_acd_config_freep) NAcdConfig *config = NULL;
+_c_public_ int n_acd_config_new(NAcdConfig **configp) {
+        _c_cleanup_(n_acd_config_freep) NAcdConfig *config = NULL;
 
         config = malloc(sizeof(*config));
         if (!config)
@@ -120,9 +176,15 @@ _public_ int n_acd_config_new(NAcdConfig **configp) {
 }
 
 /**
- * XXX
+ * n_acd_config_free() - destroy configuration object
+ * @config:                     configuration to operate on, or NULL
+ *
+ * This destroys the configuration object @config. If @config is NULL, this is
+ * a no-op.
+ *
+ * Return: NULL is returned.
  */
-_public_ NAcdConfig *n_acd_config_free(NAcdConfig *config) {
+_c_public_ NAcdConfig *n_acd_config_free(NAcdConfig *config) {
         if (!config)
                 return NULL;
 
@@ -132,23 +194,55 @@ _public_ NAcdConfig *n_acd_config_free(NAcdConfig *config) {
 }
 
 /**
- * XXX
+ * n_acd_config_set_ifindex() - set ifindex property
+ * @config:                     configuration to operate on
+ * @ifindex:                    ifindex to set
+ *
+ * This sets the @ifindex property of the configuration object. Any previous
+ * value is overwritten.
+ *
+ * A valid ifindex is a 32bit integer greater than 0. Any other value is
+ * treated as unspecified.
+ *
+ * The ifindex corresponds to the interface index provided by the linux kernel.
+ * It specifies the network device to be used.
  */
-_public_ void n_acd_config_set_ifindex(NAcdConfig *config, int ifindex) {
+_c_public_ void n_acd_config_set_ifindex(NAcdConfig *config, int ifindex) {
         config->ifindex = ifindex;
 }
 
 /**
- * XXX
+ * n_acd_config_set_transport() - set transport property
+ * @config:                     configuration to operate on
+ * @transport:                  transport to set
+ *
+ * This specifies the transport to use. A transport must be one of the
+ * `N_ACD_TRANSPORT_*` identifiers. It selects which transport protocol `n-acd`
+ * will run on.
  */
-_public_ void n_acd_config_set_transport(NAcdConfig *config, unsigned int transport) {
+_c_public_ void n_acd_config_set_transport(NAcdConfig *config, unsigned int transport) {
         config->transport = transport;
 }
 
 /**
- * XXX
+ * n_acd_config_set_mac() - set mac property
+ * @config:                     configuration to operate on
+ * @mac:                        mac to set
+ *
+ * This specifies the hardware address (also referred to as `MAC Address`) to
+ * use. Any hardware address can be specified. It is the caller's
+ * responsibility to make sure the address can actually be used.
+ *
+ * The address in @mac is copied into @config. It does not have to be retained
+ * by the caller.
  */
-_public_ void n_acd_config_set_mac(NAcdConfig *config, const uint8_t *mac, size_t n_mac) {
+_c_public_ void n_acd_config_set_mac(NAcdConfig *config, const uint8_t *mac, size_t n_mac) {
+        /*
+         * We truncate the address at the maximum we support. We still remember
+         * the original length, so any consumer of this configuration can then
+         * complain about an unsupported address length. This allows us to
+         * avoid a memory allocation here and having to return `int`.
+         */
         config->n_mac = n_mac;
         memcpy(config->mac, mac, n_mac > ETH_ALEN ? ETH_ALEN : n_mac);
 }
@@ -179,7 +273,7 @@ NAcdEventNode *n_acd_event_node_free(NAcdEventNode *node) {
 
 int n_acd_ensure_bpf_map_space(NAcd *acd) {
         NAcdProbe *probe;
-        _cleanup_(n_acd_closep) int fd_map = -1, fd_prog = -1;
+        _c_cleanup_(c_closep) int fd_map = -1, fd_prog = -1;
         size_t  max_map;
         int r;
 
@@ -205,7 +299,7 @@ int n_acd_ensure_bpf_map_space(NAcd *acd) {
         if (fd_prog >= 0) {
                 r = setsockopt(acd->fd_socket, SOL_SOCKET, SO_ATTACH_BPF, &fd_prog, sizeof(fd_prog));
                 if (r)
-                        return -n_acd_errno();
+                        return -c_errno();
         }
 
         if (acd->fd_bpf_map >= 0)
@@ -218,16 +312,20 @@ int n_acd_ensure_bpf_map_space(NAcd *acd) {
 
 /**
  * n_acd_new() - create a new ACD context
- * @acdp:       output argument for context
- * @config:     configuration parameters
+ * @acdp:                       output argument for new context object
+ * @config:                     configuration parameters
  *
- * Create a new ACD context and return it in @acdp.
+ * Create a new ACD context and return it in @acdp. The configuration @config
+ * must be initialized by the caller and must specify a valid network
+ * interface, transport mechanism, as well as hardware address compatible with
+ * the selected transport. The configuration is copied into the context. The
+ * @config object thus does not have to be retained by the caller.
  *
- * Return: 0 on success, or a negative error code on failure.
+ * Return: 0 on success, negative error code on failure.
  */
-_public_ int n_acd_new(NAcd **acdp, NAcdConfig *config) {
-        _cleanup_(n_acd_unrefp) NAcd *acd = NULL;
-        _cleanup_(n_acd_closep) int fd_bpf_prog = -1;
+_c_public_ int n_acd_new(NAcd **acdp, NAcdConfig *config) {
+        _c_cleanup_(n_acd_unrefp) NAcd *acd = NULL;
+        _c_cleanup_(c_closep) int fd_bpf_prog = -1;
         int r;
 
         if (config->ifindex <= 0 ||
@@ -250,7 +348,7 @@ _public_ int n_acd_new(NAcd **acdp, NAcdConfig *config) {
 
         acd->fd_epoll = epoll_create1(EPOLL_CLOEXEC);
         if (acd->fd_epoll < 0)
-                return -n_acd_errno();
+                return -c_errno();
 
         r = timer_init(&acd->timer);
         if (r < 0)
@@ -276,7 +374,7 @@ _public_ int n_acd_new(NAcd **acdp, NAcdConfig *config) {
                               .data.u32 = N_ACD_EPOLL_TIMER,
                       });
         if (r < 0)
-                return -n_acd_errno();
+                return -c_errno();
 
         r = epoll_ctl(acd->fd_epoll, EPOLL_CTL_ADD, acd->fd_socket,
                       &(struct epoll_event){
@@ -284,14 +382,14 @@ _public_ int n_acd_new(NAcd **acdp, NAcdConfig *config) {
                               .data.u32 = N_ACD_EPOLL_SOCKET,
                       });
         if (r < 0)
-                return -n_acd_errno();
+                return -c_errno();
 
         *acdp = acd;
         acd = NULL;
         return 0;
 }
 
-static void n_acd_free(NAcd *acd) {
+static void n_acd_free_internal(NAcd *acd) {
         NAcdEventNode *node, *t_node;
 
         if (!acd)
@@ -300,10 +398,10 @@ static void n_acd_free(NAcd *acd) {
         c_list_for_each_entry_safe(node, t_node, &acd->event_list, acd_link)
                 n_acd_event_node_free(node);
 
-        assert(c_rbtree_is_empty(&acd->ip_tree));
+        c_assert(c_rbtree_is_empty(&acd->ip_tree));
 
         if (acd->fd_socket >= 0) {
-                assert(acd->fd_epoll >= 0);
+                c_assert(acd->fd_epoll >= 0);
                 epoll_ctl(acd->fd_epoll, EPOLL_CTL_DEL, acd->fd_socket, NULL);
                 close(acd->fd_socket);
                 acd->fd_socket = -1;
@@ -315,7 +413,7 @@ static void n_acd_free(NAcd *acd) {
         }
 
         if (acd->timer.fd >= 0) {
-                assert(acd->fd_epoll >= 0);
+                c_assert(acd->fd_epoll >= 0);
                 epoll_ctl(acd->fd_epoll, EPOLL_CTL_DEL, acd->timer.fd, NULL);
                 timer_deinit(&acd->timer);
         }
@@ -329,20 +427,32 @@ static void n_acd_free(NAcd *acd) {
 }
 
 /**
- * XXX
+ * n_acd_ref() - acquire reference
+ * @acd:                        context to operate on, or NULL
+ *
+ * This acquires a single reference to the context specified as @acd. If @acd
+ * is NULL, this is a no-op.
+ *
+ * Return: @acd is returned.
  */
-_public_ NAcd *n_acd_ref(NAcd *acd) {
+_c_public_ NAcd *n_acd_ref(NAcd *acd) {
         if (acd)
                 ++acd->n_refs;
         return acd;
 }
 
 /**
- * XXX
+ * n_acd_unref() - release reference
+ * @acd:                        context to operate on, or NULL
+ *
+ * This releases a single reference to the context @acd. If this is the last
+ * reference, the context is torn down and deallocated.
+ *
+ * Return: NULL is returned.
  */
-_public_ NAcd *n_acd_unref(NAcd *acd) {
+_c_public_ NAcd *n_acd_unref(NAcd *acd) {
         if (acd && !--acd->n_refs)
-                n_acd_free(acd);
+                n_acd_free_internal(acd);
         return NULL;
 }
 
@@ -426,7 +536,7 @@ int n_acd_send(NAcd *acd, const struct in_addr *tpa, const struct in_addr *spa)
                  * Random network error. We treat this as fatal and propagate
                  * the error, so it is noticed and can be investigated.
                  */
-                return -n_acd_errno();
+                return -c_errno();
         } else if (l != (ssize_t)sizeof(arp)) {
                 /*
                  * Ugh, the kernel modified the packet. This is unexpected. We
@@ -440,13 +550,22 @@ int n_acd_send(NAcd *acd, const struct in_addr *tpa, const struct in_addr *spa)
 
 /**
  * n_acd_get_fd() - get pollable file descriptor
- * @acd:        ACD context
- * @fdp:        output argument for file descriptor
+ * @acd:                        context object to operate on
+ * @fdp:                        output argument for file descriptor
+ *
+ * This returns the backing file-descriptor of the context object @acd. The
+ * file-descriptor is owned by @acd and valid as long as @acd is. The
+ * file-descriptor never changes, so it can be cached by the caller as long as
+ * they hold a reference to @acd.
+ *
+ * The file-descriptor is internal to the @acd context and should not be
+ * modified by the caller. It is only exposed to allow the caller to poll on
+ * it. Whenever the file-descriptor polls readable, n_acd_dispatch() should be
+ * called.
  *
- * Returns a file descriptor in @fdp. This file descriptor can be polled by
- * the caller to indicate when the ACD context can be dispatched.
+ * Currently, the file-descriptor is an epoll-fd.
  */
-_public_ void n_acd_get_fd(NAcd *acd, int *fdp) {
+_c_public_ void n_acd_get_fd(NAcd *acd, int *fdp) {
         *fdp = acd->fd_epoll;
 }
 
@@ -597,7 +716,7 @@ static int n_acd_dispatch_timer(NAcd *acd, struct epoll_event *event) {
                 if (r <= 0)
                         return r;
 
-                assert(r == TIMER_E_TRIGGERED);
+                c_assert(r == TIMER_E_TRIGGERED);
 
                 /*
                  * A timer triggered, handle all pending timeouts at a given
@@ -710,7 +829,7 @@ static int n_acd_dispatch_socket(NAcd *acd, struct epoll_event *event) {
                          * Something went wrong. Propagate the error-code, so
                          * this can be investigated.
                          */
-                        return -n_acd_errno();
+                        return -c_errno();
                 }
         } else if (n >= (ssize_t)n_batch) {
                 /*
@@ -750,16 +869,34 @@ static int n_acd_dispatch_socket(NAcd *acd, struct epoll_event *event) {
 }
 
 /**
- * XXX
+ * n_acd_dispatch() - dispatch context
+ * @acd:                        context object to operate on
+ *
+ * This dispatches the internal state-machine of all probes and operations
+ * running on the context @acd.
+ *
+ * Any outside effect or event triggered by this dispatcher will be queued on
+ * the event-queue of @acd. Whenever the dispatcher returns, the caller is
+ * required to drain the event-queue via n_acd_pop_event() until it is empty.
+ *
+ * This function dispatches as many events as possible up to a static limit to
+ * prevent stalling execution. If the static limit is reached, this function
+ * will return with N_ACD_E_PREEMPTED, otherwise 0 is returned. In most cases
+ * preemption can be ignored, because level-triggered event notification
+ * handles it automatically. However, in case of edge-triggered event
+ * mechanisms, the caller must make sure to call the dispatcher again.
+ *
+ * Return: 0 on success, N_ACD_E_PREEMPTED on preemption, negative error code
+ *         on failure.
  */
-_public_ int n_acd_dispatch(NAcd *acd) {
+_c_public_ int n_acd_dispatch(NAcd *acd) {
         struct epoll_event events[2];
         int n, i, r = 0;
 
         n = epoll_wait(acd->fd_epoll, events, sizeof(events) / sizeof(*events), 0);
         if (n < 0) {
                 /* Linux never returns EINTR if `timeout == 0'. */
-                return -n_acd_errno();
+                return -c_errno();
         }
 
         acd->preempted = false;
@@ -773,7 +910,7 @@ _public_ int n_acd_dispatch(NAcd *acd) {
                         r = n_acd_dispatch_socket(acd, events + i);
                         break;
                 default:
-                        assert(0);
+                        c_assert(0);
                         r = 0;
                         break;
                 }
@@ -787,8 +924,8 @@ _public_ int n_acd_dispatch(NAcd *acd) {
 
 /**
  * n_acd_pop_event() - get the next pending event
- * @acd:        ACD context
- * @eventp:     output argument for the event
+ * @acd:                        context object to operate on
+ * @eventp:                     output argument for the event
  *
  * Returns a pointer to the next pending event. The event is still owend by
  * the context, and is only valid until the next call to n_acd_pop_event()
@@ -840,7 +977,7 @@ _public_ int n_acd_dispatch(NAcd *acd) {
  *          @eventp and 0 is returned. If an error is returned, @eventp is left
  *          untouched.
  */
-_public_ int n_acd_pop_event(NAcd *acd, NAcdEvent **eventp) {
+_c_public_ int n_acd_pop_event(NAcd *acd, NAcdEvent **eventp) {
         NAcdEventNode *node, *t_node;
 
         c_list_for_each_entry_safe(node, t_node, &acd->event_list, acd_link) {
@@ -859,8 +996,29 @@ _public_ int n_acd_pop_event(NAcd *acd, NAcdEvent **eventp) {
 }
 
 /**
- * XXX
+ * n_acd_probe() - start new probe
+ * @acd:                        context object to operate on
+ * @probep:                     output argument for new probe
+ * @config:                     probe configuration
+ *
+ * This creates a new probe on the context @acd and returns the probe in
+ * @probep. The configuration @config must provide valid probe parameters. At
+ * least a valid IP address must be provided through the configuration.
+ *
+ * This function does not reject duplicate probes for the same address. It is
+ * the caller's decision whether duplicates are allowed or not. But note that
+ * duplicate probes on the same context will not conflict each other. That is,
+ * running a probe for the same address twice on the same context will not
+ * cause them to consider each other a duplicate.
+ *
+ * Probes are rather lightweight objects. They do not create any
+ * file-descriptors or other kernel objects. Probes always re-use the
+ * infrastructure provided by the context object @acd. This allows running many
+ * probes simultaneously without exhausting resources.
+ *
+ * Return: 0 on success, N_ACD_E_INVALID_ARGUMENT on invalid configuration
+ *         parameters, negative error code on failure.
  */
-_public_ int n_acd_probe(NAcd *acd, NAcdProbe **probep, NAcdProbeConfig *config) {
+_c_public_ int n_acd_probe(NAcd *acd, NAcdProbe **probep, NAcdProbeConfig *config) {
         return n_acd_probe_new(probep, acd, config);
 }
diff --git a/shared/n-acd/src/n-acd.h b/shared/n-acd/src/n-acd.h
index 74b0aacb..e2b01270 100644
--- a/shared/n-acd/src/n-acd.h
+++ b/shared/n-acd/src/n-acd.h
@@ -13,7 +13,9 @@ extern "C" {
 #endif
 
 #include <netinet/in.h>
+#include <inttypes.h>
 #include <stdbool.h>
+#include <stdlib.h>
 
 typedef struct NAcd NAcd;
 typedef struct NAcdConfig NAcdConfig;
diff --git a/shared/n-acd/src/util/timer.c b/shared/n-acd/src/util/timer.c
index 07dbf34e..3c9570a1 100644
--- a/shared/n-acd/src/util/timer.c
+++ b/shared/n-acd/src/util/timer.c
@@ -4,6 +4,7 @@
 
 #include <assert.h>
 #include <c-rbtree.h>
+#include <c-stdaux.h>
 #include <errno.h>
 #include <stdlib.h>
 #include <sys/timerfd.h>
@@ -30,7 +31,7 @@ int timer_init(Timer *timer) {
 }
 
 void timer_deinit(Timer *timer) {
-        assert(c_rbtree_is_empty(&timer->tree));
+        c_assert(c_rbtree_is_empty(&timer->tree));
 
         if (timer->fd >= 0) {
                 close(timer->fd);
@@ -43,8 +44,7 @@ void timer_now(Timer *timer, uint64_t *nowp) {
         int r;
 
         r = clock_gettime(timer->clock, &ts);
-        assert(r >= 0);
-        (void)r;
+        c_assert(r >= 0);
 
         *nowp = ts.tv_sec * UINT64_C(1000000000) + ts.tv_nsec;
 }
@@ -60,7 +60,7 @@ void timer_rearm(Timer *timer) {
          */
 
         timeout = c_rbnode_entry(c_rbtree_first(&timer->tree), Timeout, node);
-        assert(!timeout || timeout->timeout);
+        c_assert(!timeout || timeout->timeout);
 
         time = timeout ? timeout->timeout : 0;
 
@@ -74,8 +74,7 @@ void timer_rearm(Timer *timer) {
                                             },
                                     },
                                     NULL);
-                assert(r >= 0);
-                (void)r;
+                c_assert(r >= 0);
 
                 timer->scheduled_timeout = time;
         }
@@ -134,8 +133,7 @@ int timer_pop_timeout(Timer *timer, uint64_t until, Timeout **timeoutp) {
 }
 
 void timeout_schedule(Timeout *timeout, Timer *timer, uint64_t time) {
-
-        assert(time);
+        c_assert(time);
 
         /*
          * In case @timeout was already scheduled, remove it from the
diff --git a/shared/n-acd/src/util/timer.h b/shared/n-acd/src/util/timer.h
index 2acc99e3..d01b2741 100644
--- a/shared/n-acd/src/util/timer.h
+++ b/shared/n-acd/src/util/timer.h
@@ -1,6 +1,7 @@
 #pragma once
 
 #include <c-rbtree.h>
+#include <c-stdaux.h>
 #include <inttypes.h>
 #include <stdlib.h>
 #include <time.h>
diff --git a/shared/n-dhcp4/src/n-dhcp4-c-connection.c b/shared/n-dhcp4/src/n-dhcp4-c-connection.c
new file mode 100644
index 00000000..5c50dacf
--- /dev/null
+++ b/shared/n-dhcp4/src/n-dhcp4-c-connection.c
@@ -0,0 +1,1150 @@
+/*
+ * DHCPv4 Client Connection
+ *
+ * XXX
+ */
+
+#include <assert.h>
+#include <c-stdaux.h>
+#include <errno.h>
+#include <limits.h>
+#include <sys/socket.h> /* needed by linux/netdevice.h */
+#include <linux/netdevice.h>
+#include <net/if_arp.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/epoll.h>
+#include "n-dhcp4-private.h"
+#include "util/packet.h"
+
+/**
+ * n_dhcp4_c_connection_init() - initialize client connection
+ * @connection:                 connection to operate on
+ * @client_config:              client configuration to use
+ * @probe_config:               client probe configuration to use
+ * @fd_epoll:                   epoll context to attach to, or -1
+ *
+ * This initializes a new client connection using the configuration given in
+ * @client_config and @probe_config.
+ *
+ * The client-configuration given as @client_config must survive the lifetime
+ * of @connection. It is pinned in the connection and used all over the place.
+ * The caller must guarantee that the configuration is not deallocated in the
+ * meantime. Same is true for @probe_config.
+ *
+ * The new connection automatically attaches to the epoll context given as
+ * @fd_epoll. The epoll FD is retained in the connection and the caller must
+ * guarantee that it lives as long as the connection.
+ * The caller is explicitly allowed to pass -1 as @fd_epoll, in which case the
+ * connection will initialize correctly, but will not be in a usable state.
+ * That is, any call to n_dhcp4_c_connection_listen() will fail, since it will
+ * be unable to attach to the epoll context. Such a connection can be used to
+ * get a detached object that behaves sound, but provides no runtime.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int n_dhcp4_c_connection_init(NDhcp4CConnection *connection,
+                              NDhcp4ClientConfig *client_config,
+                              NDhcp4ClientProbeConfig *probe_config,
+                              int fd_epoll) {
+        *connection = (NDhcp4CConnection)N_DHCP4_C_CONNECTION_NULL(*connection);
+        connection->client_config = client_config;
+        connection->probe_config = probe_config;
+        connection->fd_epoll = fd_epoll;
+
+        /*
+         * We explicitly allow initializing connections with an invalid
+         * epoll-fd. The resulting connection immediately transitions into the
+         * CLOSED state. This allows the caller to create dummy connections
+         * useful to provide asynchronous constructor-feedback in the API.
+         *
+         * The effect of this is as if you immediately call
+         * n_dhcp4_c_connection_close() on the new connection. However, by
+         * directly passing -1 in the constructor, you are guaranteed not even
+         * the constructor can ever mess with your epoll-set.
+         */
+        if (connection->fd_epoll < 0)
+                connection->state = N_DHCP4_C_CONNECTION_STATE_CLOSED;
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_c_connection_deinit() - deinitialize client connection
+ * @connection:                 connection to operate on
+ *
+ * This deinitializes a connection that was previously initialized via
+ * n_dhcp4_c_connection_init(). It will tear down all allocated state and
+ * release it.
+ *
+ * Once this function returns, @connection is re-initialized to
+ * N_DHCP4_C_CONNECTION_NULL. If this function is called on a deinitialized
+ * connection, it is a no-op.
+ */
+void n_dhcp4_c_connection_deinit(NDhcp4CConnection *connection) {
+        n_dhcp4_c_connection_close(connection);
+        n_dhcp4_outgoing_free(connection->request);
+        *connection = (NDhcp4CConnection)N_DHCP4_C_CONNECTION_NULL(*connection);
+}
+
+static void n_dhcp4_c_connection_outgoing_set_secs(NDhcp4Outgoing *message) {
+        uint32_t secs;
+
+        /*
+         * This function sets the `secs` field for outgoing messages. It
+         * expects the base-time and start-time to be already set by the
+         * caller.
+         * For a given outgoing message, its `secs` field describes the time
+         * (in seconds) between the start of the transaction this message is
+         * part of and the start of the operational process (also called the
+         * base time here).
+         *
+         * The operational process in the DHCP sense describes the entire
+         * process of requesting a lease and acquiring it. That is, it starts
+         * with the caller's intent to request a lease, and it ends when we
+         * got granted a lease. The act of refreshing a lease is, in itself, a
+         * new operational process. The base-time describes the start-time
+         * recorded when such a process as initiated.
+         *
+         * A transaction in the DHCP sense describes a request+reply
+         * combination, in most cases. That is, the time a request is sent is
+         * the start-time of a transaction. In the ideal case, the start-time
+         * of the first transaction in an operational process matches the
+         * base-time. However, transactions are often delayed with a randomized
+         * offset to reduce traffic during network bursts.
+         * In some cases, however, transactions are composed out of multiple
+         * requests+reply combinations. This includes, for instance, the SELECT
+         * message following an OFFER. The specification clearly says that
+         * those must be considered a single transaction and thus share the
+         * transaction start-time.
+         *
+         * The `secs` field, thus, describes how long a client has been busy
+         * requesting a lease. DHCP servers and proxies do use it to prioritize
+         * clients.
+         *
+         * Note: Some DHCP relays reject a `secs` value of 0 (which might look
+         *       like it is uninitialized). Hence, we always clamp the value to
+         *       the range `[1, INF[`.
+         */
+
+        secs = message->userdata.base_time - message->userdata.start_time;
+        secs /= 1000ULL * 1000ULL * 1000ULL; /* nsecs to secs */
+        secs = secs ?: 1; /* clamp to `[1, INF[` */
+
+        n_dhcp4_outgoing_set_secs(message, secs);
+}
+
+int n_dhcp4_c_connection_listen(NDhcp4CConnection *connection) {
+        _c_cleanup_(c_closep) int fd_packet = -1;
+        int r;
+
+        c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_INIT);
+
+        r = n_dhcp4_c_socket_packet_new(&fd_packet, connection->client_config->ifindex);
+        if (r)
+                return r;
+
+        r = epoll_ctl(connection->fd_epoll,
+                      EPOLL_CTL_ADD,
+                      fd_packet,
+                      &(struct epoll_event){
+                              .events = EPOLLIN,
+                              .data = { .u32 = N_DHCP4_CLIENT_EPOLL_IO },
+                      });
+        if (r < 0)
+                return -errno;
+
+        connection->state = N_DHCP4_C_CONNECTION_STATE_PACKET;
+        connection->fd_packet = fd_packet;
+        fd_packet = -1;
+        return 0;
+}
+
+int n_dhcp4_c_connection_connect(NDhcp4CConnection *connection,
+                                 const struct in_addr *client,
+                                 const struct in_addr *server) {
+        int r, fd_udp;
+
+        c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_PACKET);
+
+        r = n_dhcp4_c_socket_udp_new(&fd_udp,
+                                     connection->client_config->ifindex,
+                                     client,
+                                     server);
+        if (r)
+                return r;
+
+        r = epoll_ctl(connection->fd_epoll,
+                      EPOLL_CTL_ADD,
+                      fd_udp,
+                      &(struct epoll_event){
+                              .events = EPOLLIN,
+                              .data = { .u32 = N_DHCP4_CLIENT_EPOLL_IO },
+                      });
+        if (r < 0) {
+                r = -errno;
+                goto exit_fd;
+        }
+
+        r = packet_shutdown(connection->fd_packet);
+        if (r < 0)
+                goto exit_epoll;
+
+        connection->state = N_DHCP4_C_CONNECTION_STATE_DRAINING;
+        connection->fd_udp = fd_udp;
+        connection->client_ip = client->s_addr;
+        connection->server_ip = server->s_addr;
+        fd_udp = -1;
+        return 0;
+
+exit_epoll:
+        epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, fd_udp, NULL);
+exit_fd:
+        close(fd_udp);
+        return r;
+}
+
+void n_dhcp4_c_connection_close(NDhcp4CConnection *connection) {
+        if (connection->fd_udp >= 0) {
+                epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, connection->fd_udp, NULL);
+                connection->fd_udp = c_close(connection->fd_udp);
+        }
+
+        if (connection->fd_packet >= 0) {
+                epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, connection->fd_packet, NULL);
+                connection->fd_packet = c_close(connection->fd_packet);
+        }
+
+        connection->fd_epoll = -1;
+        connection->state = N_DHCP4_C_CONNECTION_STATE_CLOSED;
+}
+
+static int n_dhcp4_c_connection_verify_incoming(NDhcp4CConnection *connection,
+                                                NDhcp4Incoming *message,
+                                                uint8_t *typep) {
+        NDhcp4Header *header = n_dhcp4_incoming_get_header(message);
+        uint8_t type;
+        uint32_t request_xid;
+        uint8_t *id;
+        size_t n_id;
+        int r;
+
+        r = n_dhcp4_incoming_query_message_type(message, &type);
+        if (r) {
+                if (r == N_DHCP4_E_UNSET)
+                        return N_DHCP4_E_MALFORMED;
+                else
+                        return r;
+        }
+
+        switch (type) {
+        case N_DHCP4_MESSAGE_OFFER:
+        case N_DHCP4_MESSAGE_ACK:
+        case N_DHCP4_MESSAGE_NAK:
+                /*
+                 * Only accept replies if there is a pending request, and it
+                 * has a matching transaction id.
+                 */
+                if (!connection->request)
+                        return N_DHCP4_E_UNEXPECTED;
+
+                n_dhcp4_outgoing_get_xid(connection->request, &request_xid);
+                if (header->xid != request_xid)
+                        return N_DHCP4_E_UNEXPECTED;
+
+                break;
+        case N_DHCP4_MESSAGE_FORCERENEW:
+                /*
+                 * Force renew messages are triggered by a server, and do not
+                 * match a pending request.
+                 */
+                break;
+        default:
+                return N_DHCP4_E_UNEXPECTED;
+        }
+
+        /*
+         * In case our transport makes use of the 'chaddr' field, make sure it
+         * matches exactly our address.
+         */
+        switch (connection->client_config->transport) {
+        case N_DHCP4_TRANSPORT_ETHERNET:
+                c_assert(connection->client_config->n_mac == ETH_ALEN);
+
+                if (header->hlen != ETH_ALEN)
+                        return N_DHCP4_E_UNEXPECTED;
+                if (memcmp(header->chaddr, connection->client_config->mac, ETH_ALEN) != 0)
+                        return N_DHCP4_E_UNEXPECTED;
+
+                break;
+        case N_DHCP4_TRANSPORT_INFINIBAND:
+                if (header->hlen != 0)
+                        return N_DHCP4_E_UNEXPECTED;
+
+                break;
+        }
+
+        /*
+         * If a server passes us back a client ID, it must be the one we
+         * provided. We ignore any packets that have mismatching client-ids.
+         */
+        id = NULL;
+        n_id = 0;
+        r = n_dhcp4_incoming_query(message, N_DHCP4_OPTION_CLIENT_IDENTIFIER, &id, &n_id);
+        if (r) {
+                if (r != N_DHCP4_E_UNSET)
+                        return r;
+        } else {
+                if (n_id != connection->client_config->n_client_id)
+                        return N_DHCP4_E_UNEXPECTED;
+                if (memcmp(id, connection->client_config->client_id, n_id) != 0)
+                        return N_DHCP4_E_UNEXPECTED;
+        }
+
+        *typep = type;
+        return 0;
+}
+
+void n_dhcp4_c_connection_get_timeout(NDhcp4CConnection *connection,
+                                      uint64_t *timeoutp) {
+        uint64_t timeout;
+        size_t n_send;
+
+        if (!connection->request) {
+                *timeoutp = 0;
+                return;
+        }
+
+        switch (connection->request->userdata.type) {
+        case N_DHCP4_C_MESSAGE_DISCOVER:
+        case N_DHCP4_C_MESSAGE_SELECT:
+        case N_DHCP4_C_MESSAGE_REBOOT:
+        case N_DHCP4_C_MESSAGE_INFORM:
+                /*
+                 * Resend with an exponential backoff and a one second random
+                 * slack, from a minimum of two seconds to a maximum of sixty
+                 * four.
+                 *
+                 * Note that the RFC says to start at four rather than two
+                 * seconds, and use [-1,1] slack, rather than [0,1].
+                 */
+                n_send = connection->request->userdata.n_send;
+                if (n_send >= 6)
+                        n_send = 6;
+
+                timeout = connection->request->userdata.send_time + ((1ULL << n_send) * 1000000000ULL) + connection->request->userdata.send_jitter;
+
+                break;
+        case N_DHCP4_C_MESSAGE_REBIND:
+        case N_DHCP4_C_MESSAGE_RENEW:
+                /*
+                 * Resend every sixty seconds with a one second random slack.
+                 *
+                 * Note that the RFC says to do this at most once, but we do
+                 * it until we are cancelled.
+                 */
+                timeout = connection->request->userdata.send_time + (60ULL * 1000000000ULL) + connection->request->userdata.send_jitter;
+
+                break;
+        case N_DHCP4_C_MESSAGE_DECLINE:
+        case N_DHCP4_C_MESSAGE_RELEASE:
+                /* XXX make sure these message types are never pinned? */
+                timeout = 0;
+                break;
+        default:
+                c_assert(0);
+        }
+
+        *timeoutp = timeout;
+}
+
+static int n_dhcp4_c_connection_packet_broadcast(NDhcp4CConnection *connection,
+                                                 NDhcp4Outgoing *message) {
+        int r;
+
+        c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_PACKET);
+
+        r = n_dhcp4_c_socket_packet_send(connection->fd_packet,
+                                         connection->client_config->ifindex,
+                                         connection->client_config->broadcast_mac,
+                                         connection->client_config->n_broadcast_mac,
+                                         message);
+        if (r)
+                return r;
+
+        return 0;
+}
+
+static int n_dhcp4_c_connection_udp_broadcast(NDhcp4CConnection *connection,
+                                              NDhcp4Outgoing *message) {
+        int r;
+
+        c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_DRAINING ||
+               connection->state == N_DHCP4_C_CONNECTION_STATE_UDP);
+
+        r = n_dhcp4_c_socket_udp_broadcast(connection->fd_udp, message);
+        if (r)
+                return r;
+
+        return 0;
+}
+
+static int n_dhcp4_c_connection_udp_send(NDhcp4CConnection *connection,
+                                         NDhcp4Outgoing *message) {
+        int r;
+
+        c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_DRAINING ||
+               connection->state == N_DHCP4_C_CONNECTION_STATE_UDP);
+
+        r = n_dhcp4_c_socket_udp_send(connection->fd_udp, message);
+        if (r)
+                return r;
+
+        return 0;
+}
+
+static void n_dhcp4_c_connection_init_header(NDhcp4CConnection *connection,
+                                             NDhcp4Header *header) {
+        bool broadcast = connection->client_config->request_broadcast;
+
+        header->op = N_DHCP4_OP_BOOTREQUEST;
+
+        switch (connection->client_config->transport) {
+        case N_DHCP4_TRANSPORT_ETHERNET:
+                c_assert(connection->client_config->n_mac == ETH_ALEN);
+
+                header->htype = ARPHRD_ETHER;
+                header->hlen = ETH_ALEN;
+                memcpy(header->chaddr, connection->client_config->mac, ETH_ALEN);
+                break;
+        case N_DHCP4_TRANSPORT_INFINIBAND:
+                header->htype = ARPHRD_INFINIBAND;
+                header->hlen = 0;
+
+                /* infiniband mandates to request broadcasts */
+                broadcast = true;
+                break;
+        default:
+                abort();
+                break;
+        }
+
+        if (connection->client_ip != INADDR_ANY) {
+                header->ciaddr = connection->client_ip;
+        } else {
+                /*
+                 * When the IP stack has not been configured, we may
+                 * not be able to receive unicast packets, depending
+                 * on the hardware. If that is the case we must request
+                 * replies from the server to be broadcast.
+                 *
+                 * Once the IP stack has been configured, receiving
+                 * unicast packets is never a problem, so the broadcast
+                 * flag should not be set.
+                 */
+                if (broadcast)
+                        header->flags |= N_DHCP4_MESSAGE_FLAG_BROADCAST;
+        }
+}
+
+static int n_dhcp4_c_connection_new_message(NDhcp4CConnection *connection,
+                                            NDhcp4Outgoing **messagep,
+                                            uint8_t type) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
+        NDhcp4Header *header;
+        uint8_t message_type;
+        bool via_packet_socket = false;
+        int r;
+
+        switch (type) {
+        case N_DHCP4_C_MESSAGE_DISCOVER:
+                message_type = N_DHCP4_MESSAGE_DISCOVER;
+                via_packet_socket = true;
+                break;
+        case N_DHCP4_C_MESSAGE_INFORM:
+                message_type = N_DHCP4_MESSAGE_INFORM;
+                break;
+        case N_DHCP4_C_MESSAGE_SELECT:
+                message_type = N_DHCP4_MESSAGE_REQUEST;
+                via_packet_socket = true;
+                break;
+        case N_DHCP4_C_MESSAGE_RENEW:
+                message_type = N_DHCP4_MESSAGE_REQUEST;
+                break;
+        case N_DHCP4_C_MESSAGE_REBIND:
+                message_type = N_DHCP4_MESSAGE_REQUEST;
+                break;
+        case N_DHCP4_C_MESSAGE_REBOOT:
+                message_type = N_DHCP4_MESSAGE_REQUEST;
+                via_packet_socket = true;
+                break;
+        case N_DHCP4_C_MESSAGE_RELEASE:
+                message_type = N_DHCP4_MESSAGE_RELEASE;
+                break;
+        case N_DHCP4_C_MESSAGE_DECLINE:
+                message_type = N_DHCP4_MESSAGE_DECLINE;
+                via_packet_socket = true;
+                break;
+        default:
+                abort();
+                return -ENOTRECOVERABLE;
+        }
+
+        /*
+         * We explicitly pass 0 as maximum message size, which makes
+         * NDhcp4Outgoing use the mandated default value from the spec (see its
+         * implementation). While the transport and like layers might support
+         * bigger MTUs (and we very likely know about them through
+         * n_dhcp4_client_update_mtu()), we cannot assume the target DHCP
+         * server supports parsing packets bigger than the minimum (and it is
+         * allowed to refuse bigger IP packets, even if the network supports
+         * transmission of them).
+         *
+         * We could theoretically increase this for packets other than the
+         * initial discovery. However, clients are unlikely to ever send large
+         * packets, so we just keep the same default for all outgoing packets.
+         */
+        r = n_dhcp4_outgoing_new(&message, 0, N_DHCP4_OVERLOAD_FILE | N_DHCP4_OVERLOAD_SNAME);
+        if (r)
+                return r;
+
+        header = n_dhcp4_outgoing_get_header(message);
+        n_dhcp4_c_connection_init_header(connection, header);
+
+        message->userdata.type = type;
+
+        /*
+         * Note that some implementations expect the MESSAGE_TYPE option to be
+         * the first option, and possibly even hard-code access to it. Hence,
+         * we really should make sure to pass it first as well.
+         */
+        r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_MESSAGE_TYPE, &message_type, sizeof(message_type));
+        if (r)
+                return r;
+
+        r = n_dhcp4_outgoing_append(message,
+                                    N_DHCP4_OPTION_CLIENT_IDENTIFIER,
+                                    connection->client_config->client_id,
+                                    connection->client_config->n_client_id);
+        if (r)
+                return r;
+
+        switch (message_type) {
+        case N_DHCP4_MESSAGE_DISCOVER:
+        case N_DHCP4_MESSAGE_REQUEST:
+        case N_DHCP4_MESSAGE_INFORM: {
+                uint16_t mtu;
+
+                if (connection->probe_config->n_request_parameters > 0) {
+                        r = n_dhcp4_outgoing_append(message,
+                                                    N_DHCP4_OPTION_PARAMETER_REQUEST_LIST,
+                                                    connection->probe_config->request_parameters,
+                                                    connection->probe_config->n_request_parameters);
+                        if (r)
+                                return r;
+                }
+
+                if (via_packet_socket) {
+                        /*
+                         * In case of packet sockets, we do not support
+                         * fragmentation. Hence, our maximum message size
+                         * equals the transport MTU. In case no mtu is given,
+                         * we use the minimum size mandated by the IP spec. If
+                         * we omit the field, some implementations will
+                         * interpret this to mean any packet size is supported,
+                         * which we rather not want as default behavior (we can
+                         * always support suppressing this field, if that is
+                         * what the caller wants).
+                         */
+                        mtu = htons(connection->mtu ?: N_DHCP4_NETWORK_IP_MINIMUM_MAX_SIZE);
+                        r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE, &mtu, sizeof(mtu));
+                        if (r)
+                                return r;
+                } else {
+                        /*
+                         * Once we use UDP sockets, we support fragmentation
+                         * through the kernel IP stack. This means, the biggest
+                         * message we can receive is the maximum UDP size plus
+                         * the possible IP header. This would sum up to
+                         * 2^16-1 + 20 (or even 2^16-1 + 60 if pedantic) and
+                         * thus exceed the option field. Hence, we simply set
+                         * the option to the maximum possible value.
+                         */
+                        mtu = htons(UINT16_MAX);
+                        r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE, &mtu, sizeof(mtu));
+                        if (r)
+                                return r;
+                }
+
+                break;
+        }
+        default:
+                break;
+        }
+
+        *messagep = message;
+        message = NULL;
+        return 0;
+}
+
+/*
+ *      RFC2131 3.1
+ *
+ *      The client broadcasts a DHCPDISCOVER message on its local physical
+ *      subnet.  The DHCPDISCOVER message MAY include options that suggest
+ *      values for the network address and lease duration.  BOOTP relay
+ *      agents may pass the message on to DHCP servers not on the same
+ *      physical subnet.
+ *
+ *      RFC2131 3.5
+ *
+ *      [...] in its initial DHCPDISCOVER or DHCPREQUEST message, a client
+ *      may provide the server with a list of specific parameters the
+ *      client is interested in.  If the client includes a list of
+ *      parameters in a DHCPDISCOVER message, it MUST include that list in
+ *      any subsequent DHCPREQUEST messages.
+ *
+ *      [...]
+ *
+ *      In addition, the client may suggest values for the network address
+ *      and lease time in the DHCPDISCOVER message.  The client may include
+ *      the 'requested IP address' option to suggest that a particular IP
+ *      address be assigned, and may include the 'IP address lease time'
+ *      option to suggest the lease time it would like.  Other options
+ *      representing "hints" at configuration parameters are allowed in a
+ *      DHCPDISCOVER or DHCPREQUEST message.
+ *
+ *      RFC2131 4.4.1
+ *
+ *      The client generates and records a random transaction identifier and
+ *      inserts that identifier into the 'xid' field.  The client records its
+ *      own local time for later use in computing the lease expiration.  The
+ *      client then broadcasts the DHCPDISCOVER on the local hardware
+ *      broadcast address to the 0xffffffff IP broadcast address and 'DHCP
+ *      server' UDP port.
+ *
+ *      If the 'xid' of an arriving DHCPOFFER message does not match the
+ *      'xid' of the most recent DHCPDISCOVER message, the DHCPOFFER message
+ *      must be silently discarded.  Any arriving DHCPACK messages must be
+ *      silently discarded.
+ */
+int n_dhcp4_c_connection_discover_new(NDhcp4CConnection *connection,
+                                      NDhcp4Outgoing **requestp) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_DISCOVER);
+        if (r)
+                return r;
+
+        *requestp = message;
+        message = NULL;
+        return 0;
+}
+
+/*
+ *
+ *      RFC2131 4.1.1
+ *
+ *      The DHCPREQUEST message contains the same 'xid' as the DHCPOFFER
+ *      message.
+ *
+ *      RFC2131 4.3.2
+ *
+ *      Client inserts the address of the selected server in 'server
+ *      identifier', 'ciaddr' MUST be zero, 'requested IP address' MUST be
+ *      filled in with the yiaddr value from the chosen DHCPOFFER.
+ */
+int n_dhcp4_c_connection_select_new(NDhcp4CConnection *connection,
+                                    NDhcp4Outgoing **requestp,
+                                    NDhcp4Incoming *offer) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
+        struct in_addr client;
+        struct in_addr server;
+        uint32_t xid;
+        int r;
+
+        n_dhcp4_incoming_get_yiaddr(offer, &client);
+
+        r = n_dhcp4_incoming_query_server_identifier(offer, &server);
+        if (r)
+                return r;
+
+        r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_SELECT);
+        if (r)
+                return r;
+
+        r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_REQUESTED_IP_ADDRESS, &client, sizeof(client));
+        if (r)
+                return r;
+
+        r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_SERVER_IDENTIFIER, &server, sizeof(server));
+        if (r)
+                return r;
+
+        /*
+         * SELECT continues the transaction started by DISCOVER, and as such
+         * keeps the same start time. We also have to preserve the base time
+         * of the selected lease as well as the transaction ID.
+         */
+        message->userdata.start_time = offer->userdata.start_time;
+        message->userdata.base_time = offer->userdata.base_time;
+        n_dhcp4_incoming_get_xid(offer, &xid);
+        n_dhcp4_outgoing_set_xid(message, xid);
+
+        *requestp = message;
+        message = NULL;
+        return 0;
+}
+
+/*
+ *      RFC2131 4.3.2
+ *
+ *      'server identifier' MUST NOT be filled in, 'requested IP address'
+ *      option MUST be filled in with client's notion of its previously
+ *      assigned address. 'ciaddr' MUST be zero. The client is seeking to
+ *      verify a previously allocated, cached configuration. Server SHOULD
+ *      send a DHCPNAK message to the client if the 'requested IP address'
+ *      is incorrect, or is on the wrong network.
+ */
+int n_dhcp4_c_connection_reboot_new(NDhcp4CConnection *connection,
+                                    NDhcp4Outgoing **requestp,
+                                    const struct in_addr *client) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_REBOOT);
+        if (r)
+                return r;
+
+        r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_REQUESTED_IP_ADDRESS, client, sizeof(*client));
+        if (r)
+                return r;
+
+        *requestp = message;
+        message = NULL;
+        return 0;
+}
+
+/*
+ *      RFC2131 4.3.2
+ *
+ *      'server identifier' MUST NOT be filled in, 'requested IP address'
+ *      option MUST NOT be filled in, 'ciaddr' MUST be filled in with
+ *      client's IP address. In this situation, the client is completely
+ *      configured, and is trying to extend its lease. This message will
+ *      be unicast, so no relay agents will be involved in its
+ *      transmission.  Because 'giaddr' is therefore not filled in, the
+ *      DHCP server will trust the value in 'ciaddr', and use it when
+ *      replying to the client.
+ *
+ *      A client MAY choose to renew or extend its lease prior to T1.  The
+ *      server may choose not to extend the lease (as a policy decision by
+ *      the network administrator), but should return a DHCPACK message
+ *      regardless.
+ *
+ *      RFC2131 4.4.5
+ *
+ *      At time T1 the client moves to RENEWING state and sends (via unicast)
+ *      a DHCPREQUEST message to the server to extend its lease.  The client
+ *      sets the 'ciaddr' field in the DHCPREQUEST to its current network
+ *      address. The client records the local time at which the DHCPREQUEST
+ *      message is sent for computation of the lease expiration time.  The
+ *      client MUST NOT include a 'server identifier' in the DHCPREQUEST
+ *      message.
+ */
+int n_dhcp4_c_connection_renew_new(NDhcp4CConnection *connection,
+                                   NDhcp4Outgoing **requestp) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_RENEW);
+        if (r)
+                return r;
+
+        *requestp = message;
+        message = NULL;
+        return 0;
+}
+
+/*
+ *      RFC2131 4.3.2
+ *
+ *      'server identifier' MUST NOT be filled in, 'requested IP address'
+ *      option MUST NOT be filled in, 'ciaddr' MUST be filled in with
+ *      client's IP address. In this situation, the client is completely
+ *      configured, and is trying to extend its lease. This message MUST
+ *      be broadcast to the 0xffffffff IP broadcast address.  The DHCP
+ *      server SHOULD check 'ciaddr' for correctness before replying to
+ *      the DHCPREQUEST.
+ *
+ *      RFC2131 4.4.5
+ *
+ *      If no DHCPACK arrives before time T2, the client moves to REBINDING
+ *      state and sends (via broadcast) a DHCPREQUEST message to extend its
+ *      lease.  The client sets the 'ciaddr' field in the DHCPREQUEST to its
+ *      current network address.  The client MUST NOT include a 'server
+ *      identifier' in the DHCPREQUEST message.
+ */
+int n_dhcp4_c_connection_rebind_new(NDhcp4CConnection *connection,
+                                    NDhcp4Outgoing **requestp) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_REBIND);
+        if (r)
+                return r;
+
+        *requestp = message;
+        message = NULL;
+        return 0;
+}
+
+/*
+ *      RFC2131 3.2
+ *
+ *      If the client detects that the IP address in the DHCPACK message
+ *      is already in use, the client MUST send a DHCPDECLINE message to the
+ *      server and restarts the configuration process by requesting a
+ *      new network address.
+ *
+ *      RFC2131 4.4.4
+ *
+ *      Because the client is declining the use of the IP address supplied by
+ *      the server, the client broadcasts DHCPDECLINE messages.
+ */
+int n_dhcp4_c_connection_decline_new(NDhcp4CConnection *connection,
+                                     NDhcp4Outgoing **requestp,
+                                     NDhcp4Incoming *ack,
+                                     const char *error) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
+        struct in_addr client;
+        struct in_addr server;
+        int r;
+
+        n_dhcp4_incoming_get_yiaddr(ack, &client);
+
+        r = n_dhcp4_incoming_query_server_identifier(ack, &server);
+        if (r)
+                return r;
+
+        r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_DECLINE);
+        if (r)
+                return r;
+
+        r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_REQUESTED_IP_ADDRESS, &client, sizeof(client));
+        if (r)
+                return r;
+
+        r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_SERVER_IDENTIFIER, &server, sizeof(server));
+        if (r)
+                return r;
+
+        if (error) {
+                r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_ERROR_MESSAGE, error, strlen(error) + 1);
+                if (r)
+                        return r;
+        }
+
+        *requestp = message;
+        message = NULL;
+        return 0;
+}
+
+/*
+ *      RFC2131 3.4
+ *
+ *      If a client has obtained a network address through some other means
+ *      (e.g., manual configuration), it may use a DHCPINFORM request message
+ *      to obtain other local configuration parameters.
+ *
+ *      RFC2131 4.4
+ *
+ *      The DHCPINFORM message is not shown in figure 5.  A client simply
+ *      sends the DHCPINFORM and waits for DHCPACK messages.  Once the client
+ *      has selected its parameters, it has completed the configuration
+ *      process.
+ *
+ *      RFC2131 4.4.3
+ *
+ *      The client sends a DHCPINFORM message. The client may request
+ *      specific configuration parameters by including the 'parameter request
+ *      list' option. The client generates and records a random transaction
+ *      identifier and inserts that identifier into the 'xid' field. The
+ *      client places its own network address in the 'ciaddr' field. The
+ *      client SHOULD NOT request lease time parameters.
+ *
+ *      The client then unicasts the DHCPINFORM to the DHCP server if it
+ *      knows the server's address, otherwise it broadcasts the message to
+ *      the limited (all 1s) broadcast address.  DHCPINFORM messages MUST be
+ *      directed to the 'DHCP server' UDP port.
+ */
+int n_dhcp4_c_connection_inform_new(NDhcp4CConnection *connection,
+                                    NDhcp4Outgoing **requestp) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_INFORM);
+        if (r)
+                return r;
+
+        *requestp = message;
+        message = NULL;
+        return 0;
+}
+
+/*
+ *      RFC2131 3.1
+ *
+ *      The client may choose to relinquish its lease on a network address
+ *      by sending a DHCPRELEASE message to the server.  The client
+ *      identifies the lease to be released with its 'client identifier',
+ *      or 'chaddr' and network address in the DHCPRELEASE message. If the
+ *      client used a 'client identifier' when it obtained the lease, it
+ *      MUST use the same 'client identifier' in the DHCPRELEASE message.
+ *
+ *      RFC2131 3.2
+ *
+ *      The client may choose to relinquish its lease on a network
+ *      address by sending a DHCPRELEASE message to the server.  The
+ *      client identifies the lease to be released with its
+ *      'client identifier', or 'chaddr' and network address in the
+ *      DHCPRELEASE message.
+ *
+ *      Note that in this case, where the client retains its network
+ *      address locally, the client will not normally relinquish its
+ *      lease during a graceful shutdown.  Only in the case where the
+ *      client explicitly needs to relinquish its lease, e.g., the client
+ *      is about to be moved to a different subnet, will the client send
+ *      a DHCPRELEASE message.
+ *
+ *      RFC2131 4.4.4
+ *
+ *      The client unicasts DHCPRELEASE messages to the server.
+ *
+ *      RFC2131 4.4.6
+ *
+ *      If the client no longer requires use of its assigned network address
+ *      (e.g., the client is gracefully shut down), the client sends a
+ *      DHCPRELEASE message to the server.  Note that the correct operation
+ *      of DHCP does not depend on the transmission of DHCPRELEASE messages.
+ */
+int n_dhcp4_c_connection_release_new(NDhcp4CConnection *connection,
+                                     NDhcp4Outgoing **requestp,
+                                     const char *error) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
+        int r;
+
+        r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_RELEASE);
+        if (r)
+                return r;
+
+        r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_SERVER_IDENTIFIER, &connection->server_ip, sizeof(connection->server_ip));
+        if (r)
+                return r;
+
+        if (error) {
+                r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_ERROR_MESSAGE, error, strlen(error) + 1);
+                if (r)
+                        return r;
+        }
+
+        *requestp = message;
+        message = NULL;
+        return 0;
+}
+
+static int n_dhcp4_c_connection_send_request(NDhcp4CConnection *connection,
+                                             NDhcp4Outgoing *request,
+                                             uint64_t timestamp) {
+        int r;
+
+        /*
+         * Increment the base time and reset the xid field,
+         * where applicable. We never alter the header on
+         * resends of SELECT, as it must always match the
+         * OFFER message they are in reply to.
+         */
+        switch (request->userdata.type) {
+        case N_DHCP4_C_MESSAGE_DISCOVER:
+        case N_DHCP4_C_MESSAGE_INFORM:
+        case N_DHCP4_C_MESSAGE_REBOOT:
+        case N_DHCP4_C_MESSAGE_REBIND:
+        case N_DHCP4_C_MESSAGE_RENEW:
+                request->userdata.base_time = timestamp;
+                n_dhcp4_outgoing_set_xid(request, n_dhcp4_client_probe_config_get_random(connection->probe_config));
+
+                break;
+        case N_DHCP4_C_MESSAGE_SELECT:
+        case N_DHCP4_C_MESSAGE_DECLINE:
+        case N_DHCP4_C_MESSAGE_RELEASE:
+                break;
+        default:
+                c_assert(0);
+        }
+
+        request->userdata.send_time = timestamp;
+        request->userdata.send_jitter = (n_dhcp4_client_probe_config_get_random(connection->probe_config) % 1000000000ULL);
+        n_dhcp4_c_connection_outgoing_set_secs(request);
+
+        switch (request->userdata.type) {
+        case N_DHCP4_C_MESSAGE_DISCOVER:
+        case N_DHCP4_C_MESSAGE_SELECT:
+        case N_DHCP4_C_MESSAGE_REBOOT:
+        case N_DHCP4_C_MESSAGE_DECLINE:
+                r = n_dhcp4_c_connection_packet_broadcast(connection, request);
+                if (r)
+                        return r;
+                break;
+        case N_DHCP4_C_MESSAGE_INFORM:
+        case N_DHCP4_C_MESSAGE_REBIND:
+                r = n_dhcp4_c_connection_udp_broadcast(connection, request);
+                if (r)
+                        return r;
+
+                break;
+        case N_DHCP4_C_MESSAGE_RENEW:
+        case N_DHCP4_C_MESSAGE_RELEASE:
+                r = n_dhcp4_c_connection_udp_send(connection, request);
+                if (r)
+                        return r;
+
+                break;
+        default:
+                c_assert(0);
+        }
+
+        ++request->userdata.n_send;
+        return 0;
+}
+
+int n_dhcp4_c_connection_start_request(NDhcp4CConnection *connection,
+                                       NDhcp4Outgoing *request,
+                                       uint64_t timestamp) {
+        int r;
+
+        /*
+         * This function starts a request, but in the case of SELECT it
+         * continues a previous transaction, so we do not want to reset
+         * the start time. Only set the start time if it was not already
+         * set.
+         */
+        if (request->userdata.start_time == 0)
+                request->userdata.start_time = timestamp;
+
+        n_dhcp4_outgoing_free(connection->request);
+        connection->request = request;
+
+        r = n_dhcp4_c_connection_send_request(connection, request, timestamp);
+        if (r)
+                return r;
+
+        return 0;
+}
+
+int n_dhcp4_c_connection_dispatch_timer(NDhcp4CConnection *connection,
+                                        uint64_t timestamp) {
+        uint64_t timeout;
+        int r;
+
+        if (!connection->request)
+                return 0;
+
+        n_dhcp4_c_connection_get_timeout(connection, &timeout);
+
+        if (timeout > timestamp)
+                return 0;
+
+        r = n_dhcp4_c_connection_send_request(connection, connection->request, timestamp);
+        if (r)
+                return r;
+
+        return 0;
+}
+
+int n_dhcp4_c_connection_dispatch_io(NDhcp4CConnection *connection,
+                                     NDhcp4Incoming **messagep) {
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL;
+        uint8_t type;
+        int r;
+
+        switch (connection->state) {
+        case N_DHCP4_C_CONNECTION_STATE_PACKET:
+                r = n_dhcp4_c_socket_packet_recv(connection->fd_packet,
+                                                 connection->scratch_buffer,
+                                                 sizeof(connection->scratch_buffer),
+                                                 &message);
+                if (r)
+                        return r;
+
+                break;
+        case N_DHCP4_C_CONNECTION_STATE_DRAINING:
+                r = n_dhcp4_c_socket_packet_recv(connection->fd_packet,
+                                                 connection->scratch_buffer,
+                                                 sizeof(connection->scratch_buffer),
+                                                 &message);
+                if (!r)
+                        break;
+                else if (r != N_DHCP4_E_AGAIN)
+                        return r;
+
+                /*
+                 * The UDP socket is open and the packet socket has been shut down
+                 * and drained, clean up the packet socket and fall through to
+                 * dispatching the UDP socket.
+                 */
+                r = epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, connection->fd_packet, NULL);
+                c_assert(!r);
+                connection->fd_packet = c_close(connection->fd_packet);
+                connection->state = N_DHCP4_C_CONNECTION_STATE_UDP;
+
+                /* fall-through */
+        case N_DHCP4_C_CONNECTION_STATE_UDP:
+                r = n_dhcp4_c_socket_udp_recv(connection->fd_udp,
+                                              connection->scratch_buffer,
+                                              sizeof(connection->scratch_buffer),
+                                              &message);
+                if (r)
+                        return r;
+
+                break;
+        default:
+                abort();
+                return -ENOTRECOVERABLE;
+        }
+
+        r = n_dhcp4_c_connection_verify_incoming(connection, message, &type);
+        if (r)
+                return r;
+
+        switch (type) {
+        case N_DHCP4_MESSAGE_OFFER:
+        case N_DHCP4_MESSAGE_ACK:
+        case N_DHCP4_MESSAGE_NAK:
+                /*
+                 * Remember the start time of the transaction, and the base
+                 * time of any relative timestamps from the pending request.
+                 * Thes same times applies to the response, and sholud be
+                 * copied over.
+                 */
+                message->userdata.start_time = connection->request->userdata.start_time;
+                message->userdata.base_time = connection->request->userdata.base_time;
+
+                if (type != N_DHCP4_MESSAGE_OFFER) {
+                        /*
+                         * We only allow one reply to ACK or NAK, but for OFFER we must
+                         * accept several, so we do not free the pinned request.
+                         */
+                        connection->request = n_dhcp4_outgoing_free(connection->request);
+                }
+
+                break;
+        default:
+                break;
+        }
+
+        *messagep = message;
+        message = NULL;
+        return 0;
+}
diff --git a/shared/n-dhcp4/src/n-dhcp4-c-lease.c b/shared/n-dhcp4/src/n-dhcp4-c-lease.c
new file mode 100644
index 00000000..db1ffbae
--- /dev/null
+++ b/shared/n-dhcp4/src/n-dhcp4-c-lease.c
@@ -0,0 +1,356 @@
+/*
+ * DHCP4 Client Leases
+ *
+ * This implements the public API wrapping DHCP4 client leases. A lease object
+ * conists of the information given to us from the server, together with the
+ * timestamp recording the start of the validity of the lease.
+ *
+ * A probe may yield many OFFERS, each of which contains a lease object. One of
+ * these offers may be SELECTED, which implicitly rejects all the others.
+ * The server may then ACK or NAK the lease which tells us whether or not we
+ * are permitted to start using it. Once an ACK has been received, we can
+ * configure the address, and only then can we SELECT the lease. If we
+ * determine that the offered lease was not appropriate after all we
+ * may DECLINE it instead.
+ */
+
+#include <assert.h>
+#include <c-list.h>
+#include <c-stdaux.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include "n-dhcp4.h"
+#include "n-dhcp4-private.h"
+
+/*
+ * Compute the absolute timeouts from an incoming message. A message contains relative timeouts and the userdata
+ * of the incoming message is set to the offset we must apply to get the absolute values.
+ *
+ * The special value UINT64_MAX is returned to indicate no or infinite timeouts. In case the given timeouts
+ * are invalid relative to each other, we recompute T1 and/or T2 to take their default values. Later timeouts
+ * take predecende above earlier ones (T1 is adjusted if it conflicts with T2, etc).
+ */
+static int n_dhcp4_incoming_get_timeouts(NDhcp4Incoming *message, uint64_t *t1p, uint64_t *t2p, uint64_t *lifetimep) {
+        uint64_t lifetime, t2, t1;
+        uint32_t u32;
+        int r;
+
+        r = n_dhcp4_incoming_query_lifetime(message, &u32);
+        if (r == N_DHCP4_E_UNSET) {
+                lifetime = UINT64_MAX;
+        } else if (r) {
+                return r;
+        } else if (u32 == UINT32_MAX) {
+                lifetime = UINT64_MAX;
+        } else {
+                if (u32 == UINT32_MAX)
+                        lifetime = UINT64_MAX;
+                else
+                        lifetime = u32 * (1000000000ULL);
+        }
+
+        r = n_dhcp4_incoming_query_t2(message, &u32);
+        if (r == N_DHCP4_E_UNSET) {
+                if (lifetime == UINT64_MAX)
+                        t2 = UINT64_MAX;
+                else
+                        t2 = (lifetime * 7) / 8;
+        } else if (r) {
+                return r;
+        } else {
+                if (u32 == UINT32_MAX)
+                        t2 = UINT64_MAX;
+                else
+                        t2 = u32 * (1000000000ULL);
+
+                if (t2 > lifetime)
+                        t2 = (lifetime * 7) / 8;
+        }
+
+        r = n_dhcp4_incoming_query_t1(message, &u32);
+        if (r == N_DHCP4_E_UNSET) {
+                if (t2 == UINT64_MAX)
+                        t1 = UINT64_MAX;
+                else
+                        t1 = (t2 * 4) / 7;
+        } else if (r) {
+                return r;
+        } else {
+                if (u32 == UINT32_MAX)
+                        t1 = UINT64_MAX;
+                else
+                        t1 = u32 * (1000000000ULL);
+
+                if (t1 > t2)
+                        t1 = (t2 * 4) / 7;
+        }
+
+        if (lifetime != UINT64_MAX)
+                lifetime += message->userdata.base_time;
+        if (t2 != UINT64_MAX)
+                t2 += message->userdata.base_time;
+        if (t1 != UINT64_MAX)
+                t1 += message->userdata.base_time;
+
+        *lifetimep = lifetime;
+        *t2p = t2;
+        *t1p = t1;
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_lease_new() - allocate new client lease object
+ * @leasep:                     output argumnet for new client lease object
+ * @message:                    incoming message representing the lease
+ *
+ * This creates a new client lease object. Client lease objects are simple
+ * wrappers around an incoming message representing a lease.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int n_dhcp4_client_lease_new(NDhcp4ClientLease **leasep, NDhcp4Incoming *message) {
+        _c_cleanup_(n_dhcp4_client_lease_unrefp) NDhcp4ClientLease *lease = NULL;
+        int r;
+
+        c_assert(leasep);
+
+        lease = malloc(sizeof(*lease));
+        if (!lease)
+                return -ENOMEM;
+
+        *lease = (NDhcp4ClientLease)N_DHCP4_CLIENT_LEASE_NULL(*lease);
+
+        r = n_dhcp4_incoming_get_timeouts(message, &lease->t1, &lease->t2, &lease->lifetime);
+        if (r)
+                return r;
+
+        lease->message = message;
+        *leasep = lease;
+        lease = NULL;
+        return 0;
+}
+
+static void n_dhcp4_client_lease_free(NDhcp4ClientLease *lease) {
+        n_dhcp4_client_lease_unlink(lease);
+        n_dhcp4_incoming_free(lease->message);
+        free(lease);
+}
+
+/**
+ * n_dhcp4_client_lease_ref() - reference client lease
+ * @lease:                      the client lease object to reference
+ *
+ * Take a new reference to a client lease.
+ *
+ * Return: the lease.
+ */
+_c_public_ NDhcp4ClientLease *n_dhcp4_client_lease_ref(NDhcp4ClientLease *lease) {
+        if (lease)
+                ++lease->n_refs;
+        return lease;
+}
+
+/**
+ * n_dhcp4_client_lease_unref() - dereference client lease
+ * @lease:                      the client lease object to dereference
+ *
+ * Relase a reference to a client lease.
+ *
+ * Return: NULL.
+ */
+_c_public_ NDhcp4ClientLease *n_dhcp4_client_lease_unref(NDhcp4ClientLease *lease) {
+        if (lease && !--lease->n_refs)
+                n_dhcp4_client_lease_free(lease);
+        return NULL;
+}
+
+/**
+ * n_dhcp4_client_lease_link() - link lease into probe
+ * @lease:                      the lease to operate on
+ * @probe:                      the probe to link the lease into
+ *
+ * Associate a lease with a probe. The lease may not already be linked.
+ */
+void n_dhcp4_client_lease_link(NDhcp4ClientLease *lease, NDhcp4ClientProbe *probe) {
+        c_assert(!lease->probe);
+        c_assert(!c_list_is_linked(&lease->probe_link));
+
+        lease->probe = probe;
+        c_list_link_tail(&probe->lease_list, &lease->probe_link);
+}
+
+/**
+ * n_dhcp4_client_lease_unlink() - unlinke lease from its probe
+ * @lease:                      the lease to operate on
+ *
+ * Dissassociate a lease from a probe if it is associated with one. Otherwise,
+ * this is a noop.
+ */
+void n_dhcp4_client_lease_unlink(NDhcp4ClientLease *lease) {
+        lease->probe = NULL;
+        c_list_unlink(&lease->probe_link);
+}
+
+/**
+ * n_dhcp4_client_lease_get_yiaddr() - get the IP address
+ * @lease:                      the lease to operate on
+ * @yiaddr:                     return argument for the IP address
+ *
+ * Gets the IP address cotained in the lease. Or INADDR_ANY if the lease
+ * does not contain an IP address.
+ */
+_c_public_ void n_dhcp4_client_lease_get_yiaddr(NDhcp4ClientLease *lease, struct in_addr *yiaddr) {
+        NDhcp4Header *header = n_dhcp4_incoming_get_header(lease->message);
+
+        yiaddr->s_addr = header->yiaddr;
+}
+
+/**
+ * n_dhcp4_client_lease_get_lifetime() - get the lifetime
+ * @lease:                      the lease to operate on
+ * @ns_lifetimep:               return argument for the lifetime in nano seconds
+ *
+ * Gets the end of the lease's lifetime in nanoseconds according to CLOCK_BOOTTIME,
+ * or (uint64_t)-1 for permanent leases.
+ */
+_c_public_ void n_dhcp4_client_lease_get_lifetime(NDhcp4ClientLease *lease, uint64_t *ns_lifetimep) {
+        *ns_lifetimep = lease->lifetime;
+}
+
+/**
+ * n_dhcp4_client_lease_query() - query the lease for an option
+ * @lease:                      the lease to operate on
+ * @option:                     the DHCP4 option code
+ * @datap:                      return argument of the data pointer
+ * @n_datap:                    return argument of data length in bytes
+ *
+ * Query the lease for a given option. Options internal to the DHCP protocol cannot
+ * be queried, and only options that were explicitly requested can be queried.
+ *
+ * Return: 0 on success,
+ *         N_DCHP4_E_INTERNAL if an invalid option is queried,
+ *         N_DHCP4_E_UNSET if the lease did not contain the option, or
+ *         a negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_lease_query(NDhcp4ClientLease *lease, uint8_t option, uint8_t **datap, size_t *n_datap) {
+        switch (option) {
+        case N_DHCP4_OPTION_PAD:
+        case N_DHCP4_OPTION_REQUESTED_IP_ADDRESS:
+        case N_DHCP4_OPTION_IP_ADDRESS_LEASE_TIME:
+        case N_DHCP4_OPTION_OVERLOAD:
+        case N_DHCP4_OPTION_MESSAGE_TYPE:
+        case N_DHCP4_OPTION_SERVER_IDENTIFIER:
+        case N_DHCP4_OPTION_PARAMETER_REQUEST_LIST:
+        case N_DHCP4_OPTION_ERROR_MESSAGE:
+        case N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE:
+        case N_DHCP4_OPTION_RENEWAL_T1_TIME:
+        case N_DHCP4_OPTION_REBINDING_T2_TIME:
+        case N_DHCP4_OPTION_END:
+                return N_DHCP4_E_INTERNAL;
+        }
+
+        /* XXX: refuse to return options that were not requested */
+
+        return n_dhcp4_incoming_query(lease->message, option, datap, n_datap);
+}
+
+/**
+ * n_dhcp4_client_lease_select() - select an offered lease
+ * @lease:                      lease to operate on
+ *
+ * Select a lease. This must be a lease that was offered, once
+ * one of the leases that were offered in response to a probe was
+ * selected none of the others can be.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_lease_select(NDhcp4ClientLease *lease) {
+        NDhcp4ClientLease *l, *t_l;
+        NDhcp4ClientProbe *probe;
+        int r;
+
+        /* XXX error handling, this must be an OFFER */
+
+        if (!lease->probe)
+                return -ENOTRECOVERABLE;
+        if (lease->probe->current_lease)
+                return -ENOTRECOVERABLE;
+
+        r = n_dhcp4_client_probe_transition_select(lease->probe, lease->message, n_dhcp4_gettime(CLOCK_BOOTTIME));
+        if (r)
+                return r;
+
+        /*
+         * Only one of the offered leases can be selected, so flush the list.
+         * All offered lease, including this one are now dead.
+         */
+        probe = lease->probe;
+        c_list_for_each_entry_safe(l, t_l, &probe->lease_list, probe_link)
+                n_dhcp4_client_lease_unlink(l);
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_lease_accept() - accept an ack'ed lease
+ * @lease:                      lease to operate on
+ *
+ * Accept a lease. This must be a lease that was ack'ed by the
+ * server.
+ *
+ * The offered IP address must be fully configured before the lease
+ * can be accepted.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_lease_accept(NDhcp4ClientLease *lease) {
+        int r;
+
+        /* XXX error handling, this must be an ACK */
+
+        if (!lease->probe)
+                return -ENOTRECOVERABLE;
+        if (lease->probe->current_lease != lease)
+                return -ENOTRECOVERABLE;
+
+        r = n_dhcp4_client_probe_transition_accept(lease->probe, lease->message);
+        if (r)
+                return r;
+
+        n_dhcp4_client_lease_unlink(lease);
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_lease_decline() - decline an ack'ed lease
+ * @lease:                      lease to operate on
+ *
+ * Decline a lease. This must be a lease that was ack'ed by the
+ * server.
+ *
+ * The offered IP address must not be used once the lease has been
+ * decline.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_lease_decline(NDhcp4ClientLease *lease, const char *error) {
+        int r;
+
+        /* XXX: error handling, this must be an ACK */
+
+        if (!lease->probe)
+                return -ENOTRECOVERABLE;
+        if (lease->probe->current_lease != lease)
+                return -ENOTRECOVERABLE;
+
+        r = n_dhcp4_client_probe_transition_decline(lease->probe, lease->message, error, n_dhcp4_gettime(CLOCK_BOOTTIME));
+        if (r)
+                return r;
+
+        lease->probe->current_lease = n_dhcp4_client_lease_unref(lease->probe->current_lease);
+        n_dhcp4_client_lease_unlink(lease);
+
+        return 0;
+}
diff --git a/shared/n-dhcp4/src/n-dhcp4-c-probe.c b/shared/n-dhcp4/src/n-dhcp4-c-probe.c
new file mode 100644
index 00000000..308cff83
--- /dev/null
+++ b/shared/n-dhcp4/src/n-dhcp4-c-probe.c
@@ -0,0 +1,1203 @@
+/*
+ * DHCPv4 Client Probes
+ *
+ * The probe object is used to represent the lifetime of a DHCP client session.
+ * A running probe discovers DHCP servers, requests a lease, and maintains that
+ * lease.
+ */
+
+#include <assert.h>
+#include <c-list.h>
+#include <c-siphash.h>
+#include <c-stdaux.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/auxv.h>
+#include "n-dhcp4.h"
+#include "n-dhcp4-private.h"
+
+
+static int n_dhcp4_client_probe_option_new(NDhcp4ClientProbeOption **optionp,
+                                    uint8_t option,
+                                    const void *data,
+                                    uint8_t n_data) {
+        NDhcp4ClientProbeOption *op;
+
+        op = malloc(sizeof(op) + n_data);
+        if (!op)
+                return -ENOMEM;
+
+        op->option = option;
+        op->n_data = n_data;
+        memcpy(op->data, data, n_data);
+
+        *optionp = op;
+        return 0;
+}
+
+static void n_dhcp4_client_probe_option_free(NDhcp4ClientProbeOption *option) {
+        if (option)
+                free(option);
+}
+
+/**
+ * n_dhcp4_client_probe_config_new() - create new probe configuration
+ * @configp:                    output argument to store new configuration
+ *
+ * This creates a new probe configuration object. The object is a collection of
+ * parameters for probes. No data verification is done by the configuration
+ * object. Instead, when passing the configuration to the constructor of a
+ * probe, this constructor will perform parameter validation.
+ *
+ * A probe configuration is an unlinked object only used to pass information to
+ * a probe constructor. The caller fully owns the returned configuration object
+ * and is responsible to free it when no longer needed.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_probe_config_new(NDhcp4ClientProbeConfig **configp) {
+        _c_cleanup_(n_dhcp4_client_probe_config_freep) NDhcp4ClientProbeConfig *config = NULL;
+
+        config = calloc(1, sizeof(*config));
+        if (!config)
+                return -ENOMEM;
+
+        *config = (NDhcp4ClientProbeConfig)N_DHCP4_CLIENT_PROBE_CONFIG_NULL(*config);
+
+        *configp = config;
+        config = NULL;
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_probe_config_free() - destroy probe configuration
+ * @config:                     configuration to operate on, or NULL
+ *
+ * This destroys a probe configuration object and deallocates all its
+ * resources.
+ *
+ * If @config is NULL, this is a no-op.
+ *
+ * Return: NULL is returned.
+ */
+_c_public_ NDhcp4ClientProbeConfig *n_dhcp4_client_probe_config_free(NDhcp4ClientProbeConfig *config) {
+        if (!config)
+                return NULL;
+
+        for (unsigned int i = 0; i <= UINT8_MAX; ++i)
+                n_dhcp4_client_probe_option_free(config->options[i]);
+
+        free(config);
+
+        return NULL;
+}
+
+/**
+ * n_dhcp4_client_probe_config_dup() - duplicate probe configuration
+ * @config:                     configuration to operate on
+ * @dupp:                       output argument for duplicate
+ *
+ * This duplicates the probe configuration given as @config and returns it in
+ * @dupp to the caller.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int n_dhcp4_client_probe_config_dup(NDhcp4ClientProbeConfig *config,
+                                    NDhcp4ClientProbeConfig **dupp) {
+        _c_cleanup_(n_dhcp4_client_probe_config_freep) NDhcp4ClientProbeConfig *dup = NULL;
+        int r;
+
+        r = n_dhcp4_client_probe_config_new(&dup);
+        if (r)
+                return r;
+
+        dup->inform_only = config->inform_only;
+        dup->init_reboot = config->init_reboot;
+        dup->requested_ip = config->requested_ip;
+        dup->ms_start_delay = config->ms_start_delay;
+
+        for (unsigned int i = 0; i < config->n_request_parameters; ++i)
+                dup->request_parameters[dup->n_request_parameters++] = config->request_parameters[i];
+
+        for (unsigned int i = 0; i <= UINT8_MAX; ++i) {
+                if (!config->options[i])
+                        break;
+
+                r = n_dhcp4_client_probe_option_new(&dup->options[i],
+                                                    config->options[i]->option,
+                                                    config->options[i]->data,
+                                                    config->options[i]->n_data);
+                if (r)
+                        return r;
+        }
+
+        *dupp = dup;
+        dup = NULL;
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_probe_config_set_inform_only() - set inform-only property
+ * @config:                     configuration to operate on
+ * @inform_only:                value to set
+ *
+ * This sets the inform-only property of the given configuration object. This
+ * property controls whether the client probe should request a full lease, or
+ * whether it should just ask for auxiliary information without requesting an
+ * address.
+ *
+ * The default is to request a full lease and address. If inform-only is set to
+ * true, only auxiliary information will be requested.
+ *
+ * XXX: This is currently not implemented, and setting the property has no effect.
+ */
+_c_public_ void n_dhcp4_client_probe_config_set_inform_only(NDhcp4ClientProbeConfig *config, bool inform_only) {
+        config->inform_only = inform_only;
+}
+
+/**
+ * n_dhcp4_client_probe_config_set_init_reboot() - set init-reboot property
+ * @config:                     configuration to operate on
+ * @init_reboot:                value to set
+ *
+ * This sets the init-reboot property of the given configuration object. If this
+ * is enabled, a requested IP address must also be set.
+ *
+ * The default is false. If set to true, a probe will make use of the
+ * INIT-REBOOT path, as described by the DHCP specification. In most cases, you
+ * do not want this.
+ *
+ * XXX: This is currently not implemented, and setting the property has no effect.
+ *
+ * Background: The INIT-REBOOT path allows a DHCP client to skip
+ *             server-discovery when rebooting/resuming their machine. The DHCP
+ *             client simply re-requests the lease it had acquired before. This
+ *             saves one roundtrip in the success-case, since the DISCOVER step
+ *             is skipped. However, there are little to no timeouts involved,
+ *             so the roundtrip should be barely noticeable. In contrast, if
+ *             the INIT-REBOOT fails (because the lease is no longer valid, or
+ *             not valid on this network), the client has to wait for a
+ *             possible answer to the request before actually starting the DHCP
+ *             process all over. This significantly increases the time needed
+ *             to switch networks.
+ *             The INIT-REBOOT state might have been a real improvements with
+ *             the old resend-timeouts mandated by the DHCP specification.
+ *             However, on modern networks with improved timeout values we
+ *             recommend against using it.
+ */
+_c_public_ void n_dhcp4_client_probe_config_set_init_reboot(NDhcp4ClientProbeConfig *config, bool init_reboot) {
+        config->init_reboot = init_reboot;
+}
+
+/**
+ * n_dhcp4_client_probe_config_set_requested_ip() - set requested-ip property
+ * @config:                     configuration to operate on
+ * @ip:                         value to set
+ *
+ * This sets the requested-ip property of the given configuration object.
+ *
+ * The default is all 0. If set to something else, the DHCP discovery will
+ * include this IP in its requests to tell DHCP servers which address to pick.
+ * Servers are not required to honor this, nor does this have any effect on
+ * servers not serving this address.
+ *
+ * This field should always be set if the caller knows of an address that was
+ * previously acquired on this network. It serves as hint to servers and will
+ * allow them to provide the same address again.
+ */
+_c_public_ void n_dhcp4_client_probe_config_set_requested_ip(NDhcp4ClientProbeConfig *config, struct in_addr ip) {
+        config->requested_ip = ip;
+}
+
+/**
+ * n_dhcp4_client_probe_config_set_start_delay() - set start delay
+ * @config:                     configuration to operate on
+ * @msecs:                      value to set
+ *
+ * This sets the start delay property of the given configuration object.
+ *
+ * The default is 9000 ms, which is based on RFC2131. In the RFC the start
+ * delay is specified to be a random value in the range 1000 to 10.000 ms.
+ * However, there does not appear to be any particular reason to
+ * unconditionally wait at least one second, so we move the range down to
+ * start at 0 ms. The reaon for the random delay is to avoid network-wide
+ * events causing too much simultaneous network traffic. However, on modern
+ * networks, a more reasonable value may be in the 10 ms range.
+ */
+_c_public_ void n_dhcp4_client_probe_config_set_start_delay(NDhcp4ClientProbeConfig *config, uint64_t msecs) {
+        config->ms_start_delay = msecs;
+}
+
+/**
+ * n_dhpc4_client_probe_config_request_option() - append option to request from the server
+ * @config:                     configuration to operate on
+ * @option:                     option to request
+ *
+ * This adds an option to the list of options to request from the server.
+ *
+ * A server may send options that we do not requst, and it may omit options
+ * that we do request. However, to increase the likelyhood of uniform behavior
+ * between server implementations, we do not expose options that were not
+ * explicitly requested.
+ *
+ * When called multiple times, the order matters. Earlier requests are
+ * considered higher priority than later requests, in case the server must omit
+ * some, due to a lack of space. If the same option is requested more than once,
+ * only the first call has an effect.
+ */
+_c_public_ void n_dhcp4_client_probe_config_request_option(NDhcp4ClientProbeConfig *config, uint8_t option) {
+        for (unsigned int i = 0; i < config->n_request_parameters; ++i) {
+                if (config->request_parameters[i] == option)
+                        return;
+        }
+
+        c_assert(config->n_request_parameters <= UINT8_MAX);
+
+        config->request_parameters[config->n_request_parameters++] = option;
+}
+
+/**
+ * n_dhcp4_client_probe_config_append_option() - append option to outgoing messages
+ * @config:                     configuration to operate on
+ * @option:                     DHCP option number
+ * @data:                       payload
+ * @n_data:                     number of bytes in payload
+ *
+ * This sets extra options on a given configuration object.
+ *
+ * These options are appended verbatim to outgoing messages where
+ * that is supported by the specification. The same options are
+ * appended to all messages.
+ *
+ * No option may be appended more than once. Options considered internal
+ * to the DHCP protocol may not be appended.
+ *
+ * Return: 0 on success, N_DHCP4_E_DUPLICATE_OPTION if an option has already been
+ *         appended, N_DHCP4_E_INTERNAL if the option is not configurable, or
+ *         a negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_probe_config_append_option(NDhcp4ClientProbeConfig *config,
+                                                       uint8_t option,
+                                                       const void *data,
+                                                       uint8_t n_data) {
+        int r;
+
+        /* XXX: filter internal options */
+
+        for (unsigned int i = 0; i <= UINT8_MAX; ++i) {
+                if (config->options[i]) {
+                        if (config->options[i]->option == option)
+                                return N_DHCP4_E_DUPLICATE_OPTION;
+
+                        continue;
+                }
+
+                r = n_dhcp4_client_probe_option_new(&config->options[i],
+                                                    option,
+                                                    data,
+                                                    n_data);
+                if (r)
+                        return r;
+
+                return 0;
+        }
+
+        c_assert(0);
+        return -ENOTRECOVERABLE;
+}
+
+static void n_dhcp4_client_probe_config_initialize_random_seed(NDhcp4ClientProbeConfig *config) {
+        uint8_t hash_seed[] = {
+                0x25, 0x3f, 0x02, 0x75, 0x3a, 0xb8, 0x4f, 0x91,
+                0x9d, 0x0a, 0xd6, 0x15, 0x9d, 0x72, 0x7b, 0xcb,
+        };
+        CSipHash hash = C_SIPHASH_NULL;
+        unsigned short int seed16v[3];
+        const uint8_t *p;
+        uint64_t u64;
+        int r;
+
+        /*
+         * Initialize seed48_r(3)
+         *
+         * We need random jitter for all timeouts and delays, used to reduce
+         * network traffic during bursts. This is not meant as security measure
+         * but only meant to improve network utilization during bursts. The
+         * random source is thus negligible. However, we want, under all
+         * circumstances, avoid two instances running with the same seed. Thus
+         * we source the seed from AT_RANDOM, which grants us a per-process
+         * unique seed. We then add the current time to make sure consequetive
+         * instances use different seeds (to avoid clashes if processes are
+         * duplicated, or similar), and lastly we add the config memory address
+         * to avoid clashes of multiple parallel instances.
+         *
+         * Again, none of these are meant as security measure, but only to
+         * avoid *ACCIDENTAL* seed clashes. That is, in the case that many
+         * transactions are started in parallel, we delay the individual
+         * messages (as described in the spec), to reduce the traffic on the
+         * network and the chance of packets being dropped (and thus triggering
+         * timeouts and resends).
+         *
+         * We hash everything through SipHash, to avoid exposing AT_RANDOM and
+         * other sources to the network. We use a static salt to distinguish it
+         * from other implementations using the same random source.
+         */
+        c_siphash_init(&hash, hash_seed);
+
+        p = (const uint8_t *)getauxval(AT_RANDOM);
+        if (p)
+                c_siphash_append(&hash, p, 16);
+
+        u64 = n_dhcp4_gettime(CLOCK_MONOTONIC);
+        c_siphash_append(&hash, (const uint8_t *)&u64, sizeof(u64));
+
+        c_siphash_append(&hash, (const uint8_t *)&config, sizeof(config));
+
+        u64 = c_siphash_finalize(&hash);
+
+        seed16v[0] = (u64 >>  0) ^ (u64 >> 48);
+        seed16v[1] = (u64 >> 16) ^ (u64 >>  0);
+        seed16v[2] = (u64 >> 32) ^ (u64 >> 16);
+
+        r = seed48_r(seed16v, &config->entropy);
+        c_assert(!r);
+}
+
+/**
+ * n_dhcp4_client_probe_config_get_random() - get random data
+ * @config:                     config object to operate on
+ *
+ * Fetch the next 32bit random number from the entropy pool in @config.
+ * Note that this is in no way suitable for security purposes.
+ *
+ * Return: the random data.
+ */
+uint32_t n_dhcp4_client_probe_config_get_random(NDhcp4ClientProbeConfig *config) {
+        long int result;
+        int r;
+
+        r = mrand48_r(&config->entropy, &result);
+        c_assert(!r);
+
+        return result;
+};
+
+/**
+ * n_dhcp4_client_probe_new() - create new client probe
+ * @probep:                     output argument for new client probe
+ * @config:                     probe configuration
+ * @client:                     client to probe on behalf of
+ * @ns_now:                     the current time
+ *
+ * This creates a new client probe object.
+ *
+ * If one is already running, the new one will be immediately (but asynchronously)
+ * cancelled. Otherwise, a DISCOVER event is scheduled after a randomized delay.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+int n_dhcp4_client_probe_new(NDhcp4ClientProbe **probep,
+                             NDhcp4ClientProbeConfig *config,
+                             NDhcp4Client *client,
+                             uint64_t ns_now) {
+        _c_cleanup_(n_dhcp4_client_probe_freep) NDhcp4ClientProbe *probe = NULL;
+        bool active;
+        int r;
+
+        /*
+         * If there is already a probe attached, we create the new probe in
+         * detached state. It will not be linked into the epoll context and not
+         * be useful in any way. We immediately raise the CANCELLED event to
+         * notify the caller about it.
+         */
+        active = !client->current_probe;
+
+        probe = calloc(1, sizeof(*probe));
+        if (!probe)
+                return -ENOMEM;
+
+        *probe = (NDhcp4ClientProbe)N_DHCP4_CLIENT_PROBE_NULL(*probe);
+        probe->client = n_dhcp4_client_ref(client);
+
+        r = n_dhcp4_client_probe_config_dup(config, &probe->config);
+        if (r)
+                return r;
+
+        /*
+         * XXX: make seed initialization optional, so the entropy can be reused.
+         */
+        n_dhcp4_client_probe_config_initialize_random_seed(probe->config);
+
+        r = n_dhcp4_c_connection_init(&probe->connection,
+                                      client->config,
+                                      probe->config,
+                                      active ? client->fd_epoll : -1);
+        if (r)
+                return r;
+
+        if (active) {
+                /*
+                 * Defer the sending of DISCOVER by a random amount (by default up to 9 seconds).
+                 */
+                probe->ns_deferred = ns_now + (n_dhcp4_client_probe_config_get_random(probe->config) % (probe->config->ms_start_delay * 1000000ULL));
+                probe->client->current_probe = probe;
+        } else {
+                r = n_dhcp4_client_probe_raise(probe,
+                                               NULL,
+                                               N_DHCP4_CLIENT_EVENT_CANCELLED);
+                if (r)
+                        return r;
+        }
+
+        *probep = probe;
+        probe = NULL;
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_probe_free() - destroy a probe
+ * @probe:                      probe to operate on, or NULL
+ *
+ * This destroys a probe object and deallocates all its resources.
+ *
+ * If @probe is NULL, this is a no-op.
+ *
+ * Return: NULL is returned.
+ */
+_c_public_ NDhcp4ClientProbe *n_dhcp4_client_probe_free(NDhcp4ClientProbe *probe) {
+        NDhcp4CEventNode *node, *t_node;
+        NDhcp4ClientLease *lease, *t_lease;
+
+        if (!probe)
+                return NULL;
+
+        c_list_for_each_entry_safe(lease, t_lease, &probe->lease_list, probe_link)
+                n_dhcp4_client_lease_unlink(lease);
+
+        c_list_for_each_entry_safe(node, t_node, &probe->event_list, probe_link)
+                n_dhcp4_c_event_node_free(node);
+
+        if (probe == probe->client->current_probe)
+                probe->client->current_probe = NULL;
+
+        n_dhcp4_client_lease_unref(probe->current_lease);
+        n_dhcp4_c_connection_deinit(&probe->connection);
+        n_dhcp4_client_unref(probe->client);
+        n_dhcp4_client_probe_config_free(probe->config);
+
+        c_assert(c_list_is_empty(&probe->lease_list));
+        c_assert(c_list_is_empty(&probe->event_list));
+        free(probe);
+
+        return NULL;
+}
+
+/**
+ * n_dhcp4_client_probe_set_userdata() - set userdata pointer
+ * @probe:                      the probe to operate on
+ * @userdata:                   pointer to userdata
+ *
+ * Set a userdata pointer. The pointed to data is still owned by the caller, and
+ * is completely opaque to the probe.
+ */
+_c_public_ void n_dhcp4_client_probe_set_userdata(NDhcp4ClientProbe *probe, void *userdata) {
+        probe->userdata = userdata;
+}
+
+/**
+ * n_dhcp4_client_probe_get_userdata() - get userdata pointer
+ * @probe:                      the probe to operate on
+ * @userdatap:                  return pointer for userdata pointer
+ *
+ * Get the userdata pointer. The lifetime of the userdata and making sure it is
+ * still valid when accessed via the probe is the responsibility of the caller.
+ */
+_c_public_ void n_dhcp4_client_probe_get_userdata(NDhcp4ClientProbe *probe, void **userdatap) {
+        *userdatap = probe->userdata;
+}
+
+/**
+ * n_dhcp4_client_probe_raise() - XXX
+ */
+int n_dhcp4_client_probe_raise(NDhcp4ClientProbe *probe, NDhcp4CEventNode **nodep, unsigned int event) {
+        NDhcp4CEventNode *node;
+        int r;
+
+        r = n_dhcp4_client_raise(probe->client, &node, event);
+        if (r)
+                return r;
+
+        switch (event) {
+        case N_DHCP4_CLIENT_EVENT_OFFER:
+                node->event.offer.probe = probe;
+                break;
+        case N_DHCP4_CLIENT_EVENT_GRANTED:
+                node->event.granted.probe = probe;
+                break;
+        case N_DHCP4_CLIENT_EVENT_RETRACTED:
+                node->event.retracted.probe = probe;
+                break;
+        case N_DHCP4_CLIENT_EVENT_EXTENDED:
+                node->event.extended.probe = probe;
+                break;
+        case N_DHCP4_CLIENT_EVENT_EXPIRED:
+                node->event.expired.probe = probe;
+                break;
+        case N_DHCP4_CLIENT_EVENT_CANCELLED:
+                node->event.cancelled.probe = probe;
+                break;
+        default:
+                c_assert(0);
+                n_dhcp4_c_event_node_free(node);
+                return -ENOTRECOVERABLE;
+        }
+
+        if (nodep)
+                *nodep = node;
+        return 0;
+}
+
+void n_dhcp4_client_probe_get_timeout(NDhcp4ClientProbe *probe, uint64_t *timeoutp) {
+        uint64_t t1 = 0;
+        uint64_t t2 = 0;
+        uint64_t lifetime = 0;
+        uint64_t timeout = 0;
+
+        if (probe->current_lease) {
+                t1 = probe->current_lease->t1;
+                t2 = probe->current_lease->t2;
+                lifetime = probe->current_lease->lifetime;
+        }
+
+        n_dhcp4_c_connection_get_timeout(&probe->connection, &timeout);
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+                if (probe->ns_deferred && (!timeout || probe->ns_deferred < timeout))
+                        timeout = probe->ns_deferred;
+
+                break;
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+                if (t1 && (!timeout || t1 < timeout))
+                        timeout = t1;
+
+                /* fall-through */
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+                if (t2 && (!timeout || t2 < timeout))
+                        timeout = t2;
+
+                /* fall-through */
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+                if (lifetime && (!timeout || lifetime < timeout))
+                        timeout = lifetime;
+                break;
+        default:
+                /* ignore */
+                break;
+        }
+
+        *timeoutp = timeout;
+}
+
+static int n_dhcp4_client_probe_outgoing_append_options(NDhcp4ClientProbe *probe, NDhcp4Outgoing *outgoing) {
+        int r;
+
+        for (unsigned int i = 0; i <= UINT8_MAX; ++i) {
+                if (!probe->config->options[i])
+                        break;
+
+                r = n_dhcp4_outgoing_append(outgoing,
+                                            probe->config->options[i]->option,
+                                            probe->config->options[i]->data,
+                                            probe->config->options[i]->n_data);
+                if (r) {
+                        if (r == N_DHCP4_E_NO_SPACE)
+                                /* XXX */
+                                break;
+
+                        return r;
+                }
+        }
+
+        return 0;
+}
+
+static int n_dhcp4_client_probe_transition_deferred(NDhcp4ClientProbe *probe, uint64_t ns_now) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request = NULL;
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+                r = n_dhcp4_c_connection_listen(&probe->connection);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_c_connection_discover_new(&probe->connection, &request);
+                if (r)
+                        return r;
+
+                if (probe->config->requested_ip.s_addr != INADDR_ANY) {
+                        r = n_dhcp4_outgoing_append_requested_ip(request, probe->config->requested_ip);
+                        if (r)
+                                return r;
+                }
+
+                r = n_dhcp4_client_probe_outgoing_append_options(probe, request);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_c_connection_start_request(&probe->connection, request, ns_now);
+                if (r)
+                        return r;
+                else
+                        request = NULL; /* consumed */
+
+                probe->state = N_DHCP4_CLIENT_PROBE_STATE_SELECTING;
+                probe->ns_deferred = 0;
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                abort();
+                break;
+        }
+
+        return 0;
+}
+
+static int n_dhcp4_client_probe_transition_t1(NDhcp4ClientProbe *probe, uint64_t ns_now) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request = NULL;
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+                r = n_dhcp4_c_connection_renew_new(&probe->connection, &request);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_client_probe_outgoing_append_options(probe, request);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_c_connection_start_request(&probe->connection, request, ns_now);
+                if (r)
+                        return r;
+                else
+                        request = NULL; /* consumed */
+
+                probe->state = N_DHCP4_CLIENT_PROBE_STATE_RENEWING;
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                abort();
+                break;
+        }
+
+        return 0;
+}
+
+static int n_dhcp4_client_probe_transition_t2(NDhcp4ClientProbe *probe, uint64_t ns_now) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request = NULL;
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+                r = n_dhcp4_c_connection_rebind_new(&probe->connection, &request);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_client_probe_outgoing_append_options(probe, request);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_c_connection_start_request(&probe->connection, request, ns_now);
+                if (r)
+                        return r;
+                else
+                        request = NULL; /* consumed */
+
+                probe->state = N_DHCP4_CLIENT_PROBE_STATE_REBINDING;
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                abort();
+                break;
+        }
+
+        return 0;
+}
+
+static int n_dhcp4_client_probe_transition_lifetime(NDhcp4ClientProbe *probe) {
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+
+                /* XXX */
+
+                r = n_dhcp4_client_probe_raise(probe,
+                                               NULL,
+                                               N_DHCP4_CLIENT_EVENT_EXPIRED);
+                if (r)
+                        return r;
+
+                c_assert(probe->client->current_probe == probe);
+                probe->client->current_probe = NULL;
+
+                n_dhcp4_c_connection_close(&probe->connection);
+
+                probe->state = N_DHCP4_CLIENT_PROBE_STATE_EXPIRED;
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                abort();
+                break;
+        }
+
+        return 0;
+}
+
+static int n_dhcp4_client_probe_transition_offer(NDhcp4ClientProbe *probe, NDhcp4Incoming *message) {
+        _c_cleanup_(n_dhcp4_client_lease_unrefp) NDhcp4ClientLease *lease = NULL;
+        NDhcp4CEventNode *node;
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+
+                r = n_dhcp4_client_probe_raise(probe,
+                                               &node,
+                                               N_DHCP4_CLIENT_EVENT_OFFER);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_client_lease_new(&lease, message);
+                if (r)
+                        return r;
+
+                /* message consumed, do not fail */
+
+                n_dhcp4_client_lease_link(lease, probe);
+
+                node->event.offer.lease = n_dhcp4_client_lease_ref(lease);
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                /* ignore */
+                break;
+        }
+
+        return 0;
+}
+
+static int n_dhcp4_client_probe_transition_ack(NDhcp4ClientProbe *probe, NDhcp4Incoming *message) {
+        _c_cleanup_(n_dhcp4_client_lease_unrefp) NDhcp4ClientLease *lease = NULL;
+        NDhcp4CEventNode *node;
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+
+                r = n_dhcp4_client_probe_raise(probe,
+                                               &node,
+                                               N_DHCP4_CLIENT_EVENT_EXTENDED);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_client_lease_new(&lease, message);
+                if (r)
+                        return r;
+
+                /* message consumed, do not fail */
+
+                n_dhcp4_client_lease_link(lease, probe);
+
+                node->event.extended.lease = n_dhcp4_client_lease_ref(lease);
+                n_dhcp4_client_lease_unref(probe->current_lease);
+                probe->current_lease = n_dhcp4_client_lease_ref(lease);
+                probe->state = N_DHCP4_CLIENT_PROBE_STATE_BOUND;
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+
+                r = n_dhcp4_client_probe_raise(probe,
+                                               &node,
+                                               N_DHCP4_CLIENT_EVENT_GRANTED);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_client_lease_new(&lease, message);
+                if (r)
+                        return r;
+
+                /* message consumed, don to fail */
+
+                n_dhcp4_client_lease_link(lease, probe);
+
+                node->event.granted.lease = n_dhcp4_client_lease_ref(lease);
+                probe->current_lease = n_dhcp4_client_lease_ref(lease);
+                probe->state = N_DHCP4_CLIENT_PROBE_STATE_GRANTED;
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                /* ignore */
+                break;
+        }
+
+        return 0;
+}
+
+static int n_dhcp4_client_probe_transition_nak(NDhcp4ClientProbe *probe) {
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+
+                /* XXX */
+
+                r = n_dhcp4_client_probe_raise(probe,
+                                               NULL,
+                                               N_DHCP4_CLIENT_EVENT_RETRACTED);
+                if (r)
+                        return r;
+
+                probe->state = N_DHCP4_CLIENT_PROBE_STATE_INIT;
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                /* ignore */
+                break;
+        }
+
+        return 0;
+}
+
+int n_dhcp4_client_probe_transition_select(NDhcp4ClientProbe *probe, NDhcp4Incoming *offer, uint64_t ns_now) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request = NULL;
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+                r = n_dhcp4_c_connection_select_new(&probe->connection, &request, offer);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_client_probe_outgoing_append_options(probe, request);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_c_connection_start_request(&probe->connection, request, ns_now);
+                if (r)
+                        return r;
+                else
+                        request = NULL; /* consumed */
+
+                /* XXX: ignore other offers */
+
+                probe->state = N_DHCP4_CLIENT_PROBE_STATE_REQUESTING;
+
+                break;
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                /* ignore */
+                break;
+        }
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_probe_transition_accept() - XXX
+ */
+int n_dhcp4_client_probe_transition_accept(NDhcp4ClientProbe *probe, NDhcp4Incoming *ack) {
+        struct in_addr client = {};
+        struct in_addr server = {};
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+                n_dhcp4_incoming_get_yiaddr(ack, &client);
+
+                r = n_dhcp4_incoming_query_server_identifier(ack, &server);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_c_connection_connect(&probe->connection, &client, &server);
+                if (r)
+                        return r;
+
+                probe->state = N_DHCP4_CLIENT_PROBE_STATE_BOUND;
+
+                /* XXX: trigger timers */
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                /* ignore */
+                break;
+        }
+
+        return 0;
+}
+
+/**
+ * n_dhc4_client_probe_transition_decline() - XXX
+ */
+int n_dhcp4_client_probe_transition_decline(NDhcp4ClientProbe *probe, NDhcp4Incoming *offer, const char *error, uint64_t ns_now) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *request = NULL;
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+                r = n_dhcp4_c_connection_decline_new(&probe->connection, &request, offer, error);
+                if (r)
+                        return r;
+
+                r = n_dhcp4_c_connection_start_request(&probe->connection, request, ns_now);
+                if (r)
+                        return r;
+                else
+                        request = NULL; /* consumed */
+
+                /* XXX: what state to transition to? */
+
+                break;
+
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBOOTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_SELECTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REQUESTING:
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+        case N_DHCP4_CLIENT_PROBE_STATE_EXPIRED:
+        default:
+                /* ignore */
+                break;
+        }
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_probe_dispatch_timer() - XXX
+ */
+int n_dhcp4_client_probe_dispatch_timer(NDhcp4ClientProbe *probe, uint64_t ns_now) {
+        int r;
+
+        switch (probe->state) {
+        case N_DHCP4_CLIENT_PROBE_STATE_INIT:
+                if (ns_now >= probe->ns_deferred) {
+                        r = n_dhcp4_client_probe_transition_deferred(probe, ns_now);
+                        if (r)
+                                return r;
+                }
+
+                break;
+        case N_DHCP4_CLIENT_PROBE_STATE_GRANTED:
+                if (ns_now >= probe->current_lease->lifetime) {
+                        r = n_dhcp4_client_probe_transition_lifetime(probe);
+                        if (r)
+                                return r;
+                }
+
+                break;
+        case N_DHCP4_CLIENT_PROBE_STATE_BOUND:
+        case N_DHCP4_CLIENT_PROBE_STATE_RENEWING:
+        case N_DHCP4_CLIENT_PROBE_STATE_REBINDING:
+                if (ns_now >= probe->current_lease->lifetime) {
+                        r = n_dhcp4_client_probe_transition_lifetime(probe);
+                        if (r)
+                                return r;
+                } else if (ns_now >= probe->current_lease->t2) {
+                        r = n_dhcp4_client_probe_transition_t2(probe, ns_now);
+                        if (r)
+                                return r;
+                } else if (ns_now >= probe->current_lease->t1) {
+                        r = n_dhcp4_client_probe_transition_t1(probe, ns_now);
+                        if (r)
+                                return r;
+                }
+
+                break;
+        default:
+                /* ignore */
+                break;
+        }
+
+        r = n_dhcp4_c_connection_dispatch_timer(&probe->connection, ns_now);
+        if (r)
+                return r;
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_probe_dispatch_connection() - XXX
+ */
+int n_dhcp4_client_probe_dispatch_io(NDhcp4ClientProbe *probe, uint32_t events) {
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL;
+        uint8_t type;
+        int r;
+
+        r = n_dhcp4_c_connection_dispatch_io(&probe->connection, &message);
+        if (r) {
+                if (r == N_DHCP4_E_AGAIN)
+                        return 0;
+                else if (r == N_DHCP4_E_MALFORMED || r == N_DHCP4_E_UNEXPECTED) {
+                        /*
+                         * We fetched something from the sockets, which we
+                         * discarded. We don't know whether there is more data
+                         * to fetch, so we set the preempted flag to notify the
+                         * caller we want to be called again.
+                         */
+                        probe->client->preempted = true;
+                        return 0;
+                }
+
+                return r;
+        }
+
+        /*
+         * We fetched something from the sockets, which we will handle below.
+         * We don't know whether there is more data to fetch, so we set the
+         * preempted flag to notify the caller we want to be called again.
+         */
+        probe->client->preempted = true;
+
+        r = n_dhcp4_incoming_query_message_type(message, &type);
+        if (r == N_DHCP4_E_UNSET || r == N_DHCP4_E_MALFORMED)
+                /*
+                 * XXX: this can never happen as we already queried the message
+                 * type.
+                 */
+                return 0;
+
+        switch (type) {
+        case N_DHCP4_MESSAGE_OFFER:
+                r = n_dhcp4_client_probe_transition_offer(probe, message);
+                if (r)
+                        return r;
+                else
+                        message = NULL; /* consumed */
+                break;
+        case N_DHCP4_MESSAGE_ACK:
+                r = n_dhcp4_client_probe_transition_ack(probe, message);
+                if (r)
+                        return r;
+                else
+                        message = NULL; /* consumed */
+                break;
+        case N_DHCP4_MESSAGE_NAK:
+                r = n_dhcp4_client_probe_transition_nak(probe);
+                if (r)
+                        return r;
+                break;
+        default:
+                /*
+                 * We receiveda message type we do not support, simply discard
+                 * it.
+                 */
+                break;
+        }
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_probe_update_mtu() - XXX
+ */
+int n_dhcp4_client_probe_update_mtu(NDhcp4ClientProbe *probe, uint16_t mtu) {
+        return 0;
+}
diff --git a/shared/n-dhcp4/src/n-dhcp4-client.c b/shared/n-dhcp4/src/n-dhcp4-client.c
new file mode 100644
index 00000000..5f7794fb
--- /dev/null
+++ b/shared/n-dhcp4/src/n-dhcp4-client.c
@@ -0,0 +1,839 @@
+/*
+ * Client Side of the Dynamic Host Configuration Protocol for IPv4
+ *
+ * This implements the public API around the NDhcp4Client object. The client
+ * object is simply a context to track running probes. It manages pending
+ * events of all probes, as well as forwards the dispatching requests whenever
+ * the dispatcher is run.
+ */
+
+#include <assert.h>
+#include <c-list.h>
+#include <c-stdaux.h>
+#include <errno.h>
+#include <linux/if_ether.h>
+#include <linux/if_infiniband.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/epoll.h>
+#include <sys/time.h>
+#include <sys/timerfd.h>
+#include <time.h>
+#include <unistd.h>
+#include "n-dhcp4.h"
+#include "n-dhcp4-private.h"
+
+/**
+ * n_dhcp4_client_config_new() - allocate new client configuration
+ * @configp:                    output argument for new client config
+ *
+ * This creates a new client configuration object. Client configurations are
+ * unlinked objects that merely serve as collection of parameters. They do not
+ * perform validity checks.
+ *
+ * The new client configuration is fully owned by the caller. They are
+ * responsible to free the object if no longer needed.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_config_new(NDhcp4ClientConfig **configp) {
+        _c_cleanup_(n_dhcp4_client_config_freep) NDhcp4ClientConfig *config = NULL;
+
+        config = calloc(1, sizeof(*config));
+        if (!config)
+                return -ENOMEM;
+
+        *config = (NDhcp4ClientConfig)N_DHCP4_CLIENT_CONFIG_NULL(*config);
+
+        *configp = config;
+        config = NULL;
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_config_free() - destroy client configuration
+ * @config:                     client configuration to operate on, or NULL
+ *
+ * This destroys a client configuration and deallocates all its resources. If
+ * NULL is passed, this is a no-op.
+ *
+ * Return: NULL is returned.
+ */
+_c_public_ NDhcp4ClientConfig *n_dhcp4_client_config_free(NDhcp4ClientConfig *config) {
+        if (!config)
+                return NULL;
+
+        free(config->client_id);
+        free(config);
+
+        return NULL;
+}
+
+/**
+ * n_dhcp4_client_config_dup() - duplicate client configuration
+ * @config:                     client configuration to operate on
+ * @dupp:                       output argument for duplicate
+ *
+ * This duplicates the client configuration given as @config and returns it in
+ * @dupp to the caller.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int n_dhcp4_client_config_dup(NDhcp4ClientConfig *config, NDhcp4ClientConfig **dupp) {
+        _c_cleanup_(n_dhcp4_client_config_freep) NDhcp4ClientConfig *dup = NULL;
+        int r;
+
+        r = n_dhcp4_client_config_new(&dup);
+        if (r)
+                return r;
+
+        dup->ifindex = config->ifindex;
+        dup->transport = config->transport;
+        dup->request_broadcast = config->request_broadcast;
+        memcpy(dup->mac, config->mac, sizeof(dup->mac));
+        dup->n_mac = config->n_mac;
+        memcpy(dup->broadcast_mac, config->broadcast_mac, sizeof(dup->broadcast_mac));
+        dup->n_broadcast_mac = config->n_broadcast_mac;
+
+        r = n_dhcp4_client_config_set_client_id(dup,
+                                                config->client_id,
+                                                config->n_client_id);
+        if (r)
+                return r;
+
+        *dupp = dup;
+        dup = NULL;
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_config_set_ifindex() - set ifindex property
+ * @config:                     client configuration to operate on
+ * @ifindex:                    ifindex to set
+ *
+ * This sets the ifindex property of the client configuration. The ifindex
+ * specifies the network device that a DHCP client will run on.
+ */
+_c_public_ void n_dhcp4_client_config_set_ifindex(NDhcp4ClientConfig *config, int ifindex) {
+        config->ifindex = ifindex;
+}
+
+/**
+ * n_dhcp4_client_config_set_transport() - set transport property
+ * @config:                     client configuration to operate on
+ * @transport:                  transport to set
+ *
+ * This sets the transport property of the client configuration. The transport
+ * defines the hardware transport of the network device that a DHCP client
+ * runs on.
+ *
+ * This takes one of the N_DHCP4_TRANSPORT_* identifiers as argument.
+ */
+_c_public_ void n_dhcp4_client_config_set_transport(NDhcp4ClientConfig *config, unsigned int transport) {
+        config->transport = transport;
+}
+
+/**
+ * n_dhcp4_client_config_set_request_broadcast() - set request-broadcast property
+ * @config:                           configuration to operate on
+ * @request_broadcast:                value to set
+ *
+ * This sets the request_broadcast property of the given configuration object.
+ *
+ * The default is false. If set to true, a the server will be told to not unicast
+ * replies to the client's IP address before it has been configured, but broadcast
+ * to INADDR_ANY instead. In most cases, you do not want this.
+ *
+ * Background: OFFER and ACK messages from DHCP servers to clients are unicast
+ *             to the IP address handed out, even before the IP address has
+ *             been configured on the taregt interface. This usually works
+ *             because the correct destination hardware address is explicitly
+ *             set on the outgoing packets, rather than being resolved (which
+ *             would not work). However, some hardware does not accept incoming
+ *             IP packets destined for addresses they do not own, even if the
+ *             hardware address is correct. In this case, the server must
+ *             broadcast the replies in order for the client to receive them.
+ *             In general, unneccesary broadcasting is something one wants to
+ *             avoid, and some networks will not deliver broadcasts to the
+ *             client at all, in which case this flag must not be set.
+ */
+_c_public_ void n_dhcp4_client_config_set_request_broadcast(NDhcp4ClientConfig *config, bool request_broadcast) {
+        config->request_broadcast = request_broadcast;
+}
+
+/**
+ * n_dhcp4_client_config_set_mac() - set mac property
+ * @config:                     client configuration to operate on
+ * @mac:                        hardware address to set
+ * @n_mac:                      length of the hardware address
+ *
+ * This sets the mac property of the client configuration. It specifies the
+ * hardware address of the local interface that the DHCP client runs on.
+ *
+ * This function copies the specified hardware address into @config. Any
+ * hardware address is supported. It is up to the consumer of the client
+ * configuration to verify the validity of the hardware address.
+ *
+ * Note: This function may truncate the hardware address internally, but
+ *       retains the original length. The consumer of this configuration can
+ *       thus tell whether the data was truncated and will refuse it.
+ *       The internal buffer is big enough to hold any hardware address of all
+ *       supported transports. Thus, truncation only happens if you use
+ *       unsupported transports, and those will be rejected, anyway.
+ */
+_c_public_ void n_dhcp4_client_config_set_mac(NDhcp4ClientConfig *config, const uint8_t *mac, size_t n_mac) {
+        config->n_mac = n_mac;
+
+        if (n_mac > sizeof(config->mac))
+                n_mac = sizeof(config->mac);
+
+        memcpy(config->mac, mac, n_mac);
+}
+
+/**
+ * n_dhcp4_client_config_set_broadcast_mac() - set broadcast-mac property
+ * @config:                     client configuration to operate on
+ * @mac:                        hardware address to set
+ * @n_mac:                      length of the hardware address
+ *
+ * This sets the broadcast-mac property of the client configuration. It
+ * specifies the destination hardware address to use for broadcasts on the
+ * local interface that the DHCP client runs on.
+ *
+ * This function copies the specified hardware address into @config. Any
+ * hardware address is supported. It is up to the consumer of the client
+ * configuration to verify the validity of the hardware address.
+ *
+ * Note: This function may truncate the hardware address internally, but
+ *       retains the original length. The consumer of this configuration can
+ *       thus tell whether the data was truncated and will refuse it.
+ *       The internal buffer is big enough to hold any hardware address of all
+ *       supported transports. Thus, truncation only happens if you use
+ *       unsupported transports, and those will be rejected, anyway.
+ */
+_c_public_ void n_dhcp4_client_config_set_broadcast_mac(NDhcp4ClientConfig *config, const uint8_t *mac, size_t n_mac) {
+        config->n_broadcast_mac = n_mac;
+
+        if (n_mac > sizeof(config->mac))
+                n_mac = sizeof(config->mac);
+
+        memcpy(config->broadcast_mac, mac, n_mac);
+}
+
+/**
+ * n_dhcp4_client_config_set_client_id() - set client-id property
+ * @config:                     client configuration to operate on
+ * @id:                         client id
+ * @n_id:                       length of the client id in bytes
+ *
+ * This sets the client-id property of @config. It copies the entire client-id
+ * buffer into the configuration.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_config_set_client_id(NDhcp4ClientConfig *config, const uint8_t *id, size_t n_id) {
+        uint8_t *t;
+
+        t = malloc(n_id + 1);
+        if (!t)
+                return -ENOMEM;
+
+        free(config->client_id);
+        config->client_id = t;
+        config->n_client_id = n_id;
+
+        memcpy(config->client_id, id, n_id);
+        config->client_id[n_id] = 0; /* safety 0 for debugging */
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_c_event_node_new() - allocate new event
+ * @nodep:                      output argument for new event
+ *
+ * This allocates a new event node and returns it to the caller. The caller
+ * fully owns the event-node and is reposonsible to either link it somewhere,
+ * or release it.
+ *
+ * Event nodes can be linked on a client object, as well as optionally on a
+ * probe object. As long as an event-node is linked, it will be retrievable by
+ * the API user through n_dhcp4_client_pop_event(). Furthermore, destruction of
+ * the client, or probe respectively, will clean-up all pending events.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int n_dhcp4_c_event_node_new(NDhcp4CEventNode **nodep) {
+        NDhcp4CEventNode *node;
+
+        node = calloc(1, sizeof(*node));
+        if (!node)
+                return -ENOMEM;
+
+        *node = (NDhcp4CEventNode)N_DHCP4_C_EVENT_NODE_NULL(*node);
+
+        *nodep = node;
+        return 0;
+}
+
+/**
+ * n_dhcp4_c_event_node_free() - deallocate event
+ * @node:                       node to operate on, or NULL
+ *
+ * This deallocates the node given as @node. If the node is linked on a client
+ * or probe, it is unlinked automatically.
+ *
+ * If @probe is NULL, this is a no-op.
+ *
+ * Return: NULL is returned.
+ */
+NDhcp4CEventNode *n_dhcp4_c_event_node_free(NDhcp4CEventNode *node) {
+        if (!node)
+                return NULL;
+
+        switch (node->event.event) {
+        case N_DHCP4_CLIENT_EVENT_OFFER:
+                node->event.offer.lease = n_dhcp4_client_lease_unref(node->event.offer.lease);
+                break;
+        case N_DHCP4_CLIENT_EVENT_GRANTED:
+                node->event.granted.lease = n_dhcp4_client_lease_unref(node->event.granted.lease);
+                break;
+        case N_DHCP4_CLIENT_EVENT_EXTENDED:
+                node->event.extended.lease = n_dhcp4_client_lease_unref(node->event.extended.lease);
+                break;
+        default:
+                break;
+        }
+
+        c_list_unlink(&node->probe_link);
+        c_list_unlink(&node->client_link);
+        free(node);
+
+        return NULL;
+}
+
+/**
+ * n_dhcp4_client_new() - allocate new client
+ * @clientp:                    output argument for new client
+ * @config:                     configuration to use
+ *
+ * This allocates a new DHCP4 client object and returns it in @clientp to the
+ * caller. The caller then owns a single ref-count to the object and is
+ * responsible to drop it, when no longer needed.
+ *
+ * The configuration given as @config is used to initialize the client. The
+ * caller is free to destroy the configuration once this function returns.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_new(NDhcp4Client **clientp, NDhcp4ClientConfig *config) {
+        _c_cleanup_(n_dhcp4_client_unrefp) NDhcp4Client *client = NULL;
+        struct epoll_event ev = {
+                .events = EPOLLIN,
+        };
+        int r;
+
+        c_assert(clientp);
+
+        /* verify configuration */
+        {
+                if (config->ifindex < 1)
+                        return N_DHCP4_E_INVALID_IFINDEX;
+
+                switch (config->transport) {
+                case N_DHCP4_TRANSPORT_ETHERNET:
+                        if (config->n_mac != ETH_ALEN ||
+                            config->n_broadcast_mac != ETH_ALEN)
+                                return N_DHCP4_E_INVALID_ADDRESS;
+
+                        break;
+                case N_DHCP4_TRANSPORT_INFINIBAND:
+                        if (config->n_mac != INFINIBAND_ALEN ||
+                            config->n_broadcast_mac != INFINIBAND_ALEN)
+                                return N_DHCP4_E_INVALID_ADDRESS;
+
+                        break;
+                default:
+                        return N_DHCP4_E_INVALID_TRANSPORT;
+                }
+
+                if (config->n_client_id < 1)
+                        return N_DHCP4_E_INVALID_CLIENT_ID;
+        }
+
+        client = malloc(sizeof(*client));
+        if (!client)
+                return -ENOMEM;
+
+        *client = (NDhcp4Client)N_DHCP4_CLIENT_NULL(*client);
+
+        r = n_dhcp4_client_config_dup(config, &client->config);
+        if (r)
+                return r;
+
+        client->fd_epoll = epoll_create1(EPOLL_CLOEXEC);
+        if (client->fd_epoll < 0)
+                return -errno;
+
+        client->fd_timer = timerfd_create(CLOCK_BOOTTIME, TFD_CLOEXEC | TFD_NONBLOCK);
+        if (client->fd_timer < 0)
+                return -errno;
+
+        ev.data.u32 = N_DHCP4_CLIENT_EPOLL_TIMER;
+        r = epoll_ctl(client->fd_epoll, EPOLL_CTL_ADD, client->fd_timer, &ev);
+        if (r < 0)
+                return -errno;
+
+        *clientp = client;
+        client = NULL;
+        return 0;
+}
+
+static void n_dhcp4_client_free(NDhcp4Client *client) {
+        NDhcp4CEventNode *node, *t_node;
+
+        c_assert(!client->current_probe);
+
+        c_list_for_each_entry_safe(node, t_node, &client->event_list, client_link)
+                n_dhcp4_c_event_node_free(node);
+
+        if (client->fd_timer >= 0) {
+                epoll_ctl(client->fd_epoll, EPOLL_CTL_DEL, client->fd_timer, NULL);
+                close(client->fd_timer);
+        }
+
+        if (client->fd_epoll >= 0)
+                close(client->fd_epoll);
+
+        n_dhcp4_client_config_free(client->config);
+        free(client);
+}
+
+/**
+ * n_dhcp4_client_ref() - acquire client reference
+ * @client:                     client to operate on, or NULL
+ *
+ * This acquires a reference to the client given as @client. If @client is
+ * NULL, this function is a no-op.
+ *
+ * Return: @client is returned.
+ */
+_c_public_ NDhcp4Client *n_dhcp4_client_ref(NDhcp4Client *client) {
+        if (client)
+                ++client->n_refs;
+        return client;
+}
+
+/**
+ * n_dhcp4_client_unref() - release client reference
+ * @client:                     client to operate on, or NULL
+ *
+ * This releases a reference to the client given as @client. If @client is
+ * NULL, this is a no-op.
+ *
+ * Once the last reference is dropped, the client object will get destroyed and
+ * deallocated.
+ *
+ * Return: NULL is returned.
+ */
+_c_public_ NDhcp4Client *n_dhcp4_client_unref(NDhcp4Client *client) {
+        if (client && !--client->n_refs)
+                n_dhcp4_client_free(client);
+        return NULL;
+}
+
+/**
+ * n_dhcp4_client_raise() - raise event
+ * @client:                     client to operate on
+ * @nodep:                      output argument for new event, or NULL
+ * @event:                      event type to use
+ *
+ * This creates a new event-node on @client, setting the event-type to @event.
+ * The newly created event-node is returned to the caller in @nodep (unless
+ * @nodep is NULL).
+ *
+ * The event-node is automatically linked on @client.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int n_dhcp4_client_raise(NDhcp4Client *client, NDhcp4CEventNode **nodep, unsigned int event) {
+        NDhcp4CEventNode *node;
+        int r;
+
+        r = n_dhcp4_c_event_node_new(&node);
+        if (r)
+                return r;
+
+        node->event.event = event;
+        c_list_link_tail(&client->event_list, &node->client_link);
+
+        if (nodep)
+                *nodep = node;
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_arm_timer() - update timer
+ * @client:                     client to operate on
+ *
+ * This updates the timer on @client to fire on the next pending timeout. This
+ * must be called whenever a timeout on @client might have changed.
+ */
+void n_dhcp4_client_arm_timer(NDhcp4Client *client) {
+        uint64_t timeout = 0;
+        int r;
+
+        if (client->current_probe)
+                n_dhcp4_client_probe_get_timeout(client->current_probe, &timeout);
+
+        if (timeout != client->scheduled_timeout) {
+                r = timerfd_settime(client->fd_timer,
+                                    TFD_TIMER_ABSTIME,
+                                    &(struct itimerspec){
+                                        .it_value = {
+                                                .tv_sec = timeout / UINT64_C(1000000000),
+                                                .tv_nsec = timeout % UINT64_C(1000000000),
+                                        },
+                                    },
+                                    NULL);
+                c_assert(r >= 0);
+
+                client->scheduled_timeout = timeout;
+        }
+}
+
+/**
+ * n_dhcp4_client_get_fd() - retrieve event FD
+ * @client:                     client to operate on
+ * @fdp:                        output argument to store FD
+ *
+ * This retrieves the FD used by the client object given as @client. The FD is
+ * always valid, and returned in @fdp.
+ *
+ * The caller is expected to poll this FD for readable events and call
+ * n_dhcp4_client_dispatch() whenever the FD is readable.
+ */
+_c_public_ void n_dhcp4_client_get_fd(NDhcp4Client *client, int *fdp) {
+        *fdp = client->fd_epoll;
+}
+
+static int n_dhcp4_client_dispatch_timer(NDhcp4Client *client, struct epoll_event *event) {
+        uint64_t v, ns_now;
+        int r;
+
+        if (event->events & (EPOLLHUP | EPOLLERR)) {
+                /*
+                 * There is no way to handle either gracefully. If we ignored
+                 * them, we would busy-loop, so lets rather forward the error
+                 * to the caller.
+                 */
+                return -ENOTRECOVERABLE;
+        }
+
+        if (event->events & EPOLLIN) {
+                r = read(client->fd_timer, &v, sizeof(v));
+                if (r < 0) {
+                        if (errno == EAGAIN) {
+                                /*
+                                 * There are no more pending events, so nothing
+                                 * to be done. Return to the caller.
+                                 */
+                                return 0;
+                        }
+
+                        /*
+                         * Something failed. We use CLOCK_BOOTTIME/MONOTONIC,
+                         * so ECANCELED cannot happen. Hence, there is no error
+                         * that we could gracefully handle. Fail hard and let
+                         * the caller deal with it.
+                         */
+                        return -errno;
+                } else if (r != sizeof(v) || v == 0) {
+                        /*
+                         * Kernel guarantees 8-byte reads, and only to return
+                         * data if at least one timer triggered; fail hard if
+                         * it suddenly starts exposing unexpected behavior.
+                         */
+                        return -ENOTRECOVERABLE;
+                }
+
+                /*
+                 * Forward the timer-event to the active probe. Timers should
+                 * not fire if there is no probe running, but lets ignore them
+                 * for now, so probe-internals are not leaked to this generic
+                 * client dispatcher.
+                 */
+                if (client->current_probe) {
+                        /*
+                         * Read the current time *after* dispatching the timer,
+                         * to make sure we do not miss wakeups.
+                         */
+                        ns_now = n_dhcp4_gettime(CLOCK_BOOTTIME);
+
+                        r = n_dhcp4_client_probe_dispatch_timer(client->current_probe,
+                                                                ns_now);
+                        if (r)
+                                return r;
+                }
+        }
+
+        return 0;
+}
+
+static int n_dhcp4_client_dispatch_io(NDhcp4Client *client, struct epoll_event *event) {
+        int r;
+
+        if (client->current_probe)
+                r = n_dhcp4_client_probe_dispatch_io(client->current_probe,
+                                                     event->events);
+        else
+                return -ENOTRECOVERABLE;
+
+        return r;
+}
+
+/**
+ * n_dhcp4_client_dispatch() - dispatch client
+ * @client:                     client to operate on
+ *
+ * This dispatches pending operations on @client. It will read incoming
+ * messages, write pending data, and handle any timeouts.
+ *
+ * This function never blocks.
+ *
+ * If there are more events to dispatch, than would be reasonable to do in a
+ * single dispatch, this will return N_DHCP4_E_PREEMPTED. In this case the
+ * caller is expected to call into this function again when it is ready to
+ * dispatch more events.
+ * If your event loop is level-triggered (it very likely is), you can
+ * optionally ignore this return code and treat it as success.
+ *
+ * Return: 0 on success, negative error code on failure, N_DHCP4_E_PREEMPTED if
+ *         there is more data to dispatch.
+ */
+_c_public_ int n_dhcp4_client_dispatch(NDhcp4Client *client) {
+        struct epoll_event events[2];
+        int n, i, r = 0;
+
+        n = epoll_wait(client->fd_epoll, events, sizeof(events) / sizeof(*events), 0);
+        if (n < 0) {
+                /* Linux never returns EINTR if `timeout == 0'. */
+                return -errno;
+        }
+
+        client->preempted = false;
+
+        for (i = 0; i < n; ++i) {
+                switch (events[i].data.u32) {
+                case N_DHCP4_CLIENT_EPOLL_TIMER:
+                        r = n_dhcp4_client_dispatch_timer(client, events + i);
+                        break;
+                case N_DHCP4_CLIENT_EPOLL_IO:
+                        r = n_dhcp4_client_dispatch_io(client, events + i);
+                        break;
+                default:
+                        c_assert(0);
+                        r = 0;
+                        break;
+                }
+
+                if (r) {
+                        if (r == N_DHCP4_E_DOWN) {
+                                r = n_dhcp4_client_raise(client,
+                                                         NULL,
+                                                         N_DHCP4_CLIENT_EVENT_DOWN);
+                                if (r)
+                                        return r;
+
+                                /* continue normally */
+                        } else if (r) {
+                                c_assert(r < _N_DHCP4_E_INTERNAL);
+                                return r;
+                        }
+                }
+        }
+
+        n_dhcp4_client_arm_timer(client);
+
+        return client->preempted ? N_DHCP4_E_PREEMPTED : 0;
+}
+
+/**
+ * n_dhcp4_client_pop_event() - fetch pending event
+ * @client:                     client to operate on
+ * @eventp:                     output argument to store next event
+ *
+ * This fetches the next pending event from the event-queue and returns it to a
+ * caller. A pointer to the event is stored in @eventp. If there is no more
+ * event queued, NULL is returned.
+ *
+ * If a valid event is returned, it is accessible until the next call to this
+ * function, or the destruction of the context object (this might be either the
+ * client object or the probe object, pointed to by the event), whichever
+ * happens first.
+ * That is, the caller should not pin the returned event object, but copy
+ * required information into their own state tracking contexts.
+ *
+ * The possible events are:
+ * * N_DHCP4_CLIENT_EVENT_OFFER:     A lease offered from a server in response
+ *                                   to a probe. Several such offers may be
+ *                                   received until one of them is selected by
+ *                                   the caller. Only one lease may be selected.
+ *                                   The attached lease object may be queried
+ *                                   for information in order to decide which
+ *                                   lease to select, though the information is
+ *                                   not guaranteed to stay the same in the
+ *                                   final lease.
+ * * N_DHCP4_CLIENT_EVENT_GRANTED:   A selected lease was granted by the server.
+ *                                   The information in the attached lease
+ *                                   object should be used to configure the
+ *                                   client. Once the client has been
+ *                                   configured, the lease should be accepted.
+ * * N_DHCP4_CLIENT_EVENT_RETRACTED: A selected lease offer was retracted by the
+ *                                   server. This can happen in case the server
+ *                                   offers the same lease to several clients,
+ *                                   or the server discovers that the IP address
+ *                                   in the lease is already in use.
+ * * N_DHCP4_CLIENT_EVENT_EXTENDED:  An active lease is extended, if applicable
+ *                                   the kernel should be updated with the new
+ *                                   lifetime information for addresses and/or
+ *                                   routes.
+ * * N_DHCP4_CLIENT_EVENT_EXPIRED:   An active lease failed to be extended by
+ *                                   the end of its lifetime. The client should
+ *                                   immediately stop using the information
+ *                                   contained in the lease.
+ * * N_DHCP4_CLIENT_EVENT_DOWN:      The network interface was put down down.
+ *                                   The user is recommended to reestablish the
+ *                                   lease at the first opportunity when the
+ *                                   network comes back up. Note that this is
+ *                                   purely informational, the probe will keep
+ *                                   running, and if the network topology does
+ *                                   not change any lease we have will still be
+ *                                   valid.
+ * * N_DHCP4_CLIENT_EVENT_CANCELLED: The probe was cancelled. This can happen if
+ *                                   the client attempted several incompatible
+ *                                   probes in parallel, then the most recent
+ *                                   ones will be cancelled asynchronously.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_pop_event(NDhcp4Client *client, NDhcp4ClientEvent **eventp) {
+        NDhcp4CEventNode *node, *t_node;
+
+        c_list_for_each_entry_safe(node, t_node, &client->event_list, client_link) {
+                if (node->is_public) {
+                        n_dhcp4_c_event_node_free(node);
+                        continue;
+                }
+
+                node->is_public = true;
+                *eventp = &node->event;
+                return 0;
+        }
+
+        *eventp = NULL;
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_update_mtu() - update link mtu
+ * @client:                     client to operate on
+ * @mtu:                        new mtu
+ *
+ * This updates the link MTU used by the client object. By default, the minimum
+ * requirement given by the IP specification is assumed, which means 576
+ * bytes. The caller is advised to update this to the actual MTU used by the
+ * link layer.
+ *
+ * This value reflects the MTU of the link layer. That is, it is the maximum
+ * packet size that you can send on that link, excluding the link-header but
+ * including the IP-header. On ethernet-v2 this would be 1500.
+ *
+ * If unsure, it is safe to leave this unset. However, in this case a DHCP
+ * server will be required to omit information if it does not fit into the
+ * default MTU.
+ *
+ * Unless you keep the default MTU, you should update the MTU whenever the link
+ * MTU changes. That is, when it is increased *and* when it is decreased.
+ * However, you must be aware that decreasing the MTU on a link might cause
+ * temporary data loss.
+ *
+ * Background: Knowing the link MTU guarantees that we can possibly transmit
+ *             packets bigger than the IP minimum (i.e., 576 bytes). However,
+ *             it does not guarantee that a possible target supports parsing
+ *             packets bigger than the IP minimum. Hence, the MTU is used by a
+ *             client to send a hint to a server that it can receive replies
+ *             bigger than the minimum. As such, a server can reply with more
+ *             information than otherwise possible.
+ *             Since this DHCP client does not support fragmented packets, we
+ *             simply set the allowed packet-size to the local link MTU.
+ *             Note that DHCP relays might cause DHCP packets to be routed.
+ *             However, such relays are required to always reassemble any
+ *             fragments they receive into full DHCP packets, before they
+ *             forward them either way. This guarantees that incoming packets
+ *             are never fragmented, unless they exceed the local link MTU
+ *             (this would otherwise not neccessarily be true, if some other
+ *             part of the routed network had a lower MTU).
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_update_mtu(NDhcp4Client *client, uint16_t mtu) {
+        int r;
+
+        if (mtu == client->mtu)
+                return 0;
+
+        if (client->current_probe) {
+                r = n_dhcp4_client_probe_update_mtu(client->current_probe, mtu);
+                if (r)
+                        return r;
+        }
+
+        client->mtu = mtu;
+        return 0;
+}
+
+/**
+ * n_dhcp4_client_probe() - create a new probe
+ * @client:                     client to operate on
+ * @probep:                     output argument to store new probe
+ * @config:                     probe configuration to use
+ *
+ * This creates a new probe on @client. Probes represent DHCP requests and
+ * track the state over the entire lifetime of a lease. Once a probe is created
+ * it will start looking for DHCP servers, request a lease from them, and renew
+ * the lease continously whenever it expires. Furthermore, if a lease cannot be
+ * renewed, a new lease will be requested.
+ *
+ * The API allows for many probes to be run at the same time. However, the DHCP
+ * specification forbids many of those cases (e.g., you must not reuse a client
+ * id, otherwise it will be impossible to track who to forward received packets
+ * to). Hence, so far only a single probe can run at a time. If you create a
+ * new probe, all older probes that conflict with that probe will be canceled
+ * (their state machine is halted and a N_DHCP4_CLIENT_EVENT_CANCELLED event is
+ * raised.
+ * This might change in the future, though. There might be cases where multiple
+ * probes can be run in parallel (e.g., with different client-ids, or an INFORM
+ * in parallel to a REQUEST, ...).
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+_c_public_ int n_dhcp4_client_probe(NDhcp4Client *client,
+                                  NDhcp4ClientProbe **probep,
+                                  NDhcp4ClientProbeConfig *config) {
+        _c_cleanup_(n_dhcp4_client_probe_freep) NDhcp4ClientProbe *probe = NULL;
+        uint64_t ns_now;
+        int r;
+
+        ns_now = n_dhcp4_gettime(CLOCK_BOOTTIME);
+
+        r = n_dhcp4_client_probe_new(&probe, config, client, ns_now);
+        if (r)
+                return r;
+
+        n_dhcp4_client_arm_timer(client);
+
+        *probep = probe;
+        probe = NULL;
+        return 0;
+}
diff --git a/shared/n-dhcp4/src/n-dhcp4-incoming.c b/shared/n-dhcp4/src/n-dhcp4-incoming.c
new file mode 100644
index 00000000..255da458
--- /dev/null
+++ b/shared/n-dhcp4/src/n-dhcp4-incoming.c
@@ -0,0 +1,431 @@
+/*
+ * DHCPv4 Incoming Messages
+ *
+ * This file implements the message parser object for incoming DHCP4 messages.
+ * It takes a linear data blob as input, and provides accessors for the message
+ * content.
+ *
+ * This wrapper mainly deals with the OPTIONs array. That is, in hides the
+ * different overload-sections the DHCP4 spec defines, it concatenates
+ * duplicate option fields (as described by the spec), and provides a
+ * consistent view to the caller.
+ *
+ * Internally, for every incoming message we linearize its OPTIONs. This means,
+ * we create a copy of the contents, and merge all duplicate options into a
+ * single option entry. We then provide accessors to the caller to easily get
+ * O(1) access to individual fields.
+ */
+
+#include <assert.h>
+#include <c-stdaux.h>
+#include <endian.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <netinet/ip.h>
+#include <netinet/udp.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include "n-dhcp4.h"
+#include "n-dhcp4-private.h"
+
+static void n_dhcp4_incoming_prefetch(NDhcp4Incoming *incoming, size_t *offset, uint8_t option, const uint8_t *raw, size_t n_raw) {
+        uint8_t o, l;
+        size_t pos;
+
+        for (pos = 0; pos < n_raw; ) {
+                o = raw[pos++];
+                if (o == N_DHCP4_OPTION_PAD)
+                        continue;
+                if (o == N_DHCP4_OPTION_END)
+                        return;
+
+                /* bail out if no remaining space for length field */
+                if (pos >= n_raw)
+                        return;
+
+                /* bail out if length exceeds the available space */
+                l = raw[pos++];
+                if (l > n_raw || pos > n_raw - l)
+                        return;
+
+                /* prefetch content if it matches @option */
+                if (o == option) {
+                        memcpy((uint8_t *)&incoming->message + *offset, raw + pos, l);
+                        *offset += l;
+                }
+
+                pos += l;
+        }
+}
+
+static void n_dhcp4_incoming_merge(NDhcp4Incoming *incoming, size_t *offset, uint8_t overload, uint8_t option) {
+        uint8_t *m = (uint8_t *)&incoming->message;
+        size_t pos;
+
+        /*
+         * Prefetch all options matching @option from the 3 sections,
+         * concatenating their content. Remember the offset and size of the
+         * option in our message state.
+         */
+
+        pos = *offset;
+
+        /* prefetch option from OPTIONS */
+        n_dhcp4_incoming_prefetch(incoming, offset, option,
+                                  m + offsetof(NDhcp4Message, options),
+                                  incoming->n_message - offsetof(NDhcp4Message, options));
+
+        /* prefetch option from FILE */
+        if (overload & N_DHCP4_OVERLOAD_FILE)
+                n_dhcp4_incoming_prefetch(incoming, offset, option,
+                                          m + offsetof(NDhcp4Message, file),
+                                          sizeof(incoming->message.file));
+
+        /* prefetch option from SNAME */
+        if (overload & N_DHCP4_OVERLOAD_SNAME)
+                n_dhcp4_incoming_prefetch(incoming, offset, option,
+                                          m + offsetof(NDhcp4Message, sname),
+                                          sizeof(incoming->message.sname));
+
+        incoming->options[option].value = m + pos;
+        incoming->options[option].size = *offset - pos;
+}
+
+static void n_dhcp4_incoming_linearize(NDhcp4Incoming *incoming) {
+        uint8_t *m, o, l, overload;
+        size_t i, pos, end, offset;
+
+        /*
+         * Linearize all OPTIONs of the incoming message. We know that
+         * @incoming->message is preallocated to be big enough to hold the
+         * entire linearized message _trailing_ the original copy. All we have
+         * to do is walk the raw message in @incoming->message and for each
+         * option we find, copy it into the trailing space, concatenating all
+         * instances we find.
+         *
+         * Before we can copy the individual options, we must scan for the
+         * OVERLOAD option. This is required so our prefetcher knows which data
+         * arrays to scan for prefetching.
+         *
+         * So far, we require the OVERLOAD option to be present in the
+         * options-array (which is obvious and a given). However, if the option
+         * occurs multiple times outside of the options-array (i.e., SNAME or
+         * FILE), we silently ignore them. The specification does not allow
+         * multiple OVERLOAD options, anyway. Hence, this behavior only defines
+         * what we do when we see broken implementations, and we currently seem
+         * to support all styles we saw in the wild so far.
+         */
+
+        m = (uint8_t *)&incoming->message;
+        offset = incoming->n_message;
+
+        n_dhcp4_incoming_merge(incoming, &offset, 0, N_DHCP4_OPTION_OVERLOAD);
+        if (incoming->options[N_DHCP4_OPTION_OVERLOAD].size >= 1)
+                overload = *incoming->options[N_DHCP4_OPTION_OVERLOAD].value;
+        else
+                overload = 0;
+
+        for (i = 0; i < 3; ++i) {
+                if (i == 0) { /* walk OPTIONS */
+                        pos = offsetof(NDhcp4Message, options);
+                        end = incoming->n_message;
+                } else if (i == 1) { /* walk FILE */
+                        if (!(overload & N_DHCP4_OVERLOAD_FILE))
+                                continue;
+
+                        pos = offsetof(NDhcp4Message, file);
+                        end = pos + sizeof(incoming->message.file);
+                } else { /* walk SNAME */
+                        if (!(overload & N_DHCP4_OVERLOAD_SNAME))
+                                continue;
+
+                        pos = offsetof(NDhcp4Message, sname);
+                        end = pos + sizeof(incoming->message.sname);
+                }
+
+                while (pos < end) {
+                        o = m[pos++];
+                        if (o == N_DHCP4_OPTION_PAD)
+                                continue;
+                        if (o == N_DHCP4_OPTION_END)
+                                break;
+                        if (pos >= end)
+                                break;
+
+                        l = m[pos++];
+                        if (l > end || pos > end - l)
+                                break;
+
+                        if (!incoming->options[o].value)
+                                n_dhcp4_incoming_merge(incoming, &offset, overload, o);
+
+                        pos += l;
+                }
+        }
+}
+
+/**
+ * n_dhcp4_incoming_new() - Allocate new incoming message object
+ * @incomingp:          output argument for new object
+ * @raw:                raw message blob
+ * @n_raw:              length of the raw message blob
+ *
+ * This function allocates a new incoming-message object to wrap a received
+ * message blob. It performs basic verification of the message length and
+ * header, and then linearizes the DHCP4 options.
+ *
+ * The incoming-message object mainly provides accessors for the option-array.
+ * It handles all the different quirks around parsing and concatenating the
+ * options array, and provides the assembled data to the caller. It does not,
+ * however, in any way interpret the data of the individual options. This is up
+ * to the caller to do.
+ *
+ * Return: 0 on success, negative error code on failure, N_DHCP4_E_MALFORMED if
+ *         the message is not a valid DHCP4 message.
+ */
+int n_dhcp4_incoming_new(NDhcp4Incoming **incomingp, const void *raw, size_t n_raw) {
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *incoming = NULL;
+        size_t size;
+
+        if (n_raw < sizeof(NDhcp4Message) || n_raw > UINT16_MAX)
+                return N_DHCP4_E_MALFORMED;
+
+        /*
+         * Allocate enough space for book-keeping, a copy of @raw and trailing
+         * space for linearized options. The trailing space must be big enough
+         * to hold the entire options array unmodified (linearizing can only
+         * make it smaller). Hence, just allocate enough space to hold the raw
+         * message without the header.
+         */
+        size = sizeof(*incoming) + n_raw - sizeof(NDhcp4Message);
+        size += n_raw - sizeof(NDhcp4Header);
+
+        incoming = calloc(1, size);
+        if (!incoming)
+                return -ENOMEM;
+
+        *incoming = (NDhcp4Incoming)N_DHCP4_INCOMING_NULL(*incoming);
+        incoming->n_message = n_raw;
+        memcpy(&incoming->message, raw, n_raw);
+
+        if (incoming->message.magic != htobe32(N_DHCP4_MESSAGE_MAGIC))
+                return N_DHCP4_E_MALFORMED;
+
+        /* linearize options */
+        n_dhcp4_incoming_linearize(incoming);
+
+        *incomingp = incoming;
+        incoming = NULL;
+        return 0;
+}
+
+/**
+ * n_dhcp4_incoming_free() - Deallocate message object
+ * @incoming:           object to operate on, or NULL
+ *
+ * This deallocates and frees the given incoming-message object. If NULL is
+ * passed, this is a no-op.
+ *
+ * Return: NULL is returned.
+ */
+NDhcp4Incoming *n_dhcp4_incoming_free(NDhcp4Incoming *incoming) {
+        if (!incoming)
+                return NULL;
+
+        free(incoming);
+
+        return NULL;
+}
+
+/**
+ * n_dhcp4_incoming_get_header() - Return message header
+ * @incoming:           message to operate on
+ *
+ * This returns a pointer to the message header. Note that modifications to
+ * this header are permanent and will affect the original message.
+ *
+ * Return: A pointer to the message header is returned.
+ */
+NDhcp4Header *n_dhcp4_incoming_get_header(NDhcp4Incoming *incoming) {
+        return &incoming->message.header;
+}
+
+/**
+ * n_dhcp4_incoming_get_raw() - Get access to the raw original message
+ * @incoming:           message to operate on
+ * @rawp:               output argument for the raw blob, or NULL
+ *
+ * This hands out a pointer to the raw message blob to the caller. This will
+ * point to the original message content, rather than the linearized version.
+ *
+ * Note that if the caller queried the contents of the message before, any
+ * modifications done by the caller will not affect the original message. It
+ * only affects the linearized content (which is a duplicate trailing the
+ * original message). However, modifications to the message header *DO* also
+ * appear in the original, since the message header is not duplicated.
+ *
+ * In either case, it is better to never modify the message, if you intend to
+ * forward it further.
+ *
+ * Return: The length of the raw message blob is returned.
+ */
+size_t n_dhcp4_incoming_get_raw(NDhcp4Incoming *incoming, const void **rawp) {
+        if (rawp)
+                *rawp = &incoming->message;
+        return incoming->n_message;
+}
+
+/**
+ * n_dhcp4_incoming_query() - Query the contents of a specific option
+ * @incoming:           message to query
+ * @option:             option to look for
+ * @datap:              output argument for the option-data, or NULL
+ * @n_datap:            output argument for the length of the option, or NULL
+ *
+ * This returns a pointer to the requested option blob in the message. It
+ * points to a linearized version of all respective option-fields of the same
+ * type. Hence, the caller is not required to deal with multiple occurrences of
+ * the same option.
+ *
+ * If an option was not present in the incoming message, N_DHCP4_E_UNSET is
+ * returned. Note that this is different from an empty option! And empty option
+ * will return a valid pointer and size 0.
+ *
+ * Note that the pointer to the option-blob does not point to the original
+ * message, but a duplicated version. Modifications to the blob will not be
+ * reflected in the original message, but they will be permanent regarding
+ * further queries through this function.
+ *
+ * Note that the original message alignment might no longer be reflected in the
+ * returned blob. You must not alias the content of the blob, but always copy
+ * it out, or consume piecemeal.
+ *
+ * This function runs in O(1).
+ *
+ * Return: 0 on success, negative error code on failure, N_DHCP4_E_UNSET if the
+ *         option was not found,
+ */
+int n_dhcp4_incoming_query(NDhcp4Incoming *incoming, uint8_t option, uint8_t **datap, size_t *n_datap) {
+        if (!incoming->options[option].value)
+                return N_DHCP4_E_UNSET;
+
+        if (datap)
+                *datap = incoming->options[option].value;
+        if (n_datap)
+                *n_datap = incoming->options[option].size;
+        return 0;
+}
+
+static int n_dhcp4_incoming_query_u8(NDhcp4Incoming *message, uint8_t option, uint8_t *u8p) {
+        uint8_t *data;
+        size_t n_data;
+        int r;
+
+        r = n_dhcp4_incoming_query(message, option, &data, &n_data);
+        if (r)
+                return r;
+        else if (n_data != sizeof(*data))
+                return N_DHCP4_E_MALFORMED;
+
+        *u8p = *data;
+        return 0;
+}
+
+static int n_dhcp4_incoming_query_u16(NDhcp4Incoming *message, uint8_t option, uint16_t *u16p) {
+        uint8_t *data;
+        size_t n_data;
+        uint16_t be16;
+        int r;
+
+        r = n_dhcp4_incoming_query(message, option, &data, &n_data);
+        if (r)
+                return r;
+        else if (n_data != sizeof(be16))
+                return N_DHCP4_E_MALFORMED;
+
+        memcpy(&be16, data, sizeof(be16));
+
+        *u16p = ntohs(be16);
+        return 0;
+}
+
+static int n_dhcp4_incoming_query_u32(NDhcp4Incoming *message, uint8_t option, uint32_t *u32p) {
+        uint8_t *data;
+        size_t n_data;
+        uint32_t be32;
+        int r;
+
+        r = n_dhcp4_incoming_query(message, option, &data, &n_data);
+        if (r)
+                return r;
+        else if (n_data != sizeof(be32))
+                return N_DHCP4_E_MALFORMED;
+
+        memcpy(&be32, data, sizeof(be32));
+
+        if (be32 == (uint32_t)-1)
+                *u32p = 0;
+        else
+                *u32p = ntohl(be32);
+        return 0;
+}
+
+static int n_dhcp4_incoming_query_in_addr(NDhcp4Incoming *message, uint8_t option, struct in_addr *addrp) {
+        uint8_t *data;
+        size_t n_data;
+        uint32_t be32;
+        int r;
+
+        r = n_dhcp4_incoming_query(message, option, &data, &n_data);
+        if (r)
+                return r;
+        else if (n_data != sizeof(be32))
+                return N_DHCP4_E_MALFORMED;
+
+        memcpy(&be32, data, sizeof(be32));
+
+        addrp->s_addr = be32;
+        return 0;
+}
+
+int n_dhcp4_incoming_query_message_type(NDhcp4Incoming *message, uint8_t *typep) {
+        return n_dhcp4_incoming_query_u8(message, N_DHCP4_OPTION_MESSAGE_TYPE, typep);
+}
+
+int n_dhcp4_incoming_query_lifetime(NDhcp4Incoming *message, uint32_t *lifetimep) {
+        return n_dhcp4_incoming_query_u32(message, N_DHCP4_OPTION_IP_ADDRESS_LEASE_TIME, lifetimep);
+}
+
+int n_dhcp4_incoming_query_t2(NDhcp4Incoming *message, uint32_t *t2p) {
+        return n_dhcp4_incoming_query_u32(message, N_DHCP4_OPTION_REBINDING_T2_TIME, t2p);
+}
+
+int n_dhcp4_incoming_query_t1(NDhcp4Incoming *message, uint32_t *t1p) {
+        return n_dhcp4_incoming_query_u32(message, N_DHCP4_OPTION_RENEWAL_T1_TIME, t1p);
+}
+
+int n_dhcp4_incoming_query_server_identifier(NDhcp4Incoming *message, struct in_addr *idp) {
+        return n_dhcp4_incoming_query_in_addr(message, N_DHCP4_OPTION_SERVER_IDENTIFIER, idp);
+}
+
+int n_dhcp4_incoming_query_max_message_size(NDhcp4Incoming *message, uint16_t *max_message_sizep) {
+        return n_dhcp4_incoming_query_u16(message, N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE, max_message_sizep);
+}
+
+int n_dhcp4_incoming_query_requested_ip(NDhcp4Incoming *message, struct in_addr *requested_ipp) {
+        return n_dhcp4_incoming_query_in_addr(message, N_DHCP4_OPTION_REQUESTED_IP_ADDRESS, requested_ipp);
+}
+
+void n_dhcp4_incoming_get_xid(NDhcp4Incoming *message, uint32_t *xidp) {
+        NDhcp4Header *header = n_dhcp4_incoming_get_header(message);
+
+        *xidp = header->xid;
+}
+
+void n_dhcp4_incoming_get_yiaddr(NDhcp4Incoming *message, struct in_addr *yiaddr) {
+        NDhcp4Header *header = n_dhcp4_incoming_get_header(message);
+
+        yiaddr->s_addr = header->yiaddr;
+}
diff --git a/shared/n-dhcp4/src/n-dhcp4-outgoing.c b/shared/n-dhcp4/src/n-dhcp4-outgoing.c
new file mode 100644
index 00000000..c44b5880
--- /dev/null
+++ b/shared/n-dhcp4/src/n-dhcp4-outgoing.c
@@ -0,0 +1,372 @@
+/*
+ * DHCPv4 Outgoing Messages
+ *
+ * XXX
+ */
+
+#include <assert.h>
+#include <c-stdaux.h>
+#include <endian.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <netinet/ip.h>
+#include <netinet/udp.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include "n-dhcp4.h"
+#include "n-dhcp4-private.h"
+
+/**
+ * N_DHCP4_OUTGOING_MAX_PHDR - maximum protocol header size
+ *
+ * All DHCP4 messages-limits specify the size of the entire packet including
+ * the protocol layer (i.e., including the IP headers and UDP headers). To
+ * calculate the size we have remaining for the actual DHCP message, we need to
+ * substract the maximum possible header-length the linux-kernel might prepend
+ * to our messages. This turns out to be the maximum IP-header size (including
+ * optional IP headers, hence 60 bytes) plus the UDP header size (i.e., 8
+ * bytes).
+ */
+#define N_DHCP4_OUTGOING_MAX_PHDR (N_DHCP4_NETWORK_IP_MAXIMUM_HEADER_SIZE + sizeof(struct udphdr))
+
+/**
+ * n_dhcp4_outgoing_new() - Allocate new outgoing message
+ * @outgoingp:          output argument to return allocate object through
+ * @max_size:           maximum transmission size to use
+ * @overload:           select sections to overload
+ *
+ * This allocates a new outgoing message and returns it to the caller. The
+ * caller can then append data to it and send it over the wire.
+ *
+ * The @max_size parameter specifies the transport-layer MTU to consider. If 0,
+ * N_DHCP4_NETWORK_IP_MINIMUM_MAX_SIZE is used. Note that this argument
+ * specifies the maximum packet size *INCLUDING* the IP-headers and UDP-header.
+ * Internally, the allocator makes sure to never create packets bigger than the
+ * specified MTU. The append functions will return an error, if the packet size
+ * would exceed the MTU.
+ * If you use a full UDP stack that supports packet fragmentation, you can
+ * specify the maximum packet size here (e.g., UINT16_MAX).
+ *
+ * Return: 0 on success, error code on failure.
+ */
+int n_dhcp4_outgoing_new(NDhcp4Outgoing **outgoingp, size_t max_size, uint8_t overload) {
+        _c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *outgoing = NULL;
+
+        c_assert(!(overload & ~(N_DHCP4_OVERLOAD_FILE | N_DHCP4_OVERLOAD_SNAME)));
+
+        /*
+         * Make sure the minimum limit is bigger than the maximum protocol
+         * header plus the DHCP-message-header plus a single OPTION_END byte.
+         */
+        static_assert(N_DHCP4_NETWORK_IP_MINIMUM_MAX_SIZE >= N_DHCP4_OUTGOING_MAX_PHDR +
+                                                             sizeof(NDhcp4Message) + 1,
+                      "Invalid minimum IP packet limit");
+
+        outgoing = calloc(1, sizeof(*outgoing));
+        if (!outgoing)
+                return -ENOMEM;
+
+        *outgoing = (NDhcp4Outgoing)N_DHCP4_OUTGOING_NULL(*outgoing);
+        outgoing->n_message = N_DHCP4_NETWORK_IP_MINIMUM_MAX_SIZE - N_DHCP4_OUTGOING_MAX_PHDR;
+        outgoing->i_message = offsetof(NDhcp4Message, options);
+        outgoing->max_size = outgoing->n_message;
+        outgoing->overload = overload;
+
+        if (max_size > N_DHCP4_NETWORK_IP_MINIMUM_MAX_SIZE)
+                outgoing->max_size = max_size - N_DHCP4_OUTGOING_MAX_PHDR;
+
+        outgoing->message = calloc(1, outgoing->n_message);
+        if (!outgoing->message)
+                return -ENOMEM;
+
+        outgoing->message->magic = htonl(N_DHCP4_MESSAGE_MAGIC);
+        outgoing->message->options[0] = N_DHCP4_OPTION_END;
+
+        *outgoingp = outgoing;
+        outgoing = NULL;
+        return 0;
+}
+
+/**
+ * n_dhcp4_outgoing_free() - Deallocate outgoing message
+ * @outgoing:           message to deallocate, or NULL
+ *
+ * This is the opposite to n_dhcp4_outgoing_new(). It deallocates and frees the
+ * passed object. If @outgoing is NULL, this is a no-op.
+ *
+ * Return: NULL is returned.
+ */
+NDhcp4Outgoing *n_dhcp4_outgoing_free(NDhcp4Outgoing *outgoing) {
+        if (!outgoing)
+                return NULL;
+
+        free(outgoing->message);
+        free(outgoing);
+
+        return NULL;
+}
+
+/**
+ * n_dhcp4_outgoing_get_header() - Get pointer to the message header
+ * @outgoing:           message to operate on
+ *
+ * This returns a pointer to the DHCP4 message header to the caller. The caller
+ * can use this to fill-in the header-fields. Note that all fields are
+ * initialized to their default values. Hence, you only need to override the
+ * fields where the default is not sufficient.
+ *
+ * Return: A pointer to the message header is returned.
+ */
+NDhcp4Header *n_dhcp4_outgoing_get_header(NDhcp4Outgoing *outgoing) {
+        return &outgoing->message->header;
+}
+
+/**
+ * n_dhcp4_outgoing_get_raw() - Get the raw message blob
+ * @outgoing:           message to operat on
+ * @rawp:               output argument for the message-blob
+ *
+ * This function gives the caller access to the raw message-blob. That is, once
+ * message-marshaling is complete, use this to get the raw blob for sending.
+ * Note that this blob is only valid as long as you no longer append any
+ * further options to the message, nor modify it in any other way.
+ *
+ * Return: The size of the raw message blob is returned.
+ */
+size_t n_dhcp4_outgoing_get_raw(NDhcp4Outgoing *outgoing, const void **rawp) {
+        if (rawp)
+                *rawp = outgoing->message;
+
+        /*
+         * Return the DHCP message until the END option, excluding any
+         * trailing padding. We overallocate during append, so the
+         * allocated message might be bigger than what we want to
+         * send on the wire.
+         */
+        return outgoing->i_message + 1;
+}
+
+static void n_dhcp4_outgoing_append_option(NDhcp4Outgoing *outgoing,
+                                           uint8_t option,
+                                           const void *data,
+                                           uint8_t n_data) {
+        uint8_t *blob = (void *)outgoing->message;
+
+        blob[outgoing->i_message++] = option;
+        blob[outgoing->i_message++] = n_data;
+        memcpy(blob + outgoing->i_message, data, n_data);
+        outgoing->i_message += n_data;
+        blob[outgoing->i_message] = N_DHCP4_OPTION_END;
+}
+
+/**
+ * n_dhcp4_outgoing_append() - Append option to outgoing message
+ * @outgoing:           message to operate on
+ * @option:             option code to append
+ * @data:               data to append in the option
+ * @n_data:             length of the data blob
+ *
+ * This appends another option to the given outgoing message. The data is taken
+ * verbatim and copied into the message. Note that no validation is done. If
+ * you provide an option multiple times, it will be added multiple times (spec
+ * then requires them to be interpreted as concatenated option, in case the
+ * option is marked as such).
+ *
+ * The size of a message is limited, based on the restriction passed to the
+ * outgoing-message constructor. If there is not enough free space to copy in
+ * the new option, N_DHCP4_E_NO_SPACE is returned.
+ *
+ * The order in which you append options might matter to some implementations.
+ * For example, the message-type is often expected to be the first option. We
+ * do not place such restrictions, but for compatibility with external
+ * implementations, you should follow these recommendations.
+ * Furthermore, we do not implement any kind of smart allocators. That is, all
+ * options are simply appended when you call this. But due to the overloading
+ * feature, fragmentation might matter. Hence, if you use overloading, overly
+ * big options might cause padding, and as such waste space.
+ *
+ * Return: 0 on success, negative error code on failure, N_DHCP4_E_NO_SPACE
+ *         when there is not sufficient free space in the message.
+ */
+int n_dhcp4_outgoing_append(NDhcp4Outgoing *outgoing,
+                            uint8_t option,
+                            const void *data,
+                            uint8_t n_data) {
+        NDhcp4Message *m;
+        uint8_t overload;
+        size_t rem, n;
+
+        c_assert(option != N_DHCP4_OPTION_PAD);
+        c_assert(option != N_DHCP4_OPTION_END);
+        c_assert(option != N_DHCP4_OPTION_OVERLOAD);
+
+        /*
+         * If the iterator is on the OPTIONs field, try appending the new blob.
+         * We need 2 header-bytes plus @n_data bytes. Additionally, we always
+         * reserve 3 trailing bytes for a possible OVERLOAD option, and 1 byte
+         * for the END marker.
+         */
+        if (outgoing->i_message >= offsetof(NDhcp4Message, options)) {
+                rem = outgoing->n_message - outgoing->i_message;
+
+                /* try fitting into remaining OPTIONs space */
+                if (rem >= n_data + 2U + 3U + 1U) {
+                        n_dhcp4_outgoing_append_option(outgoing, option, data, n_data);
+                        return 0;
+                }
+
+                /* try fitting into allowed OPTIONs space */
+                if (outgoing->max_size - outgoing->i_message >= n_data + 2U + 3U + 1U) {
+                        /* try over-allocation to reduce allocation pressure */
+                        n = outgoing->n_message + n_data + 128;
+                        if (n > outgoing->max_size)
+                                n = outgoing->max_size;
+                        m = realloc(outgoing->message, n);
+                        if (!m)
+                                return -ENOMEM;
+
+                        memset((void *)m + outgoing->i_message, 0, n - outgoing->i_message);
+                        outgoing->message = m;
+                        outgoing->n_message = n;
+                        n_dhcp4_outgoing_append_option(outgoing, option, data, n_data);
+                        return 0;
+                }
+
+                /* not enough remaining space, try OVERLOAD */
+                if (!outgoing->overload)
+                        return N_DHCP4_E_NO_SPACE;
+
+                /*
+                 * We ran out of space in the OPTIONs array, but overloading
+                 * was enabled. This means, we can insert an OVERLOAD option
+                 * and then use SNAME/FILE to store more options.
+                 * Note that the three different sections cannot overlap and
+                 * all must have an END marker. So as soon as we add the
+                 * OVERLOAD option, we must make sure the other sections have
+                 * the valid END marker. From then on, our *_append_option()
+                 * helper makes sure to move the END marker with every
+                 * insertion.
+                 */
+                overload = outgoing->overload;
+                n_dhcp4_outgoing_append_option(outgoing, N_DHCP4_OPTION_OVERLOAD, &overload, 1);
+
+                if (overload & N_DHCP4_OVERLOAD_FILE)
+                        outgoing->message->file[0] = N_DHCP4_OPTION_END;
+                if (overload & N_DHCP4_OVERLOAD_SNAME)
+                        outgoing->message->sname[0] = N_DHCP4_OPTION_END;
+
+                if (overload & N_DHCP4_OVERLOAD_FILE)
+                        outgoing->i_message = offsetof(NDhcp4Message, file);
+                else if (overload & N_DHCP4_OVERLOAD_SNAME)
+                        outgoing->i_message = offsetof(NDhcp4Message, sname);
+        }
+
+        /*
+         * The OPTIONs section is full and OVERLOAD was enabled. Try writing
+         * into the FILE section. Always reserve 1 byte for the trailing END
+         * marker.
+         */
+        if (outgoing->i_message >= offsetof(NDhcp4Message, file)) {
+                rem = sizeof(outgoing->message->file);
+                rem -= outgoing->i_message - offsetof(NDhcp4Message, file);
+
+                if (rem >= n_data + 2U + 1U) {
+                        n_dhcp4_outgoing_append_option(outgoing, option, data, n_data);
+                        return 0;
+                }
+
+                if (overload & N_DHCP4_OVERLOAD_SNAME)
+                        outgoing->i_message = offsetof(NDhcp4Message, sname);
+                else
+                        return N_DHCP4_E_NO_SPACE;
+        }
+
+        /*
+         * OPTIONs and FILE are full, try putting data into the SNAME section
+         * as a last resort.
+         */
+        if (outgoing->i_message >= offsetof(NDhcp4Message, sname)) {
+                rem = sizeof(outgoing->message->sname);
+                rem -= outgoing->i_message - offsetof(NDhcp4Message, sname);
+
+                if (rem >= n_data + 2U + 1U) {
+                        n_dhcp4_outgoing_append_option(outgoing, option, data, n_data);
+                        return 0;
+                }
+        }
+
+        return N_DHCP4_E_NO_SPACE;
+}
+
+static int n_dhcp4_outgoing_append_u32(NDhcp4Outgoing *message, uint8_t option, uint32_t u32) {
+        uint32_t be32 = htonl(u32);
+        int r;
+
+        r = n_dhcp4_outgoing_append(message, option, &be32, sizeof(be32));
+        if (r)
+                return r;
+
+        return 0;
+}
+
+static int n_dhcp4_outgoing_append_in_addr(NDhcp4Outgoing *message, uint8_t option, struct in_addr addr) {
+        int r;
+
+        r = n_dhcp4_outgoing_append(message, option, &addr.s_addr, sizeof(addr.s_addr));
+        if (r)
+                return r;
+
+        return 0;
+}
+
+int n_dhcp4_outgoing_append_t1(NDhcp4Outgoing *message, uint32_t t1) {
+        return n_dhcp4_outgoing_append_u32(message, N_DHCP4_OPTION_RENEWAL_T1_TIME, t1);
+}
+
+int n_dhcp4_outgoing_append_t2(NDhcp4Outgoing *message, uint32_t t2) {
+        return n_dhcp4_outgoing_append_u32(message, N_DHCP4_OPTION_REBINDING_T2_TIME, t2);
+}
+
+int n_dhcp4_outgoing_append_lifetime(NDhcp4Outgoing *message, uint32_t lifetime) {
+        return n_dhcp4_outgoing_append_u32(message, N_DHCP4_OPTION_IP_ADDRESS_LEASE_TIME, lifetime);
+}
+
+int n_dhcp4_outgoing_append_server_identifier(NDhcp4Outgoing *message, struct in_addr addr) {
+        return n_dhcp4_outgoing_append_in_addr(message, N_DHCP4_OPTION_SERVER_IDENTIFIER, addr);
+}
+
+int n_dhcp4_outgoing_append_requested_ip(NDhcp4Outgoing *message, struct in_addr addr) {
+        return n_dhcp4_outgoing_append_in_addr(message, N_DHCP4_OPTION_REQUESTED_IP_ADDRESS, addr);
+}
+
+void n_dhcp4_outgoing_set_secs(NDhcp4Outgoing *message, uint32_t secs) {
+        NDhcp4Header *header = n_dhcp4_outgoing_get_header(message);
+
+        /*
+         * Some DHCP servers will reject DISCOVER or REQUEST messages if 'secs'
+         * is not set (i.e., set to 0), even though the spec allows it.
+         */
+        c_assert(secs);
+
+        header->secs = htonl(secs);
+}
+
+void n_dhcp4_outgoing_set_xid(NDhcp4Outgoing *message, uint32_t xid) {
+        NDhcp4Header *header = n_dhcp4_outgoing_get_header(message);
+
+        header->xid = xid;
+}
+
+void n_dhcp4_outgoing_get_xid(NDhcp4Outgoing *message, uint32_t *xidp) {
+        NDhcp4Header *header = n_dhcp4_outgoing_get_header(message);
+
+        *xidp = header->xid;
+}
+
+void n_dhcp4_outgoing_set_yiaddr(NDhcp4Outgoing *message, struct in_addr yiaddr) {
+        NDhcp4Header *header = n_dhcp4_outgoing_get_header(message);
+
+        header->yiaddr = yiaddr.s_addr;
+}
diff --git a/shared/n-dhcp4/src/n-dhcp4-private.h b/shared/n-dhcp4/src/n-dhcp4-private.h
new file mode 100644
index 00000000..c38ddbfc
--- /dev/null
+++ b/shared/n-dhcp4/src/n-dhcp4-private.h
@@ -0,0 +1,688 @@
+#pragma once
+
+#include <arpa/inet.h>
+#include <assert.h>
+#include <c-list.h>
+#include <c-stdaux.h>
+#include <endian.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <time.h>
+#include <unistd.h>
+#include "n-dhcp4.h"
+
+typedef struct NDhcp4CConnection NDhcp4CConnection;
+typedef struct NDhcp4CEventNode NDhcp4CEventNode;
+typedef struct NDhcp4ClientProbeOption NDhcp4ClientProbeOption;
+typedef struct NDhcp4Header NDhcp4Header;
+typedef struct NDhcp4Incoming NDhcp4Incoming;
+typedef struct NDhcp4Message NDhcp4Message;
+typedef struct NDhcp4Outgoing NDhcp4Outgoing;
+typedef struct NDhcp4SConnection NDhcp4SConnection;
+typedef struct NDhcp4SConnectionIp NDhcp4SConnectionIp;
+typedef struct NDhcp4SEventNode NDhcp4SEventNode;
+
+/* specs */
+
+#define N_DHCP4_NETWORK_IP_MAXIMUM_HEADER_SIZE (60) /* See RFC791 */
+#define N_DHCP4_NETWORK_IP_MINIMUM_MAX_SIZE (576) /* See RFC791 */
+#define N_DHCP4_NETWORK_SERVER_PORT (67)
+#define N_DHCP4_NETWORK_CLIENT_PORT (68)
+#define N_DHCP4_MESSAGE_MAGIC ((uint32_t)(0x63825363))
+#define N_DHCP4_MESSAGE_FLAG_BROADCAST (htons(0x8000))
+
+enum {
+        N_DHCP4_OP_BOOTREQUEST                          = 1,
+        N_DHCP4_OP_BOOTREPLY                            = 2,
+};
+
+enum {
+        N_DHCP4_OPTION_PAD                              = 0,
+        N_DHCP4_OPTION_SUBNET_MASK                      = 1,
+        N_DHCP4_OPTION_TIME_OFFSET                      = 2,
+        N_DHCP4_OPTION_ROUTER                           = 3,
+        N_DHCP4_OPTION_DOMAIN_NAME_SERVER               = 6,
+        N_DHCP4_OPTION_HOST_NAME                        = 12,
+        N_DHCP4_OPTION_BOOT_FILE_SIZE                   = 13,
+        N_DHCP4_OPTION_DOMAIN_NAME                      = 15,
+        N_DHCP4_OPTION_ROOT_PATH                        = 17,
+        N_DHCP4_OPTION_ENABLE_IP_FORWARDING             = 19,
+        N_DHCP4_OPTION_ENABLE_IP_FORWARDING_NL          = 20,
+        N_DHCP4_OPTION_POLICY_FILTER                    = 21,
+        N_DHCP4_OPTION_INTERFACE_MDR                    = 22,
+        N_DHCP4_OPTION_INTERFACE_TTL                    = 23,
+        N_DHCP4_OPTION_INTERFACE_MTU_AGING_TIMEOUT      = 24,
+        N_DHCP4_OPTION_INTERFACE_MTU                    = 26,
+        N_DHCP4_OPTION_BROADCAST                        = 28,
+        N_DHCP4_OPTION_STATIC_ROUTE                     = 33,
+        N_DHCP4_OPTION_NTP_SERVER                       = 42,
+        N_DHCP4_OPTION_VENDOR_SPECIFIC                  = 43,
+        N_DHCP4_OPTION_REQUESTED_IP_ADDRESS             = 50,
+        N_DHCP4_OPTION_IP_ADDRESS_LEASE_TIME            = 51,
+        N_DHCP4_OPTION_OVERLOAD                         = 52,
+        N_DHCP4_OPTION_MESSAGE_TYPE                     = 53,
+        N_DHCP4_OPTION_SERVER_IDENTIFIER                = 54,
+        N_DHCP4_OPTION_PARAMETER_REQUEST_LIST           = 55,
+        N_DHCP4_OPTION_ERROR_MESSAGE                    = 56,
+        N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE             = 57,
+        N_DHCP4_OPTION_RENEWAL_T1_TIME                  = 58,
+        N_DHCP4_OPTION_REBINDING_T2_TIME                = 59,
+        N_DHCP4_OPTION_VENDOR_CLASS_IDENTIFIER          = 60,
+        N_DHCP4_OPTION_CLIENT_IDENTIFIER                = 61,
+        N_DHCP4_OPTION_FQDN                             = 81,
+        N_DHCP4_OPTION_NEW_POSIX_TIMEZONE               = 100,
+        N_DHCP4_OPTION_NEW_TZDB_TIMEZONE                = 101,
+        N_DHCP4_OPTION_CLASSLESS_STATIC_ROUTE           = 121,
+        N_DHCP4_OPTION_PRIVATE_BASE                     = 224,
+        N_DHCP4_OPTION_PRIVATE_LAST                     = 254,
+        N_DHCP4_OPTION_END                              = 255,
+        _N_DHCP4_OPTION_N                               = 256,
+};
+
+enum {
+        N_DHCP4_OVERLOAD_FILE                           = 1,
+        N_DHCP4_OVERLOAD_SNAME                          = 2,
+};
+
+enum {
+        N_DHCP4_MESSAGE_DISCOVER                        = 1,
+        N_DHCP4_MESSAGE_OFFER                           = 2,
+        N_DHCP4_MESSAGE_REQUEST                         = 3,
+        N_DHCP4_MESSAGE_DECLINE                         = 4,
+        N_DHCP4_MESSAGE_ACK                             = 5,
+        N_DHCP4_MESSAGE_NAK                             = 6,
+        N_DHCP4_MESSAGE_RELEASE                         = 7,
+        N_DHCP4_MESSAGE_INFORM                          = 8,
+        N_DHCP4_MESSAGE_FORCERENEW                      = 9,
+};
+
+struct NDhcp4Header {
+        uint8_t op;
+        uint8_t htype;
+        uint8_t hlen;
+        uint8_t hops;
+        uint32_t xid;
+        uint16_t secs;
+        uint16_t flags;
+        uint32_t ciaddr;
+        uint32_t yiaddr;
+        uint32_t siaddr;
+        uint32_t giaddr;
+        uint8_t chaddr[16];
+} _c_packed_;
+
+struct NDhcp4Message {
+        NDhcp4Header header;
+        uint8_t sname[64];
+        uint8_t file[128];
+        uint32_t magic;
+        uint8_t options[];
+} _c_packed_;
+
+/* objects */
+
+enum {
+        _N_DHCP4_E_INTERNAL = _N_DHCP4_E_N,
+
+        N_DHCP4_E_UNEXPECTED,
+
+        N_DHCP4_E_NO_SPACE,
+        N_DHCP4_E_MALFORMED,
+
+        N_DHCP4_E_DROPPED,
+        N_DHCP4_E_DOWN,
+        N_DHCP4_E_AGAIN,
+};
+
+enum {
+        N_DHCP4_C_CONNECTION_STATE_INIT,
+        N_DHCP4_C_CONNECTION_STATE_PACKET,
+        N_DHCP4_C_CONNECTION_STATE_DRAINING,
+        N_DHCP4_C_CONNECTION_STATE_UDP,
+        N_DHCP4_C_CONNECTION_STATE_CLOSED,
+};
+
+enum {
+        N_DHCP4_CLIENT_EPOLL_TIMER,
+        N_DHCP4_CLIENT_EPOLL_IO,
+};
+
+enum {
+        N_DHCP4_CLIENT_PROBE_STATE_INIT,
+        N_DHCP4_CLIENT_PROBE_STATE_INIT_REBOOT,
+        N_DHCP4_CLIENT_PROBE_STATE_SELECTING,
+        N_DHCP4_CLIENT_PROBE_STATE_REBOOTING,
+        N_DHCP4_CLIENT_PROBE_STATE_REQUESTING,
+        N_DHCP4_CLIENT_PROBE_STATE_GRANTED,
+        N_DHCP4_CLIENT_PROBE_STATE_BOUND,
+        N_DHCP4_CLIENT_PROBE_STATE_RENEWING,
+        N_DHCP4_CLIENT_PROBE_STATE_REBINDING,
+        N_DHCP4_CLIENT_PROBE_STATE_EXPIRED,
+};
+
+enum {
+        N_DHCP4_CLIENT_LEASE_STATE_INIT,
+        N_DHCP4_CLIENT_LEASE_STATE_OFFERED,
+        N_DHCP4_CLIENT_LEASE_STATE_SELECTED,
+        N_DHCP4_CLIENT_LEASE_STATE_DECLINED,
+        N_DHCP4_CLIENT_LEASE_STATE_ACKED,
+};
+
+enum {
+        N_DHCP4_SERVER_EPOLL_TIMER,
+        N_DHCP4_SERVER_EPOLL_IO,
+};
+
+enum {
+        _N_DHCP4_C_MESSAGE_INVALID = 0,
+        N_DHCP4_C_MESSAGE_DISCOVER,
+        N_DHCP4_C_MESSAGE_INFORM,
+        N_DHCP4_C_MESSAGE_SELECT,
+        N_DHCP4_C_MESSAGE_IGNORE,
+        N_DHCP4_C_MESSAGE_RENEW,
+        N_DHCP4_C_MESSAGE_REBIND,
+        N_DHCP4_C_MESSAGE_REBOOT,
+        N_DHCP4_C_MESSAGE_RELEASE,
+        N_DHCP4_C_MESSAGE_DECLINE,
+};
+
+struct NDhcp4Outgoing {
+        NDhcp4Message *message;
+        size_t n_message;
+        size_t i_message;
+        size_t max_size;
+
+        uint8_t overload : 2;
+
+        struct {
+                uint8_t type;
+                uint64_t start_time;
+                uint64_t base_time;
+                uint64_t send_time;
+                uint64_t send_jitter;
+                size_t n_send;
+        } userdata;
+};
+
+#define N_DHCP4_OUTGOING_NULL(_x) {                                             \
+        }
+
+struct NDhcp4Incoming {
+        struct {
+                uint8_t *value;
+                size_t size;
+        } options[_N_DHCP4_OPTION_N];
+
+        struct {
+                uint8_t type;
+                uint64_t start_time;
+                uint64_t base_time;
+        } userdata;
+
+        size_t n_message;
+        NDhcp4Message message;
+        /* @message must be the last member */
+};
+
+#define N_DHCP4_INCOMING_NULL(_x) {                                             \
+        }
+
+struct NDhcp4ClientConfig {
+        int ifindex;
+        unsigned int transport;
+        bool request_broadcast;
+        uint8_t mac[32]; /* MAX_ADDR_LEN */
+        size_t n_mac;
+        uint8_t broadcast_mac[32]; /* MAX_ADDR_LEN */
+        size_t n_broadcast_mac;
+        uint8_t *client_id;
+        size_t n_client_id;
+};
+
+#define N_DHCP4_CLIENT_CONFIG_NULL(_x) {                                        \
+                .transport = _N_DHCP4_TRANSPORT_N,                              \
+        }
+
+struct NDhcp4ClientProbeOption {
+        uint8_t option;
+        uint8_t n_data;
+        uint8_t data[];
+};
+
+#define N_DHCP4_CLIENT_PROBE_OPTION_NULL(_x) {                                  \
+                .option = N_DHCP4_OPTION_PAD,                                   \
+        }
+
+struct NDhcp4ClientProbeConfig {
+        bool inform_only;
+        bool init_reboot;
+        struct in_addr requested_ip;
+        struct drand48_data entropy;    /* entropy pool */
+        uint64_t ms_start_delay;        /* max ms to wait before starting probe */
+        NDhcp4ClientProbeOption *options[UINT8_MAX + 1];
+        int8_t request_parameters[UINT8_MAX + 1];
+        size_t n_request_parameters;
+};
+
+#define N_DHCP4_CLIENT_PROBE_CONFIG_NULL(_x) {                                  \
+                .ms_start_delay = N_DHCP4_CLIENT_START_DELAY_RFC2131,           \
+        }
+
+struct NDhcp4CEventNode {
+        CList client_link;
+        CList probe_link;
+        NDhcp4ClientEvent event;
+        bool is_public : 1;
+};
+
+#define N_DHCP4_C_EVENT_NODE_NULL(_x) {                                         \
+                .client_link = C_LIST_INIT((_x).client_link),                   \
+                .probe_link = C_LIST_INIT((_x).probe_link),                     \
+        }
+
+struct NDhcp4CConnection {
+        NDhcp4ClientConfig *client_config;
+        NDhcp4ClientProbeConfig *probe_config;
+        int fd_epoll;
+
+        unsigned int state;             /* current connection state */
+        int fd_packet;                  /* packet socket */
+        int fd_udp;                     /* udp socket */
+
+        NDhcp4Outgoing *request;        /* current request */
+
+        uint32_t client_ip;             /* client IP address, or 0 */
+        uint32_t server_ip;             /* server IP address, or 0 */
+        uint16_t mtu;                   /* client mtu, or 0 */
+
+        /*
+         * When we get DHCP packets from the kernel, we need a buffer to read
+         * the data into. Since UDP packets can be up to 2^16 bytes in size, we
+         * avoid placing it on the stack and instead read into this scratch
+         * buffer. It is purely meant as stack replacement, no data is returned
+         * through this buffer.
+         */
+        uint8_t scratch_buffer[UINT16_MAX];
+};
+
+#define N_DHCP4_C_CONNECTION_NULL(_x) {                                         \
+                .fd_packet = -1,                                                \
+                .fd_udp = -1,                                                   \
+        }
+
+struct NDhcp4Client {
+        unsigned long n_refs;
+        NDhcp4ClientConfig *config;
+        CList event_list;
+        int fd_epoll;
+        int fd_timer;
+
+        uint16_t mtu;
+        NDhcp4ClientProbe *current_probe;
+        uint64_t scheduled_timeout;
+
+        bool preempted : 1;
+};
+
+#define N_DHCP4_CLIENT_NULL(_x) {                                               \
+                .n_refs = 1,                                                    \
+                .event_list = C_LIST_INIT((_x).event_list),                     \
+                .fd_epoll = -1,                                                 \
+                .fd_timer = -1,                                                 \
+        }
+
+struct NDhcp4ClientProbe {
+        NDhcp4ClientProbeConfig *config;
+        NDhcp4Client *client;
+        CList event_list;
+        CList lease_list;
+        void *userdata;
+
+        unsigned int state;                     /* current probe state */
+        uint64_t ns_deferred;                   /* timeout for deferred action */
+        NDhcp4ClientLease *current_lease;       /* current lease */
+
+        NDhcp4CConnection connection;           /* client connection wrapper */
+};
+
+#define N_DHCP4_CLIENT_PROBE_NULL(_x) {                                         \
+                .event_list = C_LIST_INIT((_x).event_list),                     \
+                .lease_list = C_LIST_INIT((_x).lease_list),                     \
+                .connection = N_DHCP4_C_CONNECTION_NULL((_x).connection),       \
+        }
+
+struct NDhcp4ClientLease {
+        unsigned long n_refs;
+
+        NDhcp4ClientProbe *probe;
+        CList probe_link;
+
+        NDhcp4Incoming *message;
+
+        uint64_t t1;
+        uint64_t t2;
+        uint64_t lifetime;
+};
+
+#define N_DHCP4_CLIENT_LEASE_NULL(_x) {                                         \
+                .n_refs = 1,                                                    \
+                .probe_link = C_LIST_INIT((_x).probe_link),                     \
+        }
+
+struct NDhcp4ServerConfig {
+        int ifindex;
+};
+
+#define N_DHCP4_SERVER_CONFIG_NULL(_x) {                                        \
+        }
+
+struct NDhcp4SEventNode {
+        CList server_link;
+        NDhcp4ServerEvent event;
+        bool is_public : 1;
+};
+
+#define N_DHCP4_S_EVENT_NODE_NULL(_x) {                                         \
+                .server_link = C_LIST_INIT((_x).server_link),                   \
+        }
+
+struct NDhcp4SConnection {
+        int ifindex;                    /* interface index */
+        int fd_packet;                  /* packet socket */
+        int fd_udp;                     /* udp socket */
+        uint8_t buf[UINT16_MAX];        /* scratch recevie buffer */
+
+        /* XXX: support a set of server addresses */
+        NDhcp4SConnectionIp *ip;        /* server IP address, or NULL */
+};
+
+#define N_DHCP4_S_CONNECTION_NULL(_x) {                                         \
+                .fd_packet = -1,                                                \
+                .fd_udp = -1,                                                   \
+        }
+
+struct NDhcp4SConnectionIp {
+        NDhcp4SConnection *connection;
+        struct in_addr ip;
+};
+
+#define N_DHCP4_S_CONNECTION_IP_NULL(_x) {                                      \
+}
+
+struct NDhcp4Server {
+        unsigned long n_refs;
+        CList event_list;
+        CList lease_list;
+
+        bool preempted : 1;
+
+        NDhcp4SConnection connection;
+};
+
+#define N_DHCP4_SERVER_NULL(_x) {                                               \
+                .n_refs = 1,                                                    \
+                .event_list = C_LIST_INIT((_x).event_list),                     \
+                .lease_list = C_LIST_INIT((_x).lease_list),                     \
+                .connection = N_DHCP4_S_CONNECTION_NULL((_x).connection),       \
+        }
+
+struct NDhcp4ServerIp {
+                NDhcp4SConnectionIp ip;
+};
+
+#define N_DHCP4_SERVER_IP_NULL(_x) {                                            \
+                .ip = N_DHCP4_S_CONNECTION_IP_NULL((_x).ip),                    \
+        }
+
+struct NDhcp4ServerLease {
+        unsigned long n_refs;
+
+        NDhcp4Server *server;
+        CList server_link;
+
+        NDhcp4Incoming *request;
+        NDhcp4Incoming *reply;
+};
+
+#define N_DHCP4_SERVER_LEASE_NULL(_x) {                                         \
+                .n_refs = 1,                                                    \
+                .server_link = C_LIST_INIT((_x).server_link),                   \
+        }
+
+/* outgoing messages */
+
+int n_dhcp4_outgoing_new(NDhcp4Outgoing **outgoingp, size_t max_size, uint8_t overload);
+NDhcp4Outgoing *n_dhcp4_outgoing_free(NDhcp4Outgoing *outgoing);
+
+NDhcp4Header *n_dhcp4_outgoing_get_header(NDhcp4Outgoing *outgoing);
+size_t n_dhcp4_outgoing_get_raw(NDhcp4Outgoing *outgoing, const void **rawp);
+int n_dhcp4_outgoing_append(NDhcp4Outgoing *outgoing, uint8_t option, const void *data, uint8_t n_data);
+
+int n_dhcp4_outgoing_append_t1(NDhcp4Outgoing *message, uint32_t t1);
+int n_dhcp4_outgoing_append_t2(NDhcp4Outgoing *message, uint32_t t2);
+int n_dhcp4_outgoing_append_lifetime(NDhcp4Outgoing *message, uint32_t lifetime);
+int n_dhcp4_outgoing_append_server_identifier(NDhcp4Outgoing *message, struct in_addr addr);
+int n_dhcp4_outgoing_append_requested_ip(NDhcp4Outgoing *message, struct in_addr addr);
+
+void n_dhcp4_outgoing_set_secs(NDhcp4Outgoing *message, uint32_t secs);
+void n_dhcp4_outgoing_set_xid(NDhcp4Outgoing *message, uint32_t xid);
+void n_dhcp4_outgoing_set_yiaddr(NDhcp4Outgoing *message, struct in_addr yiaddr);
+
+void n_dhcp4_outgoing_get_xid(NDhcp4Outgoing *message, uint32_t *xidp);
+
+/* incoming messages */
+
+int n_dhcp4_incoming_new(NDhcp4Incoming **incomingp, const void *raw, size_t n_raw);
+NDhcp4Incoming *n_dhcp4_incoming_free(NDhcp4Incoming *incoming);
+
+NDhcp4Header *n_dhcp4_incoming_get_header(NDhcp4Incoming *incoming);
+size_t n_dhcp4_incoming_get_raw(NDhcp4Incoming *incoming, const void **rawp);
+int n_dhcp4_incoming_query(NDhcp4Incoming *incoming, uint8_t option, uint8_t **datap, size_t *n_datap);
+
+int n_dhcp4_incoming_query_message_type(NDhcp4Incoming *message, uint8_t *typep);
+int n_dhcp4_incoming_query_lifetime(NDhcp4Incoming *message, uint32_t *lifetimep);
+int n_dhcp4_incoming_query_t2(NDhcp4Incoming *message, uint32_t *t2p);
+int n_dhcp4_incoming_query_t1(NDhcp4Incoming *message, uint32_t *t1p);
+int n_dhcp4_incoming_query_server_identifier(NDhcp4Incoming *message, struct in_addr *idp);
+int n_dhcp4_incoming_query_max_message_size(NDhcp4Incoming *message, uint16_t *max_message_sizep);
+int n_dhcp4_incoming_query_requested_ip(NDhcp4Incoming *message, struct in_addr *requested_ipp);
+
+void n_dhcp4_incoming_get_xid(NDhcp4Incoming *message, uint32_t *xidp);
+void n_dhcp4_incoming_get_yiaddr(NDhcp4Incoming *message, struct in_addr *yiaddr);
+
+/* sockets */
+
+int n_dhcp4_c_socket_packet_new(int *sockfdp, int ifindex);
+int n_dhcp4_c_socket_udp_new(int *sockfdp,
+                             int ifindex,
+                             const struct in_addr *client_addr,
+                             const struct in_addr *server_addr);
+int n_dhcp4_s_socket_packet_new(int *sockfdp);
+int n_dhcp4_s_socket_udp_new(int *sockfdp, int ifindex);
+
+int n_dhcp4_c_socket_packet_send(int sockfd,
+                                 int ifindex,
+                                 const unsigned char *dest_haddr,
+                                 unsigned char halen,
+                                 NDhcp4Outgoing *message);
+int n_dhcp4_c_socket_udp_send(int sockfd, NDhcp4Outgoing *message);
+int n_dhcp4_c_socket_udp_broadcast(int sockfd, NDhcp4Outgoing *message);
+int n_dhcp4_s_socket_packet_send(int sockfd,
+                                 int ifindex,
+                                 const struct in_addr *src_inaddr,
+                                 const unsigned char *dest_haddr,
+                                 unsigned char halen,
+                                 const struct in_addr *dest_inaddr,
+                                 NDhcp4Outgoing *message);
+int n_dhcp4_s_socket_udp_send(int sockfd,
+                              const struct in_addr *inaddr_src,
+                              const struct in_addr *inaddr_dest,
+                              NDhcp4Outgoing *message);
+int n_dhcp4_s_socket_udp_broadcast(int sockfd,
+                                   const struct in_addr *inaddr_src,
+                                   NDhcp4Outgoing *message);
+
+int n_dhcp4_c_socket_packet_recv(int sockfd,
+                                 uint8_t *buf,
+                                 size_t n_buf,
+                                 NDhcp4Incoming **messagep);
+int n_dhcp4_c_socket_udp_recv(int sockfd,
+                              uint8_t *buf,
+                              size_t n_buf,
+                              NDhcp4Incoming **messagep);
+int n_dhcp4_s_socket_udp_recv(int sockfd,
+                              uint8_t *buf,
+                              size_t n_buf,
+                              NDhcp4Incoming **messagep,
+                              struct sockaddr_in *dest);
+
+/* client configs */
+
+int n_dhcp4_client_config_dup(NDhcp4ClientConfig *config,
+                              NDhcp4ClientConfig **dupp);
+
+/* client probe configs */
+
+int n_dhcp4_client_probe_config_dup(NDhcp4ClientProbeConfig *config,
+                                    NDhcp4ClientProbeConfig **dupp);
+uint32_t n_dhcp4_client_probe_config_get_random(NDhcp4ClientProbeConfig *config);
+
+/* client events */
+
+int n_dhcp4_c_event_node_new(NDhcp4CEventNode **nodep);
+NDhcp4CEventNode *n_dhcp4_c_event_node_free(NDhcp4CEventNode *node);
+
+/* client connections */
+
+int n_dhcp4_c_connection_init(NDhcp4CConnection *connection,
+                              NDhcp4ClientConfig *client_config,
+                              NDhcp4ClientProbeConfig *probe_config,
+                              int fd_epoll);
+void n_dhcp4_c_connection_deinit(NDhcp4CConnection *connection);
+
+int n_dhcp4_c_connection_listen(NDhcp4CConnection *connection);
+int n_dhcp4_c_connection_connect(NDhcp4CConnection *connection,
+                                 const struct in_addr *client,
+                                 const struct in_addr *server);
+void n_dhcp4_c_connection_close(NDhcp4CConnection *connection);
+
+void n_dhcp4_c_connection_get_timeout(NDhcp4CConnection *connection,
+                                      uint64_t *timeoutp);
+
+int n_dhcp4_c_connection_discover_new(NDhcp4CConnection *connection,
+                                      NDhcp4Outgoing **request);
+int n_dhcp4_c_connection_select_new(NDhcp4CConnection *connection,
+                                    NDhcp4Outgoing **request,
+                                    NDhcp4Incoming *offer);
+int n_dhcp4_c_connection_reboot_new(NDhcp4CConnection *connection,
+                                    NDhcp4Outgoing **request,
+                                    const struct in_addr *client);
+int n_dhcp4_c_connection_renew_new(NDhcp4CConnection *connection,
+                                   NDhcp4Outgoing **request);
+int n_dhcp4_c_connection_rebind_new(NDhcp4CConnection *connection,
+                                    NDhcp4Outgoing **request);
+int n_dhcp4_c_connection_decline_new(NDhcp4CConnection *connection,
+                                     NDhcp4Outgoing **request,
+                                     NDhcp4Incoming *ack,
+                                     const char *error);
+int n_dhcp4_c_connection_inform_new(NDhcp4CConnection *connection,
+                                    NDhcp4Outgoing **request);
+int n_dhcp4_c_connection_release_new(NDhcp4CConnection *connection,
+                                     NDhcp4Outgoing **request,
+                                     const char *error);
+
+int n_dhcp4_c_connection_start_request(NDhcp4CConnection *connection,
+                                       NDhcp4Outgoing *request,
+                                       uint64_t timestamp);
+int n_dhcp4_c_connection_dispatch_timer(NDhcp4CConnection *connection,
+                                        uint64_t timestamp);
+int n_dhcp4_c_connection_dispatch_io(NDhcp4CConnection *connection,
+                                     NDhcp4Incoming **messagep);
+
+/* clients */
+
+int n_dhcp4_client_raise(NDhcp4Client *client, NDhcp4CEventNode **nodep, unsigned int event);
+void n_dhcp4_client_arm_timer(NDhcp4Client *client);
+
+/* client probes */
+
+int n_dhcp4_client_probe_new(NDhcp4ClientProbe **probep,
+                             NDhcp4ClientProbeConfig *config,
+                             NDhcp4Client *client,
+                             uint64_t ns_now);
+
+int n_dhcp4_client_probe_raise(NDhcp4ClientProbe *probe, NDhcp4CEventNode **nodep, unsigned int event);
+void n_dhcp4_client_probe_get_timeout(NDhcp4ClientProbe *probe, uint64_t *timeoutp);
+int n_dhcp4_client_probe_dispatch_timer(NDhcp4ClientProbe *probe, uint64_t ns_now);
+int n_dhcp4_client_probe_dispatch_io(NDhcp4ClientProbe *probe, uint32_t events);
+int n_dhcp4_client_probe_transition_select(NDhcp4ClientProbe *probe, NDhcp4Incoming *offer, uint64_t ns_now);
+int n_dhcp4_client_probe_transition_accept(NDhcp4ClientProbe *probe, NDhcp4Incoming *ack);
+int n_dhcp4_client_probe_transition_decline(NDhcp4ClientProbe *probe, NDhcp4Incoming *offer, const char *error, uint64_t ns_now);
+int n_dhcp4_client_probe_update_mtu(NDhcp4ClientProbe *probe, uint16_t mtu);
+
+/* client leases */
+
+int n_dhcp4_client_lease_new(NDhcp4ClientLease **leasep, NDhcp4Incoming *message);
+void n_dhcp4_client_lease_link(NDhcp4ClientLease *lease, NDhcp4ClientProbe *probe);
+void n_dhcp4_client_lease_unlink(NDhcp4ClientLease *lease);
+
+/* server connections */
+
+int n_dhcp4_s_connection_init(NDhcp4SConnection *connection, int ifindex);
+void n_dhcp4_s_connection_deinit(NDhcp4SConnection *connection);
+
+void n_dhcp4_s_connection_get_fd(NDhcp4SConnection *connection, int *fdp);
+int n_dhcp4_s_connection_dispatch_io(NDhcp4SConnection *connection, NDhcp4Incoming **messagep);
+
+int n_dhcp4_s_connection_offer_new(NDhcp4SConnection *connection,
+                                   NDhcp4Outgoing **replyp,
+                                   NDhcp4Incoming *request,
+                                   const struct in_addr *server_address,
+                                   const struct in_addr *client_address,
+                                   uint32_t lifetime);
+int n_dhcp4_s_connection_ack_new(NDhcp4SConnection *connection,
+                                   NDhcp4Outgoing **replyp,
+                                   NDhcp4Incoming *request,
+                                   const struct in_addr *server_address,
+                                   const struct in_addr *client_address,
+                                   uint32_t lifetime);
+int n_dhcp4_s_connection_nak_new(NDhcp4SConnection *connection,
+                                 NDhcp4Outgoing **replyp,
+                                 NDhcp4Incoming *request,
+                                 const struct in_addr *server_address);
+
+int n_dhcp4_s_connection_send_reply(NDhcp4SConnection *connection,
+                                    const struct in_addr *server_addr,
+                                    NDhcp4Outgoing *reply);
+
+/* server connection ips */
+
+void n_dhcp4_s_connection_ip_init(NDhcp4SConnectionIp *ip, struct in_addr addr);
+void n_dhcp4_s_connection_ip_deinit(NDhcp4SConnectionIp *ip);
+
+void n_dhcp4_s_connection_ip_link(NDhcp4SConnectionIp *ip, NDhcp4SConnection *connection);
+void n_dhcp4_s_connection_ip_unlink(NDhcp4SConnectionIp *ip);
+
+/* inline helpers */
+
+static inline void n_dhcp4_outgoing_freep(NDhcp4Outgoing **outgoing) {
+        if (*outgoing)
+                n_dhcp4_outgoing_free(*outgoing);
+}
+
+static inline void n_dhcp4_incoming_freep(NDhcp4Incoming **incoming) {
+        if (*incoming)
+                n_dhcp4_incoming_free(*incoming);
+}
+
+static inline uint64_t n_dhcp4_gettime(clockid_t clock) {
+        struct timespec ts;
+        int r;
+
+        r = clock_gettime(clock, &ts);
+        c_assert(r >= 0);
+
+        return ts.tv_sec * 1000ULL * 1000ULL * 1000ULL + ts.tv_nsec;
+}
diff --git a/shared/n-dhcp4/src/n-dhcp4-socket.c b/shared/n-dhcp4/src/n-dhcp4-socket.c
new file mode 100644
index 00000000..b9ac176f
--- /dev/null
+++ b/shared/n-dhcp4/src/n-dhcp4-socket.c
@@ -0,0 +1,657 @@
+/*
+ * DHCP specific low-level socket helpers
+ */
+
+#include <c-stdaux.h>
+#include <errno.h>
+#include <linux/filter.h>
+#include <sys/socket.h> /* needed by linux/if.h */
+#include <linux/if.h>
+#include <linux/if_packet.h>
+#include <linux/netdevice.h>
+#include <linux/udp.h>
+#include <netinet/ip.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/types.h>
+#include "n-dhcp4-private.h"
+#include "util/packet.h"
+#include "util/socket.h"
+
+/**
+ * n_dhcp4_c_socket_packet_new() - create a new DHCP4 client packet socket
+ * @sockfdp:            return argumnet for the new socket
+ * @ifindex:            interface index to bind to
+ *
+ * Create a new AF_PACKET/SOCK_DGRAM socket usable to listen to and send DHCP client
+ * packets before an IP address has been configured.
+ *
+ * Only unfragmented DHCP packets from a server to a client destined for the given
+ * ifindex is returned.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+int n_dhcp4_c_socket_packet_new(int *sockfdp, int ifindex) {
+        _c_cleanup_(c_closep) int sockfd = -1;
+        struct sock_filter filter[] = {
+                /*
+                 * IP
+                 *
+                 * Check
+                 *  - UDP
+                 *  - Unfragmented
+                 *  - Large enough to fit the DHCP header
+                 *
+                 *  Leave X the size of the IP header, for future indirect reads.
+                 */
+                BPF_STMT(BPF_LD + BPF_B + BPF_ABS, offsetof(struct iphdr, protocol)),                           /* A <- IP protocol */
+                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, IPPROTO_UDP, 1, 0),                                         /* IP protocol == UDP ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                BPF_STMT(BPF_LD + BPF_B + BPF_ABS, offsetof(struct iphdr, frag_off)),                           /* A <- Flags */
+                BPF_STMT(BPF_ALU + BPF_AND + BPF_K, ntohs(IP_MF | IP_OFFMASK)),                                 /* A <- A & (IP_MF | IP_OFFMASK) */
+                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0, 1, 0),                                                   /* fragmented packet ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                BPF_STMT(BPF_LDX + BPF_B + BPF_MSH, 0),                                                         /* X <- IP header length */
+                BPF_STMT(BPF_LD + BPF_W + BPF_LEN, 0),                                                          /* A <- packet length */
+                BPF_STMT(BPF_ALU + BPF_SUB + BPF_X, 0),                                                         /* A -= X */
+                BPF_JUMP(BPF_JMP + BPF_JGE + BPF_K, sizeof(struct udphdr) + sizeof(NDhcp4Message), 1, 0),       /* packet >= DHCPPacket ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                /*
+                 * UDP
+                 *
+                 * Check
+                 *  - DHCP client port
+                 *
+                 * Leave X the size of IP and UDP headers, for future indirect reads.
+                 */
+                BPF_STMT(BPF_LD + BPF_H + BPF_IND, offsetof(struct udphdr, dest)),                              /* A <- UDP destination port */
+                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_NETWORK_CLIENT_PORT, 1, 0),                         /* UDP destination port == DHCP client port ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                BPF_STMT(BPF_LD + BPF_W + BPF_K, sizeof(struct udphdr)),                                        /* A <- size of UDP header */
+                BPF_STMT(BPF_ALU + BPF_ADD + BPF_X, 0),                                                         /* A += X */
+                BPF_STMT(BPF_MISC + BPF_TAX, 0),                                                                /* X <- A */
+
+                /*
+                 * DHCP
+                 *
+                 * Check
+                 *  - BOOTREPLY (from server to client)
+                 *  - DHCP magic cookie
+                 */
+                BPF_STMT(BPF_LD + BPF_B + BPF_IND, offsetof(NDhcp4Header, op)),                                 /* A <- DHCP op */
+                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_OP_BOOTREPLY, 1, 0),                                /* op == BOOTREPLY ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                BPF_STMT(BPF_LD + BPF_W + BPF_IND, offsetof(NDhcp4Message, magic)),                             /* A <- DHCP magic cookie */
+                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_MESSAGE_MAGIC, 1, 0),                               /* cookie == DHCP magic cookie ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                BPF_STMT(BPF_RET + BPF_K, 65535),                                                               /* return all */
+        };
+        struct sock_fprog fprog = {
+                .filter = filter,
+                .len = sizeof(filter) / sizeof(filter[0]),
+        };
+        struct sockaddr_ll addr = {
+                .sll_family = AF_PACKET,
+                .sll_protocol = htons(ETH_P_IP),
+                .sll_ifindex = ifindex,
+        };
+        int r, on = 1;
+
+        sockfd = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
+        if (sockfd < 0)
+                return -errno;
+
+        r = setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &fprog, sizeof(fprog));
+        if (r < 0)
+                return -errno;
+
+        /* We need the flag that tells us if the checksum is correct. */
+        r = setsockopt(sockfd, SOL_PACKET, PACKET_AUXDATA, &on, sizeof(on));
+        if (r < 0)
+                return -errno;
+
+        r = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
+        if (r < 0)
+                return -errno;
+
+        *sockfdp = sockfd;
+        sockfd = -1;
+        return 0;
+}
+
+/**
+ * n_dhcp4_c_socket_udp_new() - create a new DHCP4 client UDP socket
+ * @sockfdp:            return argumnet for the new socket
+ * @ifindex:            interface index to bind to
+ * @client_addr:        client address to bind to
+ * @server_addr:        server address to connect to
+ *
+ * Create a new AF_INET/SOCK_DGRAM socket usable to listen to and send DHCP client
+ * packets.
+ *
+ * The client address given in @addr must be configured on the interface @ifindex
+ * before the socket is created.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+int n_dhcp4_c_socket_udp_new(int *sockfdp,
+                             int ifindex,
+                             const struct in_addr *client_addr,
+                             const struct in_addr *server_addr) {
+        _c_cleanup_(c_closep) int sockfd = -1;
+        struct sock_filter filter[] = {
+                /*
+                 * IP/UDP
+                 *
+                 * Set X to the size of IP and UDP headers, for future indirect reads.
+                 */
+                BPF_STMT(BPF_LDX + BPF_B + BPF_MSH, 0),                                                         /* X <- IP header length */
+                BPF_STMT(BPF_LD + BPF_W + BPF_K, sizeof(struct udphdr)),                                        /* A <- size of UDP header */
+                BPF_STMT(BPF_ALU + BPF_ADD + BPF_X, 0),                                                         /* A += X */
+                BPF_STMT(BPF_MISC + BPF_TAX, 0),                                                                /* X <- A */
+
+                /*
+                 * DHCP
+                 *
+                 * Check
+                 *  - BOOTREPLY (from server to client)
+                 *  - DHCP magic cookie
+                 */
+                BPF_STMT(BPF_LD + BPF_B + BPF_IND, offsetof(NDhcp4Header, op)),                                 /* A <- DHCP op */
+                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_OP_BOOTREPLY, 1, 0),                                /* op == BOOTREPLY ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                BPF_STMT(BPF_LD + BPF_W + BPF_IND, offsetof(NDhcp4Message, magic)),                             /* A <- DHCP magic cookie */
+                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_MESSAGE_MAGIC, 1, 0),                               /* cookie == DHCP magic cookie ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                BPF_STMT(BPF_RET + BPF_K, 65535),                                                               /* return all */
+        };
+        struct sock_fprog fprog = {
+                .filter = filter,
+                .len = sizeof(filter) / sizeof(filter[0]),
+        };
+        struct sockaddr_in saddr = {
+                .sin_family = AF_INET,
+                .sin_addr = *client_addr,
+                .sin_port = htons(N_DHCP4_NETWORK_CLIENT_PORT),
+        };
+        struct sockaddr_in daddr = {
+                .sin_family = AF_INET,
+                .sin_addr = *server_addr,
+                .sin_port = htons(N_DHCP4_NETWORK_SERVER_PORT),
+        };
+        int r, tos = IPTOS_CLASS_CS6, on = 1;
+
+        sockfd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
+        if (sockfd < 0)
+                return -errno;
+
+        r = setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &fprog, sizeof(fprog));
+        if (r < 0)
+                return -errno;
+
+        r = socket_bind_if(sockfd, ifindex);
+        if (r)
+                return r;
+
+        r = setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
+        if (r < 0)
+                return -errno;
+
+        r = setsockopt(sockfd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos));
+        if (r < 0)
+                return -errno;
+
+        r = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
+        if (r < 0)
+                return -errno;
+
+        r = connect(sockfd, (struct sockaddr*)&daddr, sizeof(daddr));
+        if (r < 0)
+                return -errno;
+
+        *sockfdp = sockfd;
+        sockfd = -1;
+        return 0;
+}
+
+/**
+ * n_dhcp4_s_socket_packet_new() - create a new DHCP4 server packet socket
+ * @sockfdp:            return argumnet for the new socket
+ *
+ * Create a new AF_PACKET/SOCK_DGRAM socket usable to send DHCP packets to clients
+ * before they have an IP address configured, on the given interface.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+int n_dhcp4_s_socket_packet_new(int *sockfdp) {
+        _c_cleanup_(c_closep) int sockfd = -1;
+
+        sockfd = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
+        if (sockfd < 0)
+                return -errno;
+
+        *sockfdp = sockfd;
+        sockfd = -1;
+        return 0;
+}
+
+/**
+ * n_dhcp4_s_socket_udp_new() - create a new DHCP4 server UDP socket
+ * @sockfdp:            return argumnet for the new socket
+ * @ifindex:            intercafe index to bind to
+ *
+ * Create a new AF_INET/SOCK_DGRAM socket usable to listen to DHCP server packets,
+ * on the given interface.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+int n_dhcp4_s_socket_udp_new(int *sockfdp, int ifindex) {
+        _c_cleanup_(c_closep) int sockfd = -1;
+        struct sock_filter filter[] = {
+                /*
+                 * IP/UDP
+                 *
+                 * Set X to the size of IP and UDP headers, for future indirect reads.
+                 */
+                BPF_STMT(BPF_LDX + BPF_B + BPF_MSH, 0),                                                         /* X <- IP header length */
+                BPF_STMT(BPF_LD + BPF_W + BPF_K, sizeof(struct udphdr)),                                        /* A <- size of UDP header */
+                BPF_STMT(BPF_ALU + BPF_ADD + BPF_X, 0),                                                         /* A += X */
+                BPF_STMT(BPF_MISC + BPF_TAX, 0),                                                                /* X <- A */
+
+                /*
+                 * DHCP
+                 *
+                 * Check
+                 *  - BOOTREQUEST (from client to server)
+                 *  - DHCP magic cookie
+                 */
+
+                BPF_STMT(BPF_LD + BPF_B + BPF_IND, offsetof(NDhcp4Header, op)),                                 /* A <- DHCP op */
+                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_OP_BOOTREQUEST, 1, 0),                              /* op == BOOTREQUEST ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                BPF_STMT(BPF_LD + BPF_W + BPF_IND, offsetof(NDhcp4Message, magic)),                             /* A <- DHCP magic cookie */
+                BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, N_DHCP4_MESSAGE_MAGIC, 1, 0),                               /* cookie == DHCP magic cookie ? */
+                BPF_STMT(BPF_RET + BPF_K, 0),                                                                   /* ignore */
+
+                BPF_STMT(BPF_RET + BPF_K, 65535),                                                               /* return all */
+        };
+        struct sock_fprog fprog = {
+                .filter = filter,
+                .len = sizeof(filter) / sizeof(filter[0]),
+        };
+        struct sockaddr_in addr = {
+                .sin_family = AF_INET,
+                .sin_addr = { INADDR_ANY },
+                .sin_port = htons(N_DHCP4_NETWORK_SERVER_PORT),
+        };
+        int r, tos = IPTOS_CLASS_CS6, on = 1;
+
+        sockfd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
+        if (sockfd < 0)
+                return -errno;
+
+        r = setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &fprog, sizeof(fprog));
+        if (r < 0)
+                return -errno;
+
+        r = socket_bind_if(sockfd, ifindex);
+        if (r)
+                return r;
+
+        r = setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
+        if (r < 0)
+                return -errno;
+
+        r = setsockopt(sockfd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos));
+        if (r < 0)
+                return -errno;
+
+        r = setsockopt(sockfd, IPPROTO_IP, IP_PKTINFO, &on, sizeof(on));
+        if (r < 0)
+                return -errno;
+
+        r = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
+        if (r < 0)
+                return -errno;
+
+        *sockfdp = sockfd;
+        sockfd = -1;
+        return 0;
+}
+
+static int n_dhcp4_socket_packet_send(int sockfd,
+                                      int ifindex,
+                                      const struct sockaddr_in *src_paddr,
+                                      const unsigned char *dest_haddr,
+                                      unsigned char halen,
+                                      const struct sockaddr_in *dest_paddr,
+                                      NDhcp4Outgoing *message) {
+        struct packet_sockaddr_ll haddr = {
+                .sll_family = AF_PACKET,
+                .sll_protocol = htons(ETH_P_IP),
+                .sll_ifindex = ifindex,
+                .sll_halen = halen,
+        };
+        const void *buf;
+        size_t n_buf, len;
+        int r;
+
+        c_assert(halen <= sizeof(haddr.sll_addr));
+
+        memcpy(haddr.sll_addr, dest_haddr, halen);
+
+        n_buf = n_dhcp4_outgoing_get_raw(message, &buf);
+
+        r = packet_sendto_udp(sockfd, buf, n_buf, &len, src_paddr, &haddr, dest_paddr);
+        if (r < 0) {
+                if (r == -EAGAIN || r == -ENOBUFS)
+                        return N_DHCP4_E_DROPPED;
+                else if (r == -ENETDOWN || r == -ENXIO)
+                        return N_DHCP4_E_DOWN;
+                else
+                        return r;
+        } else if (len != n_buf) {
+                return N_DHCP4_E_DROPPED;
+        }
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_c_socket_packet_send() - XXX
+ */
+int n_dhcp4_c_socket_packet_send(int sockfd,
+                                 int ifindex,
+                                 const unsigned char *dest_haddr,
+                                 unsigned char halen,
+                                 NDhcp4Outgoing *message) {
+        struct sockaddr_in src_paddr = {
+                .sin_family = AF_INET,
+                .sin_port = htons(N_DHCP4_NETWORK_CLIENT_PORT),
+                .sin_addr = { INADDR_ANY },
+        };
+        struct sockaddr_in dest_paddr = {
+                .sin_family = AF_INET,
+                .sin_port = htons(N_DHCP4_NETWORK_SERVER_PORT),
+                .sin_addr = { INADDR_BROADCAST }
+        };
+
+        return n_dhcp4_socket_packet_send(sockfd,
+                                          ifindex,
+                                          &src_paddr,
+                                          dest_haddr,
+                                          halen,
+                                          &dest_paddr,
+                                          message);
+}
+
+/**
+ * n_dhcp4_c_socket_udp_send() - XXX
+ */
+int n_dhcp4_c_socket_udp_send(int sockfd,
+                              NDhcp4Outgoing *message) {
+        const void *buf;
+        size_t n_buf;
+        ssize_t len;
+
+        n_buf = n_dhcp4_outgoing_get_raw(message, &buf);
+
+        len = send(sockfd, buf, n_buf, 0);
+        if (len < 0) {
+                if (errno == EAGAIN || errno == ENOBUFS)
+                        return N_DHCP4_E_DROPPED;
+                else if (errno == ENETDOWN || errno == ENXIO)
+                        return N_DHCP4_E_DOWN;
+                else
+                        return -errno;
+        } else if ((size_t)len != n_buf)
+                return N_DHCP4_E_DROPPED;
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_c_socket_udp_broadcast() - XXX
+ */
+int n_dhcp4_c_socket_udp_broadcast(int sockfd, NDhcp4Outgoing *message) {
+        struct sockaddr_in sockaddr_dest = {
+                .sin_family = AF_INET,
+                .sin_port = htons(N_DHCP4_NETWORK_SERVER_PORT),
+                .sin_addr = { INADDR_BROADCAST },
+        };
+        const void *buf;
+        size_t n_buf;
+        ssize_t len;
+
+        n_buf = n_dhcp4_outgoing_get_raw(message, &buf);
+
+        len = sendto(sockfd,
+                     buf,
+                     n_buf,
+                     0,
+                     (struct sockaddr*)&sockaddr_dest,
+                     sizeof(sockaddr_dest));
+        if (len < 0) {
+                if (errno == EAGAIN || errno == ENOBUFS)
+                        return N_DHCP4_E_DROPPED;
+                else if (errno == ENETDOWN || errno == ENXIO)
+                        return N_DHCP4_E_DOWN;
+                else
+                        return -errno;
+        } else if ((size_t)len != n_buf)
+                return N_DHCP4_E_DROPPED;
+
+        return 0;
+}
+
+/**
+ * n_dhcp4_s_socket_packet_send() - XXX
+ */
+int n_dhcp4_s_socket_packet_send(int sockfd,
+                                 int ifindex,
+                                 const struct in_addr *src_inaddr,
+                                 const unsigned char *dest_haddr,
+                                 unsigned char halen,
+                                 const struct in_addr *dest_inaddr,
+                                 NDhcp4Outgoing *message) {
+        struct sockaddr_in src_paddr = {
+                .sin_family = AF_INET,
+                .sin_port = htons(N_DHCP4_NETWORK_SERVER_PORT),
+                .sin_addr = *src_inaddr,
+        };
+        struct sockaddr_in dest_paddr = {
+                .sin_family = AF_INET,
+                .sin_port = htons(N_DHCP4_NETWORK_CLIENT_PORT),
+                .sin_addr = *dest_inaddr,
+        };
+
+        return n_dhcp4_socket_packet_send(sockfd,
+                                          ifindex,
+                                          &src_paddr,
+                                          dest_haddr,
+                                          halen,
+                                          &dest_paddr,
+                                          message);
+}
+
+/**
+ * n_dhcp4_s_socket_udp_send() - XXX
+ */
+int n_dhcp4_s_socket_udp_send(int sockfd,
+                              const struct in_addr *inaddr_src,
+                              const struct in_addr *inaddr_dest,
+                              NDhcp4Outgoing *message) {
+        struct sockaddr_in sockaddr_dest = {
+                .sin_family = AF_INET,
+                .sin_port = htons(N_DHCP4_NETWORK_CLIENT_PORT),
+                .sin_addr = *inaddr_dest,
+        };
+        struct iovec iov = {};
+        union {
+               struct cmsghdr align; /* ensure correct stack alignment */
+               char buf[CMSG_SPACE(sizeof(struct in_pktinfo))];
+        } control = {};
+        struct in_pktinfo pktinfo = {
+                .ipi_spec_dst = *inaddr_src,
+        };
+        struct msghdr msg = {
+                .msg_name = (void*)&sockaddr_dest,
+                .msg_namelen = sizeof(sockaddr_dest),
+                .msg_iov = &iov,
+                .msg_iovlen = 1,
+                .msg_control = &control.buf,
+                .msg_controllen = sizeof(control.buf),
+        };
+        struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+        ssize_t len;
+
+        cmsg->cmsg_level = IPPROTO_IP;
+        cmsg->cmsg_type = IP_PKTINFO;
+        cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo));
+        memcpy(CMSG_DATA(cmsg), &pktinfo, sizeof(pktinfo));
+
+        iov.iov_len = n_dhcp4_outgoing_get_raw(message, (const void **)&iov.iov_base);
+
+        len = sendmsg(sockfd, &msg, 0);
+        if (len < 0) {
+                if (errno == EAGAIN || errno == ENOBUFS)
+                        return N_DHCP4_E_DROPPED;
+                else if (errno == ENETDOWN || errno == ENXIO)
+                        return N_DHCP4_E_DOWN;
+                else
+                        return -errno;
+        } else if ((size_t)len != iov.iov_len)
+                return N_DHCP4_E_DROPPED;
+
+        return 0;
+}
+
+int n_dhcp4_s_socket_udp_broadcast(int sockfd,
+                                   const struct in_addr *inaddr_src,
+                                   NDhcp4Outgoing *message) {
+        return n_dhcp4_s_socket_udp_send(sockfd,
+                                         inaddr_src,
+                                         &(const struct in_addr){INADDR_BROADCAST},
+                                         message);
+}
+
+int n_dhcp4_c_socket_packet_recv(int sockfd,
+                                 uint8_t *buf,
+                                 size_t n_buf,
+                                 NDhcp4Incoming **messagep) {
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL;
+        size_t len;
+        int r;
+
+        r = packet_recv_udp(sockfd, buf, n_buf, &len);
+        if (r < 0) {
+                if (r == -ENETDOWN)
+                        return N_DHCP4_E_DOWN;
+                else if (r == -EAGAIN)
+                        return N_DHCP4_E_AGAIN;
+                else
+                        return -errno;
+        } else if (len == 0) {
+                return N_DHCP4_E_MALFORMED;
+        }
+
+        r = n_dhcp4_incoming_new(&message, buf, len);
+        if (r)
+                return r;
+
+        *messagep = message;
+        message = NULL;
+        return 0;
+}
+
+static int n_dhcp4_socket_udp_recv(int sockfd,
+                                   uint8_t *buf,
+                                   size_t n_buf,
+                                   NDhcp4Incoming **messagep,
+                                   struct in_pktinfo *pktinfo) {
+        _c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL;
+        struct iovec iov = {
+                .iov_base = buf,
+                .iov_len = n_buf,
+        };
+        uint8_t cmsgbuf[CMSG_LEN(sizeof(struct in_pktinfo))];
+        struct msghdr msg = {
+                .msg_iov = &iov,
+                .msg_iovlen = 1,
+                .msg_control = cmsgbuf,
+                .msg_controllen = sizeof(cmsgbuf),
+        };
+        ssize_t len;
+        int r;
+
+        len = recvmsg(sockfd, &msg, MSG_TRUNC);
+        if (len < 0) {
+                if (errno == ENETDOWN)
+                        return N_DHCP4_E_DOWN;
+                else if (errno == EAGAIN)
+                        return N_DHCP4_E_AGAIN;
+                else
+                        return -errno;
+        } else if (len == 0 || (size_t)len > n_buf) {
+                return N_DHCP4_E_MALFORMED;
+        }
+
+        r = n_dhcp4_incoming_new(&message, buf, len);
+        if (r)
+                return r;
+
+        if (pktinfo) {
+                struct cmsghdr *cmsg;
+
+                cmsg = CMSG_FIRSTHDR(&msg);
+                c_assert(cmsg);
+                c_assert(cmsg->cmsg_level == IPPROTO_IP);
+                c_assert(cmsg->cmsg_type == IP_PKTINFO);
+                c_assert(cmsg->cmsg_len == CMSG_LEN(sizeof(struct in_pktinfo)));
+
+                memcpy(pktinfo, (void*)CMSG_DATA(cmsg), sizeof(struct in_pktinfo));
+        }
+
+        *messagep = message;
+        message = NULL;
+        return 0;
+}
+
+int n_dhcp4_c_socket_udp_recv(int sockfd,
+                              uint8_t *buf,
+                              size_t n_buf,
+                              NDhcp4Incoming **messagep) {
+        return n_dhcp4_socket_udp_recv(sockfd, buf, n_buf, messagep, NULL);
+}
+
+int n_dhcp4_s_socket_udp_recv(int sockfd,
+                              uint8_t *buf,
+                              size_t n_buf,
+                              NDhcp4Incoming **messagep,
+                              struct sockaddr_in *dest) {
+        struct in_pktinfo pktinfo = {};
+        int r;
+
+        r = n_dhcp4_socket_udp_recv(sockfd, buf, n_buf, messagep, &pktinfo);
+        if (r)
+                return r;
+
+        if (dest) {
+                dest->sin_family = AF_INET;
+                dest->sin_port = htons(N_DHCP4_NETWORK_SERVER_PORT);
+                dest->sin_addr.s_addr = pktinfo.ipi_addr.s_addr;
+        }
+
+        return 0;
+}
diff --git a/shared/n-dhcp4/src/n-dhcp4.h b/shared/n-dhcp4/src/n-dhcp4.h
new file mode 100644
index 00000000..58a3cb80
--- /dev/null
+++ b/shared/n-dhcp4/src/n-dhcp4.h
@@ -0,0 +1,286 @@
+#pragma once
+
+/*
+ * Dynamic Host Configuration Protocol for IPv4
+ *
+ * This is the public header of the n-dhcp4 library, implementing IPv4 Dynamic
+ * Host Configuration Protocol as described in RFC-2132. This header defines
+ * the public API and all entry points of n-dhcp4.
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <inttypes.h>
+#include <netinet/in.h>
+#include <stdbool.h>
+#include <stdlib.h>
+
+typedef struct NDhcp4Client NDhcp4Client;
+typedef struct NDhcp4ClientConfig NDhcp4ClientConfig;
+typedef struct NDhcp4ClientEvent NDhcp4ClientEvent;
+typedef struct NDhcp4ClientLease NDhcp4ClientLease;
+typedef struct NDhcp4ClientProbe NDhcp4ClientProbe;
+typedef struct NDhcp4ClientProbeConfig NDhcp4ClientProbeConfig;
+typedef struct NDhcp4Server NDhcp4Server;
+typedef struct NDhcp4ServerConfig NDhcp4ServerConfig;
+typedef struct NDhcp4ServerEvent NDhcp4ServerEvent;
+typedef struct NDhcp4ServerIp NDhcp4ServerIp;
+typedef struct NDhcp4ServerLease NDhcp4ServerLease;
+
+#define N_DHCP4_CLIENT_START_DELAY_RFC2131 (UINT64_C(9000))
+
+enum {
+        _N_DHCP4_E_SUCCESS,
+
+        N_DHCP4_E_PREEMPTED,
+        N_DHCP4_E_INTERNAL,
+
+        N_DHCP4_E_INVALID_IFINDEX,
+        N_DHCP4_E_INVALID_TRANSPORT,
+        N_DHCP4_E_INVALID_ADDRESS,
+        N_DHCP4_E_INVALID_CLIENT_ID,
+        N_DHCP4_E_DUPLICATE_OPTION,
+        N_DHCP4_E_UNSET,
+
+        _N_DHCP4_E_N,
+};
+
+enum {
+        N_DHCP4_TRANSPORT_ETHERNET,
+        N_DHCP4_TRANSPORT_INFINIBAND,
+        _N_DHCP4_TRANSPORT_N,
+};
+
+enum {
+        N_DHCP4_CLIENT_EVENT_DOWN,
+        N_DHCP4_CLIENT_EVENT_OFFER,
+        N_DHCP4_CLIENT_EVENT_GRANTED,
+        N_DHCP4_CLIENT_EVENT_RETRACTED,
+        N_DHCP4_CLIENT_EVENT_EXTENDED,
+        N_DHCP4_CLIENT_EVENT_EXPIRED,
+        N_DHCP4_CLIENT_EVENT_CANCELLED,
+        _N_DHCP4_CLIENT_EVENT_N,
+};
+
+enum {
+        N_DHCP4_SERVER_EVENT_DOWN,
+        N_DHCP4_SERVER_EVENT_DISCOVER,
+        N_DHCP4_SERVER_EVENT_REQUEST,
+        N_DHCP4_SERVER_EVENT_RENEW,
+        N_DHCP4_SERVER_EVENT_DECLINE,
+        N_DHCP4_SERVER_EVENT_RELEASE,
+        _N_DHCP4_SERVER_EVENT_N,
+};
+
+struct NDhcp4ClientEvent {
+        unsigned int event;
+        union {
+                struct {
+                } down;
+                struct {
+                        NDhcp4ClientProbe *probe;
+                        NDhcp4ClientLease *lease;
+                } offer, granted, extended;
+                struct {
+                        NDhcp4ClientProbe *probe;
+                } retracted, expired, cancelled;
+        };
+};
+
+struct NDhcp4ServerEvent {
+        unsigned int event;
+        union {
+                struct {
+                } down;
+                struct {
+                        NDhcp4ServerLease *lease;
+                } discover, request, decline, release;
+        };
+};
+
+/* client configs */
+
+int n_dhcp4_client_config_new(NDhcp4ClientConfig **configp);
+NDhcp4ClientConfig *n_dhcp4_client_config_free(NDhcp4ClientConfig *config);
+
+void n_dhcp4_client_config_set_ifindex(NDhcp4ClientConfig *config, int ifindex);
+void n_dhcp4_client_config_set_transport(NDhcp4ClientConfig *config, unsigned int transport);
+void n_dhcp4_client_config_set_request_broadcast(NDhcp4ClientConfig *config, bool request_broadcast);
+void n_dhcp4_client_config_set_mac(NDhcp4ClientConfig *config, const uint8_t *mac, size_t n_mac);
+void n_dhcp4_client_config_set_broadcast_mac(NDhcp4ClientConfig *config, const uint8_t *mac, size_t n_mac);
+int n_dhcp4_client_config_set_client_id(NDhcp4ClientConfig *config, const uint8_t *id, size_t n_id);
+
+/* client-probe configs */
+
+int n_dhcp4_client_probe_config_new(NDhcp4ClientProbeConfig **configp);
+NDhcp4ClientProbeConfig *n_dhcp4_client_probe_config_free(NDhcp4ClientProbeConfig *config);
+
+void n_dhcp4_client_probe_config_set_inform_only(NDhcp4ClientProbeConfig *config, bool inform_only);
+void n_dhcp4_client_probe_config_set_init_reboot(NDhcp4ClientProbeConfig *config, bool init_reboot);
+void n_dhcp4_client_probe_config_set_requested_ip(NDhcp4ClientProbeConfig *config, struct in_addr ip);
+void n_dhcp4_client_probe_config_set_start_delay(NDhcp4ClientProbeConfig *config, uint64_t msecs);
+void n_dhcp4_client_probe_config_request_option(NDhcp4ClientProbeConfig *config, uint8_t option);
+int n_dhcp4_client_probe_config_append_option(NDhcp4ClientProbeConfig *config,
+                                              uint8_t option,
+                                              const void *data,
+                                              uint8_t n_data);
+
+/* clients */
+
+int n_dhcp4_client_new(NDhcp4Client **clientp, NDhcp4ClientConfig *config);
+NDhcp4Client *n_dhcp4_client_ref(NDhcp4Client *client);
+NDhcp4Client *n_dhcp4_client_unref(NDhcp4Client *client);
+
+void n_dhcp4_client_get_fd(NDhcp4Client *client, int *fdp);
+int n_dhcp4_client_dispatch(NDhcp4Client *client);
+int n_dhcp4_client_pop_event(NDhcp4Client *client, NDhcp4ClientEvent **eventp);
+
+int n_dhcp4_client_update_mtu(NDhcp4Client *client, uint16_t mtu);
+
+int n_dhcp4_client_probe(NDhcp4Client *client,
+                         NDhcp4ClientProbe **probep,
+                         NDhcp4ClientProbeConfig *config);
+
+/* client probes */
+
+NDhcp4ClientProbe *n_dhcp4_client_probe_free(NDhcp4ClientProbe *probe);
+
+void n_dhcp4_client_probe_set_userdata(NDhcp4ClientProbe *probe, void *userdata);
+void n_dhcp4_client_probe_get_userdata(NDhcp4ClientProbe *probe, void **userdatap);
+
+/* client leases */
+
+NDhcp4ClientLease *n_dhcp4_client_lease_ref(NDhcp4ClientLease *lease);
+NDhcp4ClientLease *n_dhcp4_client_lease_unref(NDhcp4ClientLease *lease);
+
+void n_dhcp4_client_lease_get_yiaddr(NDhcp4ClientLease *lease, struct in_addr *yiaddr);
+void n_dhcp4_client_lease_get_lifetime(NDhcp4ClientLease *lease, uint64_t *ns_lifetimep);
+int n_dhcp4_client_lease_query(NDhcp4ClientLease *lease, uint8_t option, uint8_t **datap, size_t *n_datap);
+
+int n_dhcp4_client_lease_select(NDhcp4ClientLease *lease);
+int n_dhcp4_client_lease_accept(NDhcp4ClientLease *lease);
+int n_dhcp4_client_lease_decline(NDhcp4ClientLease *lease, const char *error);
+
+/* server configs */
+
+int n_dhcp4_server_config_new(NDhcp4ServerConfig **configp);
+NDhcp4ServerConfig *n_dhcp4_server_config_free(NDhcp4ServerConfig *config);
+
+void n_dhcp4_server_config_set_ifindex(NDhcp4ServerConfig *config, int ifindex);
+
+/* servers */
+
+int n_dhcp4_server_new(NDhcp4Server **serverp, NDhcp4ServerConfig *config);
+NDhcp4Server *n_dhcp4_server_ref(NDhcp4Server *server);
+NDhcp4Server *n_dhcp4_server_unref(NDhcp4Server *server);
+
+void n_dhcp4_server_get_fd(NDhcp4Server *server, int *fdp);
+int n_dhcp4_server_dispatch(NDhcp4Server *server);
+int n_dhcp4_server_pop_event(NDhcp4Server *server, NDhcp4ServerEvent **eventp);
+
+int n_dhcp4_server_add_ip(NDhcp4Server *server, NDhcp4ServerIp **ipp, struct in_addr ip);
+
+/* server ip addresses */
+
+NDhcp4ServerIp *n_dhcp4_server_ip_free(NDhcp4ServerIp *ip);
+
+/* server leases */
+
+NDhcp4ServerLease *n_dhcp4_server_lease_ref(NDhcp4ServerLease *lease);
+NDhcp4ServerLease *n_dhcp4_server_lease_unref(NDhcp4ServerLease *lease);
+
+int n_dhcp4_server_lease_query(NDhcp4ServerLease *lease, uint8_t option, uint8_t **datap, size_t *n_datap);
+int n_dhcp4_server_lease_append(NDhcp4ServerLease *lease, uint8_t option, uint8_t *data, size_t n_data);
+
+int n_dhcp4_server_lease_offer(NDhcp4ServerLease *lease);
+int n_dhcp4_server_lease_ack(NDhcp4ServerLease *lease);
+int n_dhcp4_server_lease_nack(NDhcp4ServerLease *lease);
+
+/* inline helpers */
+
+static inline void n_dhcp4_client_config_freep(NDhcp4ClientConfig **p) {
+        if (*p)
+                n_dhcp4_client_config_free(*p);
+}
+
+static inline void n_dhcp4_client_config_freev(NDhcp4ClientConfig *p) {
+        n_dhcp4_client_config_free(p);
+}
+
+static inline void n_dhcp4_client_probe_config_freep(NDhcp4ClientProbeConfig **p) {
+        if (*p)
+                n_dhcp4_client_probe_config_free(*p);
+}
+
+static inline void n_dhcp4_client_probe_config_freev(NDhcp4ClientProbeConfig *p) {
+        n_dhcp4_client_probe_config_free(p);
+}
+
+static inline void n_dhcp4_client_unrefp(NDhcp4Client **p) {
+        if (*p)
+                n_dhcp4_client_unref(*p);
+}
+
+static inline void n_dhcp4_client_unrefv(NDhcp4Client *p) {
+        n_dhcp4_client_unref(p);
+}
+
+static inline void n_dhcp4_client_probe_freep(NDhcp4ClientProbe **p) {
+        if (*p)
+                n_dhcp4_client_probe_free(*p);
+}
+
+static inline void n_dhcp4_client_probe_freev(NDhcp4ClientProbe *p) {
+        n_dhcp4_client_probe_free(p);
+}
+
+static inline void n_dhcp4_client_lease_unrefp(NDhcp4ClientLease **p) {
+        if (*p)
+                n_dhcp4_client_lease_unref(*p);
+}
+
+static inline void n_dhcp4_client_lease_unrefv(NDhcp4ClientLease *p) {
+        n_dhcp4_client_lease_unref(p);
+}
+
+static inline void n_dhcp4_server_config_freep(NDhcp4ServerConfig **p) {
+        if (*p)
+                n_dhcp4_server_config_free(*p);
+}
+
+static inline void n_dhcp4_server_config_freev(NDhcp4ServerConfig *p) {
+        n_dhcp4_server_config_free(p);
+}
+
+static inline void n_dhcp4_server_unrefp(NDhcp4Server **p) {
+        if (*p)
+                n_dhcp4_server_unref(*p);
+}
+
+static inline void n_dhcp4_server_unrefv(NDhcp4Server *p) {
+        n_dhcp4_server_unref(p);
+}
+
+static inline void n_dhcp4_server_ip_freep(NDhcp4ServerIp **p) {
+        if (*p)
+                n_dhcp4_server_ip_free(*p);
+}
+
+static inline void n_dhcp4_server_ip_freev(NDhcp4ServerIp *p) {
+        n_dhcp4_server_ip_free(p);
+}
+
+static inline void n_dhcp4_server_lease_unrefp(NDhcp4ServerLease **p) {
+        if (*p)
+                n_dhcp4_server_lease_unref(*p);
+}
+
+static inline void n_dhcp4_server_lease_unrefv(NDhcp4ServerLease *p) {
+        n_dhcp4_server_lease_unref(p);
+}
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/shared/n-dhcp4/src/util/packet.c b/shared/n-dhcp4/src/util/packet.c
new file mode 100644
index 00000000..38cb399d
--- /dev/null
+++ b/shared/n-dhcp4/src/util/packet.c
@@ -0,0 +1,456 @@
+/*
+ * Packet Sockets
+ */
+
+#include <assert.h>
+#include <c-stdaux.h>
+#include <endian.h>
+#include <errno.h>
+#include <linux/filter.h>
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
+#include <linux/udp.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include "packet.h"
+
+/**
+ * packet_internet_checksum() - compute the internet checksum
+ * @data:               the data to checksum
+ * @size:               the length of @data in bytes
+ *
+ * Computes the internet checksum for a given blob according to RFC1071.
+ *
+ * The internet checksum is the one's complement of the one's complement sum of
+ * the 16-bit words of the data, padded with zero-bytes if the data does not
+ * end on a 16-bit boundary.
+ *
+ * Return: Checksum is returned.
+ */
+uint16_t packet_internet_checksum(const uint8_t *data, size_t size) {
+        uint64_t acc = 0;
+        uint32_t local;
+
+        while (size >= sizeof(local)) {
+                memcpy(&local, data, sizeof(local));
+                acc += local;
+
+                data += sizeof(local);
+                size -= sizeof(local);
+        }
+
+        if (size) {
+                local = 0;
+                memcpy(&local, data, size);
+                acc += local;
+        }
+
+        while (acc >> 16)
+                acc = (acc & 0xffff) + (acc >> 16);
+
+        return ~acc;
+}
+
+/**
+ * packet_internet_checksum_udp() - compute the internet checkum for UDP packets
+ * @src_addr:           source IP address
+ * @dst_addr:           destination IP address
+ * @src_port:           source port
+ * @dst_port:           destination port
+ * @data:               payload
+ * @size:               length of payload in bytes
+ * @checksum:           current checksum, or 0
+ *
+ * Computes the internet checksum for a UDP packet, given the relevant IP and
+ * UDP header fields.
+ *
+ * Note that since a UDP packet contains the checksum itself, the resulting
+ * checksum will always be 0 (this fact is used to verify that a UDP packet is
+ * valid).
+ * Inversely, when calculating the checksum for outgoing packets, you have to
+ * specify 0 as @checksum, and this function will return the checksum for the
+ * caller to use for the packet. In this case, though, the caller must check
+ * whether the returned checksum might coincidentally be 0, in which case it
+ * must be flipped to -1 (0xffff), since 0 is not allowed as checksum in UDP
+ * packets, and -1 is arithmetically equivalent in the checksum calculation.
+ *
+ * Return: Checksum is returned.
+ */
+uint16_t packet_internet_checksum_udp(const struct in_addr *src_addr,
+                                      const struct in_addr *dst_addr,
+                                      uint16_t src_port,
+                                      uint16_t dst_port,
+                                      const uint8_t *data,
+                                      size_t size,
+                                      uint16_t checksum) {
+        struct {
+                uint32_t src;
+                uint32_t dst;
+                uint8_t _zeros;
+                uint8_t protocol;
+                uint16_t length;
+                struct udphdr udp;
+        } _c_packed_ udp_phdr = {
+                .src = src_addr->s_addr,
+                .dst = dst_addr->s_addr,
+                .protocol = IPPROTO_UDP,
+                .length = htons(sizeof(struct udphdr) + size),
+                .udp = {
+                        .source = htons(src_port),
+                        .dest = htons(dst_port),
+                        .len = htons(sizeof(struct udphdr) + size),
+                        .check = checksum,
+                },
+        };
+        const uint8_t *iter;
+        uint64_t acc = 0;
+        uint32_t local;
+
+        _Static_assert(!(sizeof(udp_phdr) % sizeof(local)),
+                       "UDP header structure size is not a multiple of 4");
+
+        for (iter = (const uint8_t *)&udp_phdr;
+             iter < (const uint8_t *)(&udp_phdr + 1);
+             iter += sizeof(local)) {
+                memcpy(&local, iter, sizeof(local));
+                acc += local;
+        }
+
+        while (size >= sizeof(local)) {
+                memcpy(&local, data, sizeof(local));
+                acc += local;
+
+                data += sizeof(local);
+                size -= sizeof(local);
+        }
+
+        if (size) {
+                local = 0;
+                memcpy(&local, data, size);
+                acc += local;
+        }
+
+        while (acc >> 16)
+                acc = (acc & 0xffff) + (acc >> 16);
+
+        return ~acc;
+}
+
+/**
+ * packet_sendto_udp() - send UDP packet on AF_PACKET socket
+ * @sockfd:             AF_PACKET/SOCK_DGRAM socket
+ * @buf:                payload
+ * @n_buf:              length of payload in bytes
+ * @n_transmittedp:     output argument for number of transmitted bytes
+ * @src_paddr:          source protocol address, see ip(7)
+ * @dest_haddr:         destination hardware address, see packet(7)
+ * @dest_paddr:         destination protocol address, see ip(7)
+ *
+ * Sends an UDP packet on a AF_PACKET socket directly to a hardware
+ * address. The difference between this and sendto() on an AF_INET
+ * socket is that no routing is performed, so the packet is delivered
+ * even if the destination IP is not yet configured on the destination
+ * host.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int packet_sendto_udp(int sockfd,
+                      const void *buf,
+                      size_t n_buf,
+                      size_t *n_transmittedp,
+                      const struct sockaddr_in *src_paddr,
+                      const struct packet_sockaddr_ll *dest_haddr,
+                      const struct sockaddr_in *dest_paddr) {
+        struct iphdr ip_hdr = {
+                .version = IPVERSION,
+                .ihl = sizeof(ip_hdr) / 4, /* Length of header in multiples of four bytes */
+                .tos = IPTOS_CLASS_CS6, /* Class Selector for network control */
+                .tot_len = htons(sizeof(struct iphdr) + sizeof(struct udphdr) + n_buf),
+                .frag_off = htons(IP_DF), /* Do not fragment */
+                .ttl = IPDEFTTL,
+                .protocol = IPPROTO_UDP,
+                .saddr = src_paddr->sin_addr.s_addr,
+                .daddr = dest_paddr->sin_addr.s_addr,
+        };
+        struct udphdr udp_hdr = {
+                .source = src_paddr->sin_port,
+                .dest = dest_paddr->sin_port,
+                .len = htons(sizeof(udp_hdr) + n_buf),
+        };
+        struct iovec iov[3] = {
+                {
+                        .iov_base = &ip_hdr,
+                        .iov_len = sizeof(ip_hdr),
+                },
+                {
+                        .iov_base = &udp_hdr,
+                        .iov_len = sizeof(udp_hdr),
+                },
+                {
+                        .iov_base = (void *)buf,
+                        .iov_len = n_buf,
+                },
+        };
+        struct msghdr msg = {
+                .msg_name = (void*)dest_haddr,
+                .msg_namelen = sizeof(*dest_haddr),
+                .msg_iov = iov,
+                .msg_iovlen = sizeof(iov) / sizeof(iov[0]),
+        };
+        ssize_t pktlen;
+
+        ip_hdr.check = packet_internet_checksum((void*)&ip_hdr, sizeof(ip_hdr));
+        udp_hdr.check = packet_internet_checksum_udp(&src_paddr->sin_addr,
+                                                     &dest_paddr->sin_addr,
+                                                     ntohs(src_paddr->sin_port),
+                                                     ntohs(dest_paddr->sin_port),
+                                                     buf,
+                                                     n_buf,
+                                                     0);
+
+        /*
+         * 0x0000 and 0xffff are equivalent for computing the UDP checksum,
+         * but 0x0000 is reserved in UDP headers, to mean that the checksum is
+         * not set and should be ignored by the receiver. Hence, flip it to
+         * 0xffff in that case.
+         */
+        udp_hdr.check = udp_hdr.check ?: 0xffff;
+
+        pktlen = sendmsg(sockfd, &msg, 0);
+        if (pktlen < 0)
+                return -errno;
+
+        /*
+         * Kernel never truncates. Worst case, we get -EMSGSIZE. Kernel *might*
+         * prepend VNET headers, in which case a bigger length than sent is
+         * returned.
+         * Lets assert on this, and then return to the caller the proportion of
+         * its own buffer that we sent (which is always exactly the requested
+         * size).
+         */
+        c_assert((size_t)pktlen >= sizeof(ip_hdr) + sizeof(udp_hdr) + n_buf);
+        *n_transmittedp = n_buf;
+        return 0;
+}
+
+/**
+ * packet_recvfrom_upd() - receive UDP packet from AF_PACKET socket
+ * @sockfd:             AF_PACKET/SOCK_DGRAM socket
+ * @buf:                buffor for payload
+ * @n_buf:              max length of payload in bytes
+ * @n_transmittedp:     output argument for number transmitted bytes
+ * @src:                return argumnet for source address, or NULL, see ip(7)
+ *
+ * Receives an UDP packet on a AF_PACKET socket. The difference between
+ * this and recvfrom() on an AF_INET socket is that the packet will be
+ * received even if the destination IP address has not been configured
+ * on the interface.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int packet_recvfrom_udp(int sockfd,
+                        void *buf,
+                        size_t n_buf,
+                        size_t *n_transmittedp,
+                        struct sockaddr_in *src) {
+        union {
+                struct iphdr hdr;
+                /*
+                 * Maximum IP-header length is 15 * 4, since it is specified in
+                 * the `ihl` field, which is four bits and interpreted as
+                 * factor of 4. So maximum `ihl` value is `(2^4 - 1) * 4`.
+                 */
+                uint8_t data[15 * 4];
+        } ip_hdr;
+        struct udphdr udp_hdr;
+        struct iovec iov[3] = {
+                {
+                        .iov_base = &ip_hdr,
+                },
+                {
+                        .iov_base = &udp_hdr,
+                        .iov_len = sizeof(udp_hdr),
+                },
+                {
+                        .iov_base = buf,
+                        .iov_len = n_buf,
+                },
+        };
+        uint8_t cmsgbuf[CMSG_LEN(sizeof(struct tpacket_auxdata))];
+        struct msghdr msg = {
+                .msg_iov = iov,
+                .msg_iovlen = sizeof(iov) / sizeof(iov[0]),
+                .msg_control = cmsgbuf,
+                .msg_controllen = sizeof(cmsgbuf),
+        };
+        struct cmsghdr *cmsg;
+        bool checksum = true;
+        ssize_t pktlen;
+        size_t hdrlen;
+
+        /* Peek packet to obtain the real IP header length */
+        pktlen = recv(sockfd, &ip_hdr.hdr, sizeof(ip_hdr.hdr), MSG_PEEK);
+        if (pktlen < 0)
+                return -errno;
+
+        if ((size_t)pktlen < sizeof(ip_hdr.hdr)) {
+                /*
+                 * Received packet is smaller than the minimal IP header length,
+                 * discard it.
+                 */
+                recv(sockfd, NULL, 0, 0);
+                *n_transmittedp = 0;
+                return 0;
+        }
+
+        if (ip_hdr.hdr.version != IPVERSION) {
+                /*
+                 * This is not an IPv4 packet, discard it.
+                 */
+                recv(sockfd, NULL, 0, 0);
+                *n_transmittedp = 0;
+                return 0;
+        }
+
+        hdrlen = ip_hdr.hdr.ihl * 4;
+        if (hdrlen < sizeof(ip_hdr.hdr)) {
+                /*
+                 * The length given in the header is smaller than the minimum
+                 * header length, discard the packet.
+                 */
+                recv(sockfd, NULL, 0, 0);
+                *n_transmittedp = 0;
+                return 0;
+        }
+
+        /*
+         * Now that we know the ip-header length, we can prepare the iovec to
+         * read the entire packet into the correct buffers.
+         */
+        iov[0].iov_len = hdrlen;
+        pktlen = recvmsg(sockfd, &msg, 0);
+        if (pktlen < 0)
+                return -errno;
+
+        cmsg = CMSG_FIRSTHDR(&msg);
+        if (cmsg) {
+                if (cmsg->cmsg_level == SOL_PACKET &&
+                    cmsg->cmsg_type == PACKET_AUXDATA &&
+                    cmsg->cmsg_len == CMSG_LEN(sizeof(struct tpacket_auxdata))) {
+                        struct tpacket_auxdata *aux = (void *)CMSG_DATA(cmsg);
+                        checksum = !(aux->tp_status & TP_STATUS_CSUMNOTREADY);
+                }
+        }
+
+        if (ntohs(ip_hdr.hdr.tot_len) > pktlen) {
+                /*
+                 * The IP-packet is bigger than the chunk returned by the
+                 * kernel. So either the packet is corrupt, or our caller
+                 * provided too small a buffer. In both cases, we simply drop
+                 * the packet.
+                 */
+                *n_transmittedp = 0;
+                return 0;
+        }
+
+        /* Truncate trailing garbage. */
+        pktlen = ntohs(ip_hdr.hdr.tot_len);
+
+        if ((size_t)pktlen < hdrlen + sizeof(udp_hdr)) {
+                /*
+                 * The packet is too small to even contain an entire UDP
+                 * header, so discard it entirely.
+                 */
+                *n_transmittedp = 0;
+                return 0;
+        } else if ((size_t)pktlen < hdrlen + ntohs(udp_hdr.len)) {
+                /*
+                 * The UDP header specified a longer length than the returned
+                 * packet, so discard it entirely.
+                 */
+                *n_transmittedp = 0;
+                return 0;
+        }
+
+        /*
+         * Make @pktlen the length of the packet payload, without IP/UDP
+         * headers, since that is what the caller is interested in.
+         */
+        pktlen = ntohs(udp_hdr.len) - sizeof(struct udphdr);
+
+        /* IP */
+
+        if (ip_hdr.hdr.protocol != IPPROTO_UDP) {
+                *n_transmittedp = 0;
+                return 0; /* not a UDP packet, discard it */
+        } else if (ip_hdr.hdr.frag_off & htons(IP_MF | IP_OFFMASK)) {
+                *n_transmittedp = 0;
+                return 0; /* fragmented packet, discard it */
+        } else if (checksum && packet_internet_checksum(ip_hdr.data, hdrlen)) {
+                *n_transmittedp = 0;
+                return 0; /* invalid checksum, discard it */
+        }
+
+        /* UDP */
+
+        if (checksum && udp_hdr.check) {
+                /*
+                 * Computing the checksum of a packet that has the checksum set
+                 * must yield 0. If it does not yield 0, the packet is invalid,
+                 * in which case we discard it.
+                 */
+               if (packet_internet_checksum_udp(&(struct in_addr){ ip_hdr.hdr.saddr },
+                                                &(struct in_addr){ ip_hdr.hdr.daddr },
+                                                ntohs(udp_hdr.source),
+                                                ntohs(udp_hdr.dest),
+                                                buf,
+                                                pktlen,
+                                                udp_hdr.check)) {
+                        *n_transmittedp = 0;
+                        return 0;
+               }
+        }
+
+        if (src) {
+                src->sin_family = AF_INET;
+                src->sin_addr.s_addr = ip_hdr.hdr.saddr;
+                src->sin_port = udp_hdr.source;
+        }
+
+        /* Return length of UDP payload (i.e., data written to @buf). */
+        *n_transmittedp = pktlen;
+        return 0;
+}
+
+/**
+ * packet_shutdown() - shutdown socket for future receive operations
+ * @sockfd:     socket
+ *
+ * Partially emulates `shutdown(sockfd, SHUT_RD)`, in the sense that no
+ * further packets may be queued on the socket. All packets that are
+ * already queued will still be delivered, but once -EAGAIN is returned
+ * we are guaranteed never to be able to read more packets in the future.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+int packet_shutdown(int sockfd) {
+        struct sock_filter filter[] = {
+                BPF_STMT(BPF_RET + BPF_K, 0), /* discard all packets */
+        };
+        struct sock_fprog fprog = {
+                .filter = filter,
+                .len = sizeof(filter) / sizeof(filter[0]),
+        };
+        int r;
+
+        r = setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &fprog, sizeof(fprog));
+        if (r < 0)
+                return -errno;
+
+        return 0;
+}
diff --git a/shared/n-dhcp4/src/util/packet.h b/shared/n-dhcp4/src/util/packet.h
new file mode 100644
index 00000000..98dabf7f
--- /dev/null
+++ b/shared/n-dhcp4/src/util/packet.h
@@ -0,0 +1,61 @@
+#pragma once
+
+/*
+ * Packet Sockets
+ */
+
+#include <c-stdaux.h>
+#include <inttypes.h>
+#include <linux/if_packet.h>
+#include <netinet/in.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+/*
+ * `struct sockaddr_ll` is too small to fit the Infiniband hardware address.
+ * Introduce `struct packet_sockaddr_ll` which is the same as the original,
+ * except the `sl_addr` field is extended to fit all the supported hardware
+ * addresses.
+ */
+struct packet_sockaddr_ll {
+        unsigned short  sll_family;
+        __be16          sll_protocol;
+        int             sll_ifindex;
+        unsigned short  sll_hatype;
+        unsigned char   sll_pkttype;
+        unsigned char   sll_halen;
+        unsigned char   sll_addr[32]; /* MAX_ADDR_LEN */
+};
+
+uint16_t packet_internet_checksum(const uint8_t *data, size_t len);
+uint16_t packet_internet_checksum_udp(const struct in_addr *src_addr,
+                                      const struct in_addr *dst_addr,
+                                      uint16_t src_port,
+                                      uint16_t dst_port,
+                                      const uint8_t *data,
+                                      size_t size,
+                                      uint16_t checksum);
+
+int packet_sendto_udp(int sockfd,
+                      const void *buf,
+                      size_t n_buf,
+                      size_t *n_transmittedp,
+                      const struct sockaddr_in *src_paddr,
+                      const struct packet_sockaddr_ll *dest_haddr,
+                      const struct sockaddr_in *dest_paddr);
+int packet_recvfrom_udp(int sockfd,
+                        void *buf,
+                        size_t n_buf,
+                        size_t *n_transmittedp,
+                        struct sockaddr_in *src);
+
+int packet_shutdown(int sockfd);
+
+/* inline helpers */
+
+static inline int packet_recv_udp(int sockfd,
+                                  void *buf,
+                                  size_t n_buf,
+                                  size_t *n_transmittedp) {
+        return packet_recvfrom_udp(sockfd, buf, n_buf, n_transmittedp, NULL);
+}
diff --git a/shared/n-dhcp4/src/util/socket.c b/shared/n-dhcp4/src/util/socket.c
new file mode 100644
index 00000000..c25f76fd
--- /dev/null
+++ b/shared/n-dhcp4/src/util/socket.c
@@ -0,0 +1,121 @@
+/*
+ * Socket Utilities
+ */
+
+#include <assert.h>
+#include <c-stdaux.h>
+#include <errno.h>
+#include <net/if.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include "socket.h"
+
+/**
+ * socket_SIOCGIFNAME() - resolve an ifindex to an ifname
+ * @socket:                     socket to operate on
+ * @ifindex:                    index of network interface to resolve
+ * @ifname:                     buffer to store resolved name
+ *
+ * This uses the SIOCGIFNAME ioctl to resolve an ifindex to an ifname. The
+ * buffer provided in @ifnamep must be at least IFNAMSIZ bytes in size. The
+ * maximum ifname length is IFNAMSIZ-1, and this function always
+ * zero-terminates the result.
+ *
+ * This function is similar to if_indextoname(3) provided by glibc, but it
+ * allows to specify the target socket explicitly. This allows the caller to
+ * control the target network-namespace, rather than relying on the network
+ * namespace of the running process.
+ *
+ * Return: 0 on success, negative kernel error code on failure.
+ */
+int socket_SIOCGIFNAME(int socket, int ifindex, char (*ifnamep)[IFNAMSIZ]) {
+        struct ifreq req = { .ifr_ifindex = ifindex };
+        int r;
+
+        r = ioctl(socket, SIOCGIFNAME, &req);
+        if (r < 0)
+                return -errno;
+
+        /*
+         * The linux kernel guarantees that an interface name is always
+         * zero-terminated, and it always fully fits into IFNAMSIZ bytes,
+         * including the zero-terminator.
+         */
+        memcpy(ifnamep, req.ifr_name, IFNAMSIZ);
+        return 0;
+}
+
+/**
+ * socket_bind_if() - bind socket to a network interface
+ * @socket:                     socket to operate on
+ * @ifindex:                    index of network interface to bind to, or 0
+ *
+ * This binds the socket given via @socket to the network interface specified
+ * via @ifindex. It uses the underlying SO_BINDTODEVICE ioctl of the linux
+ * kernel. However, if available, if prefers the newer SO_BINDTOIFINDEX ioctl,
+ * which avoids resolving the interface name temporarily, and thus does not
+ * suffer from a race-condition.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int socket_bind_if(int socket, int ifindex) {
+        char ifname[IFNAMSIZ] = {};
+        int r;
+
+        c_assert(ifindex >= 0);
+
+        /*
+         * We first try the newer SO_BINDTOIFINDEX. If it is not available on
+         * the running kernel, we fall back to SO_BINDTODEVICE. This, however,
+         * requires us to first resolve the ifindex to an ifname. Note that
+         * this is racy, since the device name might theoretically change
+         * asynchronously.
+         *
+         * Using 0 as ifindex will remove the device-binding. For
+         * SO_BINDTOIFINDEX we simply pass-through the 0 to the kernel, which
+         * recognizes this correctly. For SO_BINDTODEVICE we pass the empty
+         * string, which the kernel recognizes as a request to remove the
+         * binding.
+         *
+         * The commit introducing SO_BINDTOIFINDEX first appeared in linux-5.1:
+         *
+         *     commit f5dd3d0c9638a9d9a02b5964c4ad636f06cf7e2c
+         *     Author: David Herrmann <dh.herrmann@gmail.com>
+         *     Date:   Tue Jan 15 14:42:14 2019 +0100
+         *
+         *         net: introduce SO_BINDTOIFINDEX sockopt
+         *
+         * In older kernels, setsockopt(2) is guaranteed to return ENOPROTOOPT
+         * for this ioctl.
+         */
+
+#ifdef SO_BINDTOIFINDEX
+        r = setsockopt(socket,
+                       SOL_SOCKET,
+                       SO_BINDTOIFINDEX,
+                       &ifindex,
+                       sizeof(ifindex));
+        if (r >= 0)
+                return 0;
+        else if (errno != ENOPROTOOPT)
+                return -errno;
+#endif /* SO_BINDTOIFINDEX */
+
+        if (ifindex > 0) {
+                r = socket_SIOCGIFNAME(socket, ifindex, &ifname);
+                if (r)
+                        return r;
+        }
+
+        r = setsockopt(socket,
+                       SOL_SOCKET,
+                       SO_BINDTODEVICE,
+                       ifname,
+                       strlen(ifname));
+        if (r < 0)
+                return -errno;
+
+        return 0;
+}
diff --git a/shared/n-dhcp4/src/util/socket.h b/shared/n-dhcp4/src/util/socket.h
new file mode 100644
index 00000000..b5ecf2c2
--- /dev/null
+++ b/shared/n-dhcp4/src/util/socket.h
@@ -0,0 +1,11 @@
+#pragma once
+
+/*
+ * Socket Utilities
+ */
+
+#include <c-stdaux.h>
+#include <stdlib.h>
+
+int socket_SIOCGIFNAME(int socket, int ifindex, char (*ifnamep)[IFNAMSIZ]);
+int socket_bind_if(int socket, int ifindex);
diff --git a/shared/nm-default.h b/shared/nm-default.h
index 54e99167..16a756c2 100644
--- a/shared/nm-default.h
+++ b/shared/nm-default.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
@@ -30,8 +29,6 @@
 #define NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_CORE           (1 <<  5)
 #define NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_CORE_INTERNAL  (1 <<  6)
 #define NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_CORE_PRIVATE   (1 <<  7)
-#define NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_UTIL           (1 <<  8)
-#define NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_GLIB           (1 <<  9)
 #define NM_NETWORKMANAGER_COMPILATION_WITH_DAEMON               (1 << 10)
 #define NM_NETWORKMANAGER_COMPILATION_WITH_SYSTEMD              (1 << 11)
 
@@ -52,17 +49,6 @@
                                                      | NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_CORE_INTERNAL \
                                                      )
 
-#define NM_NETWORKMANAGER_COMPILATION_LIBNM_UTIL     ( 0 \
-                                                     | NM_NETWORKMANAGER_COMPILATION_WITH_GLIB \
-                                                     | NM_NETWORKMANAGER_COMPILATION_WITH_GLIB_I18N_LIB \
-                                                     | NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_UTIL \
-                                                     )
-
-#define NM_NETWORKMANAGER_COMPILATION_LIBNM_GLIB     ( 0 \
-                                                     | NM_NETWORKMANAGER_COMPILATION_LIBNM_UTIL \
-                                                     | NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_GLIB \
-                                                     )
-
 #define NM_NETWORKMANAGER_COMPILATION_CLIENT         ( 0 \
                                                      | NM_NETWORKMANAGER_COMPILATION_WITH_GLIB \
                                                      | NM_NETWORKMANAGER_COMPILATION_WITH_GLIB_I18N_PROG \
@@ -293,16 +279,11 @@ _nm_g_return_if_fail_warning (const char *log_domain,
 #include "nm-glib-aux/nm-macros-internal.h"
 #include "nm-glib-aux/nm-shared-utils.h"
 #include "nm-glib-aux/nm-errno.h"
-
-#if (NETWORKMANAGER_COMPILATION) & NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_UTIL
-/* no hash-utils in legacy code. */
-#else
 #include "nm-glib-aux/nm-hash-utils.h"
-#endif
 
 /*****************************************************************************/
 
-#if (NETWORKMANAGER_COMPILATION) & (NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_CORE | NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_UTIL)
+#if (NETWORKMANAGER_COMPILATION) & NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_CORE
 #include "nm-version.h"
 #endif
 
diff --git a/shared/nm-glib-aux/nm-c-list.h b/shared/nm-glib-aux/nm-c-list.h
index 5c73f574..7512730d 100644
--- a/shared/nm-glib-aux/nm-c-list.h
+++ b/shared/nm-glib-aux/nm-c-list.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
@@ -33,6 +32,8 @@
 		_what && c_list_contains (list, &_what->member); \
 	})
 
+/*****************************************************************************/
+
 typedef struct {
 	CList lst;
 	void *data;
@@ -48,21 +49,34 @@ nm_c_list_elem_new_stale (void *data)
 	return elem;
 }
 
-static inline void *
-nm_c_list_elem_get (CList *lst)
+static inline gboolean
+nm_c_list_elem_free_full (NMCListElem *elem, GDestroyNotify free_fcn)
 {
-	if (!lst)
-		return NULL;
-	return c_list_entry (lst, NMCListElem, lst)->data;
+	if (!elem)
+		return FALSE;
+	c_list_unlink_stale (&elem->lst);
+	if (free_fcn)
+		free_fcn (elem->data);
+	g_slice_free (NMCListElem, elem);
+	return TRUE;
 }
 
-static inline void
+static inline gboolean
 nm_c_list_elem_free (NMCListElem *elem)
 {
-	if (elem) {
-		c_list_unlink_stale (&elem->lst);
-		g_slice_free (NMCListElem, elem);
-	}
+	return nm_c_list_elem_free_full (elem, NULL);
+}
+
+static inline void *
+nm_c_list_elem_free_steal (NMCListElem *elem)
+{
+	gpointer data;
+
+	if (!elem)
+		return NULL;
+	data = elem->data;
+	nm_c_list_elem_free_full (elem, NULL);
+	return data;
 }
 
 static inline void
@@ -70,22 +84,53 @@ nm_c_list_elem_free_all (CList *head, GDestroyNotify free_fcn)
 {
 	NMCListElem *elem;
 
-	while ((elem = c_list_first_entry (head, NMCListElem, lst))) {
-		if (free_fcn)
-			free_fcn (elem->data);
-		c_list_unlink_stale (&elem->lst);
-		g_slice_free (NMCListElem, elem);
+	while ((elem = c_list_first_entry (head, NMCListElem, lst)))
+		nm_c_list_elem_free_full (elem, free_fcn);
+}
+
+/**
+ * nm_c_list_elem_find_first:
+ * @head: the @CList head of a list containing #NMCListElem elements.
+ *   Note that the head is not itself part of the list.
+ * @needle: the needle pointer.
+ *
+ * Iterates the list and returns the first #NMCListElem with the matching @needle,
+ * using pointer equality.
+ *
+ * Returns: the found list element or %NULL if not found.
+ */
+static inline NMCListElem *
+nm_c_list_elem_find_first (CList *head, gconstpointer needle)
+{
+	NMCListElem *elem;
+
+	c_list_for_each_entry (elem, head, lst) {
+		if (elem->data == needle)
+			return elem;
 	}
+	return NULL;
 }
 
 /*****************************************************************************/
 
+/**
+ * nm_c_list_move_before:
+ * @lst: the list element to which @elem will be prepended.
+ * @elem: the list element to move.
+ *
+ * This unlinks @elem from the current list and linkes it before
+ * @lst. This is like c_list_link_before(), except that @elem must
+ * be initialized and linked. Note that @elem may be linked in @lst
+ * or in another list. In both cases it gets moved.
+ *
+ * Returns: %TRUE if there were any changes. %FALSE if elem was already
+ *   linked at the right place.
+ */
 static inline gboolean
 nm_c_list_move_before (CList *lst, CList *elem)
 {
 	nm_assert (lst);
 	nm_assert (elem);
-	nm_assert (c_list_contains (lst, elem));
 
 	if (   lst != elem
 	    && lst->prev != elem) {
@@ -97,12 +142,24 @@ nm_c_list_move_before (CList *lst, CList *elem)
 }
 #define nm_c_list_move_tail(lst, elem) nm_c_list_move_before (lst, elem)
 
+/**
+ * nm_c_list_move_after:
+ * @lst: the list element to which @elem will be prepended.
+ * @elem: the list element to move.
+ *
+ * This unlinks @elem from the current list and linkes it after
+ * @lst. This is like c_list_link_after(), except that @elem must
+ * be initialized and linked. Note that @elem may be linked in @lst
+ * or in another list. In both cases it gets moved.
+ *
+ * Returns: %TRUE if there were any changes. %FALSE if elem was already
+ *   linked at the right place.
+ */
 static inline gboolean
 nm_c_list_move_after (CList *lst, CList *elem)
 {
 	nm_assert (lst);
 	nm_assert (elem);
-	nm_assert (c_list_contains (lst, elem));
 
 	if (   lst != elem
 	    && lst->next != elem) {
@@ -114,4 +171,14 @@ nm_c_list_move_after (CList *lst, CList *elem)
 }
 #define nm_c_list_move_front(lst, elem) nm_c_list_move_after (lst, elem)
 
+#define nm_c_list_free_all(lst, type, member, destroy_fcn) \
+	G_STMT_START { \
+		CList *const _lst = (lst); \
+		type *_elem; \
+		\
+		while ((_elem = c_list_first_entry (_lst, type, member))) { \
+			destroy_fcn (_elem); \
+		} \
+	} G_STMT_END
+
 #endif /* __NM_C_LIST_H__ */
diff --git a/shared/nm-glib-aux/nm-dbus-aux.c b/shared/nm-glib-aux/nm-dbus-aux.c
new file mode 100644
index 00000000..083c4fee
--- /dev/null
+++ b/shared/nm-glib-aux/nm-dbus-aux.c
@@ -0,0 +1,69 @@
+/* NetworkManager -- Network link manager
+ *
+ * This library 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 of the License, or (at your option) any later version.
+ *
+ * This library 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 library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ *
+ * (C) Copyright 2019 Red Hat, Inc.
+ */
+
+#include "nm-default.h"
+
+#include "nm-dbus-aux.h"
+
+/*****************************************************************************/
+
+static void
+_nm_dbus_connection_call_get_name_owner_cb (GObject *source,
+                                            GAsyncResult *res,
+                                            gpointer user_data)
+{
+	gs_unref_variant GVariant *ret = NULL;
+	gs_free_error GError *error = NULL;
+	const char *owner = NULL;
+	gpointer orig_user_data;
+	NMDBusConnectionCallGetNameOwnerCb callback;
+
+	nm_utils_user_data_unpack (user_data, &orig_user_data, &callback);
+
+	ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), res, &error);
+	if (ret)
+		g_variant_get (ret, "(&s)", &owner);
+
+	callback (owner, error, orig_user_data);
+}
+
+void
+nm_dbus_connection_call_get_name_owner (GDBusConnection *dbus_connection,
+                                        const char *service_name,
+                                        int timeout_msec,
+                                        GCancellable *cancellable,
+                                        NMDBusConnectionCallGetNameOwnerCb callback,
+                                        gpointer user_data)
+{
+	nm_assert (callback);
+
+	g_dbus_connection_call (dbus_connection,
+	                        DBUS_SERVICE_DBUS,
+	                        DBUS_PATH_DBUS,
+	                        DBUS_INTERFACE_DBUS,
+	                        "GetNameOwner",
+	                        g_variant_new ("(s)", service_name),
+	                        G_VARIANT_TYPE ("(s)"),
+	                        G_DBUS_CALL_FLAGS_NONE,
+	                        timeout_msec,
+	                        cancellable,
+	                        _nm_dbus_connection_call_get_name_owner_cb,
+	                        nm_utils_user_data_pack (user_data, callback));
+}
diff --git a/shared/nm-glib-aux/nm-dbus-aux.h b/shared/nm-glib-aux/nm-dbus-aux.h
new file mode 100644
index 00000000..271a7d9c
--- /dev/null
+++ b/shared/nm-glib-aux/nm-dbus-aux.h
@@ -0,0 +1,102 @@
+/* NetworkManager -- Network link manager
+ *
+ * This library 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 of the License, or (at your option) any later version.
+ *
+ * This library 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 library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ *
+ * (C) Copyright 2019 Red Hat, Inc.
+ */
+
+#ifndef __NM_DBUS_AUX_H__
+#define __NM_DBUS_AUX_H__
+
+#include "nm-std-aux/nm-dbus-compat.h"
+
+/*****************************************************************************/
+
+static inline gboolean
+nm_clear_g_dbus_connection_signal (GDBusConnection *dbus_connection,
+                                   guint *id)
+{
+	guint v;
+
+	if (   id
+	    && (v = *id)) {
+		*id = 0;
+		g_dbus_connection_signal_unsubscribe (dbus_connection, v);
+		return TRUE;
+	}
+	return FALSE;
+}
+
+/*****************************************************************************/
+
+static inline void
+nm_dbus_connection_call_start_service_by_name (GDBusConnection *dbus_connection,
+                                               const char *name,
+                                               int timeout_msec,
+                                               GCancellable *cancellable,
+                                               GAsyncReadyCallback  callback,
+                                               gpointer user_data)
+{
+	g_dbus_connection_call (dbus_connection,
+	                        DBUS_SERVICE_DBUS,
+	                        DBUS_PATH_DBUS,
+	                        DBUS_INTERFACE_DBUS,
+	                        "StartServiceByName",
+	                        g_variant_new ("(su)", name, 0u),
+	                        G_VARIANT_TYPE ("(u)"),
+	                        G_DBUS_CALL_FLAGS_NONE,
+	                        timeout_msec,
+	                        cancellable,
+	                        callback,
+	                        user_data);
+}
+
+/*****************************************************************************/
+
+static inline guint
+nm_dbus_connection_signal_subscribe_name_owner_changed (GDBusConnection *dbus_connection,
+                                                        const char *service_name,
+                                                        GDBusSignalCallback callback,
+                                                        gpointer user_data,
+                                                        GDestroyNotify user_data_free_func)
+
+{
+	return g_dbus_connection_signal_subscribe (dbus_connection,
+	                                           DBUS_SERVICE_DBUS,
+	                                           DBUS_INTERFACE_DBUS,
+	                                           "NameOwnerChanged",
+	                                           DBUS_PATH_DBUS,
+	                                           service_name,
+	                                           G_DBUS_SIGNAL_FLAGS_NONE,
+	                                           callback,
+	                                           user_data,
+	                                           user_data_free_func);
+}
+
+typedef void (*NMDBusConnectionCallGetNameOwnerCb) (const char *name_owner,
+                                                    GError *error,
+                                                    gpointer user_data);
+
+void nm_dbus_connection_call_get_name_owner (GDBusConnection *dbus_connection,
+                                              const char *service_name,
+                                              int timeout_msec,
+                                              GCancellable *cancellable,
+                                              NMDBusConnectionCallGetNameOwnerCb callback,
+                                              gpointer user_data);
+
+/*****************************************************************************/
+
+#endif /* __NM_DBUS_AUX_H__ */
diff --git a/shared/nm-glib-aux/nm-dedup-multi.c b/shared/nm-glib-aux/nm-dedup-multi.c
index 5bdc3e3c..345062ca 100644
--- a/shared/nm-glib-aux/nm-dedup-multi.c
+++ b/shared/nm-glib-aux/nm-dedup-multi.c
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
diff --git a/shared/nm-glib-aux/nm-dedup-multi.h b/shared/nm-glib-aux/nm-dedup-multi.h
index 82c6f1e9..ca15c516 100644
--- a/shared/nm-glib-aux/nm-dedup-multi.h
+++ b/shared/nm-glib-aux/nm-dedup-multi.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
diff --git a/shared/nm-glib-aux/nm-enum-utils.c b/shared/nm-glib-aux/nm-enum-utils.c
index a4f6e809..b16267a5 100644
--- a/shared/nm-glib-aux/nm-enum-utils.c
+++ b/shared/nm-glib-aux/nm-enum-utils.c
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
diff --git a/shared/nm-glib-aux/nm-enum-utils.h b/shared/nm-glib-aux/nm-enum-utils.h
index 1827fdf4..20db07cc 100644
--- a/shared/nm-glib-aux/nm-enum-utils.h
+++ b/shared/nm-glib-aux/nm-enum-utils.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
diff --git a/shared/nm-glib-aux/nm-glib.h b/shared/nm-glib-aux/nm-glib.h
index e941e067..bdb7ea5b 100644
--- a/shared/nm-glib-aux/nm-glib.h
+++ b/shared/nm-glib-aux/nm-glib.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /*
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -523,10 +522,18 @@ _nm_g_variant_new_printf (const char *format_string, ...)
 
 /*****************************************************************************/
 
-#if !GLIB_CHECK_VERSION (2, 56, 0)
+/* Recent glib also casts the results to typeof(Obj), but only if
+ *
+ *  ( defined(g_has_typeof) && GLIB_VERSION_MAX_ALLOWED >= GLIB_VERSION_2_56 )
+ *
+ * Since we build NetworkManager with older GLIB_VERSION_MAX_ALLOWED, it's
+ * not taking effect.
+ *
+ * Override this. */
+#undef g_object_ref
+#undef g_object_ref_sink
 #define g_object_ref(Obj)      ((typeof(Obj)) g_object_ref (Obj))
 #define g_object_ref_sink(Obj) ((typeof(Obj)) g_object_ref_sink (Obj))
-#endif
 
 /*****************************************************************************/
 
diff --git a/shared/nm-glib-aux/nm-hash-utils.c b/shared/nm-glib-aux/nm-hash-utils.c
index 6e728e6b..a6158269 100644
--- a/shared/nm-glib-aux/nm-hash-utils.c
+++ b/shared/nm-glib-aux/nm-hash-utils.c
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
@@ -45,7 +44,10 @@ _get_hash_key_init (void)
 	 * to use it as guint* or guint64* pointer. */
 	static union {
 		guint8 v8[HASH_KEY_SIZE];
-	} g_arr _nm_alignas (guint64);
+		guint _align_as_uint;
+		guint32 _align_as_uint32;
+		guint64 _align_as_uint64;
+	} g_arr;
 	const guint8 *g;
 	union {
 		guint8 v8[HASH_KEY_SIZE];
@@ -125,14 +127,17 @@ void
 nm_hash_siphash42_init (CSipHash *h, guint static_seed)
 {
 	const guint8 *g;
-	guint seed[HASH_KEY_SIZE_GUINT];
+	union {
+		guint64 _align_as_uint64;
+		guint arr[HASH_KEY_SIZE_GUINT];
+	} seed;
 
 	nm_assert (h);
 
 	g = _get_hash_key ();
-	memcpy (seed, g, HASH_KEY_SIZE);
-	seed[0] ^= static_seed;
-	c_siphash_init (h, (const guint8 *) seed);
+	memcpy (&seed, g, HASH_KEY_SIZE);
+	seed.arr[0] ^= static_seed;
+	c_siphash_init (h, (const guint8 *) &seed);
 }
 
 guint
@@ -194,3 +199,25 @@ nm_pstr_equal (gconstpointer a, gconstpointer b)
 	           && s2
 	           && nm_streq0 (*s1, *s2));
 }
+
+guint
+nm_pdirect_hash (gconstpointer p)
+{
+	const void *const*s = p;
+
+	if (!s)
+		return nm_hash_static (1852748873u);
+	return nm_direct_hash (*s);
+}
+
+gboolean
+nm_pdirect_equal (gconstpointer a, gconstpointer b)
+{
+	const void *const*s1 = a;
+	const void *const*s2 = b;
+
+	return    (s1 == s2)
+	       || (   s1
+	           && s2
+	           && *s1 == *s2);
+}
diff --git a/shared/nm-glib-aux/nm-hash-utils.h b/shared/nm-glib-aux/nm-hash-utils.h
index 3f622f99..f13e0b6d 100644
--- a/shared/nm-glib-aux/nm-hash-utils.h
+++ b/shared/nm-glib-aux/nm-hash-utils.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
@@ -34,7 +33,7 @@ void nm_hash_siphash42_init (CSipHash *h, guint static_seed);
  *
  * Note, that this is guaranteed to use siphash42 under the hood (contrary to
  * all other NMHash API, which leave this undefined). That matters at the point,
- * where the caller needs to be sure that a reasonably strong hasing algorithm
+ * where the caller needs to be sure that a reasonably strong hashing algorithm
  * is used.  (Yes, NMHash is all about siphash24, but otherwise that is not promised
  * anywhere).
  *
@@ -291,7 +290,16 @@ gboolean nm_pstr_equal (gconstpointer a, gconstpointer b);
 
 /*****************************************************************************/
 
-#define NM_HASH_OBFUSCATE_PTR_FMT "%016llx"
+/* this hashes/compares the pointer value that we point to. Basically,
+ * (((const void *const*) a) == ((const void *const*) b)). */
+
+guint nm_pdirect_hash (gconstpointer p);
+
+gboolean nm_pdirect_equal (gconstpointer a, gconstpointer b);
+
+/*****************************************************************************/
+
+#define NM_HASH_OBFUSCATE_PTR_FMT "%016" G_GINT64_MODIFIER "x"
 
 /* sometimes we want to log a pointer directly, for providing context/information about
  * the message that get logged. Logging pointer values directly defeats ASLR, so we should
@@ -307,9 +315,19 @@ gboolean nm_pstr_equal (gconstpointer a, gconstpointer b);
 		\
 		nm_hash_init (&_h, (static_seed)); \
 		nm_hash_update_val (&_h, _val_obf_ptr); \
-		(unsigned long long) nm_hash_complete_u64 (&_h); \
+		nm_hash_complete_u64 (&_h); \
 	})
 
+/* if you want to log obfuscated pointer for a certain context (like, NMPRuleManager
+ * logging user-tags), then you are advised to use nm_hash_obfuscate_ptr() with your
+ * own, unique static-seed.
+ *
+ * However, for example the singleton constructors log the obfuscated pointer values
+ * for all singletons, so they must all be obfuscated with the same seed. So, this
+ * macro uses a particular static seed that should be used by when comparing pointer
+ * values in a global context. */
+#define NM_HASH_OBFUSCATE_PTR(ptr) (nm_hash_obfuscate_ptr (1678382159u, ptr))
+
 /*****************************************************************************/
 
 #endif /* __NM_HASH_UTILS_H__ */
diff --git a/shared/nm-glib-aux/nm-io-utils.c b/shared/nm-glib-aux/nm-io-utils.c
index 51312748..23133ec5 100644
--- a/shared/nm-glib-aux/nm-io-utils.c
+++ b/shared/nm-glib-aux/nm-io-utils.c
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
@@ -437,3 +436,26 @@ nm_utils_file_set_contents (const char *filename,
 
 	return TRUE;
 }
+
+/**
+ * nm_utils_file_stat:
+ * @filename: the filename to stat.
+ * @out_st: (allow-none) (out): if given, this will be passed to stat().
+ *
+ * Just wraps stat() and gives the errno number as function result instead
+ * of setting the errno (though, errno is also set). It's only for convenience
+ * with
+ *
+ *    if (nm_utils_file_stat (filename, NULL) == -ENOENT) {
+ *    }
+ *
+ * Returns: 0 on success a negative errno on failure. */
+int
+nm_utils_file_stat (const char *filename, struct stat *out_st)
+{
+	struct stat st;
+
+	if (stat (filename, out_st ?: &st) != 0)
+		return -NM_ERRNO_NATIVE (errno);
+	return 0;
+}
diff --git a/shared/nm-glib-aux/nm-io-utils.h b/shared/nm-glib-aux/nm-io-utils.h
index dc72a2a6..121fc481 100644
--- a/shared/nm-glib-aux/nm-io-utils.h
+++ b/shared/nm-glib-aux/nm-io-utils.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
@@ -60,4 +59,8 @@ gboolean nm_utils_file_set_contents (const char *filename,
                                      mode_t mode,
                                      GError **error);
 
+struct stat;
+
+int nm_utils_file_stat (const char *filename, struct stat *out_st);
+
 #endif /* __NM_IO_UTILS_H__ */
diff --git a/shared/nm-glib-aux/nm-jansson.h b/shared/nm-glib-aux/nm-jansson.h
index 5a73231f..d4642319 100644
--- a/shared/nm-glib-aux/nm-jansson.h
+++ b/shared/nm-glib-aux/nm-jansson.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /*
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -44,6 +43,106 @@
 NM_AUTO_DEFINE_FCN0 (json_t *, _nm_auto_decref_json, json_decref)
 #define nm_auto_decref_json nm_auto(_nm_auto_decref_json)
 
+/*****************************************************************************/
+
+static inline int
+nm_jansson_json_as_bool (const json_t *elem,
+                         bool *out_val)
+{
+	if (!elem)
+		return 0;
+
+	if (!json_is_boolean (elem))
+		return -EINVAL;
+
+	NM_SET_OUT (out_val, json_boolean_value (elem));
+	return 1;
+}
+
+static inline int
+nm_jansson_json_as_int32 (const json_t *elem,
+                          gint32 *out_val)
+{
+	json_int_t v;
+
+	if (!elem)
+		return 0;
+
+	if (!json_is_integer (elem))
+		return -EINVAL;
+
+	v = json_integer_value (elem);
+	if (   v < (gint64) G_MININT32
+	    || v > (gint64) G_MAXINT32)
+		return -ERANGE;
+
+	NM_SET_OUT (out_val, v);
+	return 1;
+}
+
+static inline int
+nm_jansson_json_as_int (const json_t *elem,
+                        int *out_val)
+{
+	json_int_t v;
+
+	if (!elem)
+		return 0;
+
+	if (!json_is_integer (elem))
+		return -EINVAL;
+
+	v = json_integer_value (elem);
+	if (   v < (gint64) G_MININT
+	    || v > (gint64) G_MAXINT)
+		return -ERANGE;
+
+	NM_SET_OUT (out_val, v);
+	return 1;
+}
+
+static inline int
+nm_jansson_json_as_string (const json_t *elem,
+                           const char **out_val)
+{
+	if (!elem)
+		return 0;
+
+	if (!json_is_string (elem))
+		return -EINVAL;
+
+	NM_SET_OUT (out_val, json_string_value (elem));
+	return 1;
+}
+
+/*****************************************************************************/
+
+#ifdef NM_VALUE_TYPE_DEFINE_FUNCTIONS
+#include "nm-value-type.h"
+static inline gboolean
+nm_value_type_from_json (NMValueType value_type,
+                         const json_t *elem,
+                         gpointer out_val)
+{
+	switch (value_type) {
+	case NM_VALUE_TYPE_BOOL:   return (nm_jansson_json_as_bool   (elem, out_val) > 0);
+	case NM_VALUE_TYPE_INT32:  return (nm_jansson_json_as_int32  (elem, out_val) > 0);
+	case NM_VALUE_TYPE_INT:    return (nm_jansson_json_as_int    (elem, out_val) > 0);
+
+	/* warning: this overwrites/leaks the previous value. You better have *out_val
+	 * point to uninitialized memory or NULL. */
+	case NM_VALUE_TYPE_STRING: return (nm_jansson_json_as_string (elem, out_val) > 0);
+
+	case NM_VALUE_TYPE_UNSPEC:
+		break;
+	}
+	nm_assert_not_reached ();
+	return FALSE;
+}
+#endif
+
+/*****************************************************************************/
+
 #endif /* WITH_JANSON */
 
 #endif  /* __NM_JANSSON_H__ */
diff --git a/shared/nm-glib-aux/nm-json-aux.c b/shared/nm-glib-aux/nm-json-aux.c
new file mode 100644
index 00000000..6f04ef2b
--- /dev/null
+++ b/shared/nm-glib-aux/nm-json-aux.c
@@ -0,0 +1,149 @@
+/*
+ * This library 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 of the License, or (at your option) any later version.
+ *
+ * This library 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 library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ *
+ * Copyright 2019 Red Hat, Inc.
+ */
+
+#include "nm-default.h"
+
+#include "nm-json-aux.h"
+
+/*****************************************************************************/
+
+static void
+_gstr_append_string_len (GString *gstr,
+                         const char *str,
+                         gsize len)
+{
+	g_string_append_c (gstr, '\"');
+
+	while (len > 0) {
+		gsize n;
+		const char *end;
+		gboolean valid;
+
+		nm_assert (len > 0);
+
+		valid = g_utf8_validate (str, len, &end);
+
+		nm_assert (   end
+		           && end >= str
+		           && end <= &str[len]);
+
+		if (end > str) {
+			const char *s;
+
+			for (s = str; s < end; s++) {
+				nm_assert (s[0] != '\0');
+
+				if (s[0] < 0x20) {
+					const char *text;
+
+					switch (s[0]) {
+					case '\\': text = "\\\\"; break;
+					case '\"': text = "\\\""; break;
+					case '\b': text = "\\b";  break;
+					case '\f': text = "\\f";  break;
+					case '\n': text = "\\n";  break;
+					case '\r': text = "\\r";  break;
+					case '\t': text = "\\t";  break;
+					default:
+						g_string_append_printf (gstr, "\\u%04X", (guint) s[0]);
+						continue;
+					}
+					g_string_append (gstr, text);
+					continue;
+				}
+
+				if (NM_IN_SET (s[0], '\\', '\"'))
+					g_string_append_c (gstr, '\\');
+				g_string_append_c (gstr, s[0]);
+			}
+		} else
+			nm_assert (!valid);
+
+		if (valid) {
+			nm_assert (end == &str[len]);
+			break;
+		}
+
+		nm_assert (end < &str[len]);
+
+		if (end[0] == '\0') {
+			/* there is a NUL byte in the string. Technically this is valid UTF-8, so we
+			 * encode it there. However, this will likely result in a truncated string when
+			 * parsing. */
+			g_string_append (gstr, "\\u0000");
+		} else {
+			/* the character is not valid UTF-8. There is nothing we can do about it, because
+			 * JSON can only contain UTF-8 and even the escape sequences can only escape Unicode
+			 * codepoints (but not binary).
+			 *
+			 * The argument is not a a string (in any known encoding), hence we cannot represent
+			 * it as a JSON string (which are unicode strings).
+			 *
+			 * Print an underscore instead of the invalid char :) */
+			g_string_append_c (gstr, '_');
+		}
+
+		n = str - end;
+		nm_assert (n < len);
+		n++;
+		str += n;
+		len -= n;
+	}
+
+	g_string_append_c (gstr, '\"');
+}
+
+void
+nm_json_aux_gstr_append_string_len (GString *gstr,
+                                    const char *str,
+                                    gsize n)
+{
+	g_return_if_fail (gstr);
+
+	_gstr_append_string_len (gstr, str, n);
+}
+
+void
+nm_json_aux_gstr_append_string (GString *gstr,
+                                const char *str)
+{
+	g_return_if_fail (gstr);
+
+	if (!str)
+		g_string_append (gstr, "null");
+	else
+		_gstr_append_string_len (gstr, str, strlen (str));
+}
+
+void
+nm_json_aux_gstr_append_obj_name (GString *gstr,
+                                  const char *key,
+                                  char start_container)
+{
+	g_return_if_fail (gstr);
+	g_return_if_fail (key);
+
+	nm_json_aux_gstr_append_string (gstr, key);
+
+	if (start_container != '\0') {
+		nm_assert (NM_IN_SET (start_container, '[', '{'));
+		g_string_append_printf (gstr, ": %c ", start_container);
+	} else
+		g_string_append (gstr, ": ");
+}
diff --git a/shared/nm-glib-aux/nm-json-aux.h b/shared/nm-glib-aux/nm-json-aux.h
new file mode 100644
index 00000000..19d43ce4
--- /dev/null
+++ b/shared/nm-glib-aux/nm-json-aux.h
@@ -0,0 +1,83 @@
+/*
+ * This library 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 of the License, or (at your option) any later version.
+ *
+ * This library 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 library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ *
+ * Copyright 2019 Red Hat, Inc.
+ */
+
+#ifndef __NM_JSON_AUX_H__
+#define __NM_JSON_AUX_H__
+
+/*****************************************************************************/
+
+static inline GString *
+nm_json_aux_gstr_append_delimiter (GString *gstr)
+{
+	g_string_append (gstr, ", ");
+	return gstr;
+}
+
+void nm_json_aux_gstr_append_string_len (GString *gstr,
+                                         const char *str,
+                                         gsize n);
+
+void nm_json_aux_gstr_append_string (GString *gstr,
+                                     const char *str);
+
+static inline void
+nm_json_aux_gstr_append_bool (GString *gstr,
+                              gboolean v)
+{
+	g_string_append (gstr, v ? "true" : "false");
+}
+
+static inline void
+nm_json_aux_gstr_append_int64 (GString *gstr,
+                               gint64 v)
+{
+	g_string_append_printf (gstr, "%"G_GINT64_FORMAT, v);
+}
+
+void nm_json_aux_gstr_append_obj_name (GString *gstr,
+                                       const char *key,
+                                       char start_container);
+
+/*****************************************************************************/
+
+#ifdef NM_VALUE_TYPE_DEFINE_FUNCTIONS
+#include "nm-value-type.h"
+static inline void
+nm_value_type_to_json (NMValueType value_type,
+                       GString *gstr,
+                       gconstpointer p_field)
+{
+	nm_assert (p_field);
+	nm_assert (gstr);
+
+	switch (value_type) {
+	case NM_VALUE_TYPE_BOOL:   nm_json_aux_gstr_append_bool   (gstr, *((const bool        *) p_field)); return;
+	case NM_VALUE_TYPE_INT32:  nm_json_aux_gstr_append_int64  (gstr, *((const gint32      *) p_field)); return;
+	case NM_VALUE_TYPE_INT:    nm_json_aux_gstr_append_int64  (gstr, *((const int         *) p_field)); return;
+	case NM_VALUE_TYPE_STRING: nm_json_aux_gstr_append_string (gstr, *((const char *const *) p_field)); return;
+	case NM_VALUE_TYPE_UNSPEC:
+		break;
+	}
+	nm_assert_not_reached ();
+}
+#endif
+
+/*****************************************************************************/
+
+#endif  /* __NM_JSON_AUX_H__ */
diff --git a/shared/nm-glib-aux/nm-keyfile-aux.c b/shared/nm-glib-aux/nm-keyfile-aux.c
new file mode 100644
index 00000000..0257bcca
--- /dev/null
+++ b/shared/nm-glib-aux/nm-keyfile-aux.c
@@ -0,0 +1,413 @@
+/* NetworkManager -- Network link manager
+ *
+ * This library 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 of the License, or (at your option) any later version.
+ *
+ * This library 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 library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ *
+ * (C) Copyright 2019 Red Hat, Inc.
+ */
+
+#include "nm-default.h"
+
+#include "nm-keyfile-aux.h"
+
+#include <syslog.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include "nm-io-utils.h"
+
+/*****************************************************************************/
+
+struct _NMKeyFileDB {
+	NMKeyFileDBLogFcn log_fcn;
+	NMKeyFileDBGotDirtyFcn got_dirty_fcn;
+	gpointer user_data;
+	const char *group_name;
+	GKeyFile *kf;
+	guint ref_count;
+
+	bool is_started:1;
+	bool dirty:1;
+	bool destroyed:1;
+
+	char filename[];
+};
+
+#define _NMLOG(self, \
+               syslog_level, \
+               fmt, \
+               ...) \
+	G_STMT_START { \
+		NMKeyFileDB *_self = (self); \
+		\
+		nm_assert (_self); \
+		nm_assert (!_self->destroyed); \
+		\
+		if (_self->log_fcn) { \
+			_self->log_fcn (_self, \
+			                (syslog_level), \
+			                _self->user_data, \
+			                ""fmt"", \
+			                ##__VA_ARGS__); \
+		}; \
+	} G_STMT_END
+
+#define _LOGD(...) _NMLOG (self, LOG_DEBUG, __VA_ARGS__)
+
+static gboolean
+_IS_KEY_FILE_DB (NMKeyFileDB *self, gboolean require_is_started, gboolean allow_destroyed)
+{
+	if (self == NULL)
+		return FALSE;
+	if (self->ref_count <= 0) {
+		nm_assert_not_reached ();
+		return FALSE;
+	}
+	if (   require_is_started
+	    && !self->is_started)
+		return FALSE;
+	if (   !allow_destroyed
+	    && self->destroyed)
+		return FALSE;
+	return TRUE;
+}
+
+/*****************************************************************************/
+
+NMKeyFileDB *
+nm_key_file_db_new (const char *filename,
+                    const char *group_name,
+                    NMKeyFileDBLogFcn log_fcn,
+                    NMKeyFileDBGotDirtyFcn got_dirty_fcn,
+                    gpointer user_data)
+{
+	NMKeyFileDB *self;
+	gsize l_filename;
+	gsize l_group;
+
+	g_return_val_if_fail (filename && filename[0], NULL);
+	g_return_val_if_fail (group_name && group_name[0], NULL);
+
+	l_filename = strlen (filename);
+	l_group = strlen (group_name);
+
+	self = g_malloc0 (sizeof (NMKeyFileDB) + l_filename + 1 + l_group + 1);
+	self->ref_count = 1;
+	self->log_fcn = log_fcn;
+	self->got_dirty_fcn = got_dirty_fcn;
+	self->user_data = user_data;
+	self->kf = g_key_file_new ();
+	g_key_file_set_list_separator (self->kf, ',');
+	memcpy (self->filename, filename, l_filename + 1);
+	self->group_name = &self->filename[l_filename + 1];
+	memcpy ((char *) self->group_name, group_name, l_group + 1);
+
+	return self;
+}
+
+NMKeyFileDB *
+nm_key_file_db_ref (NMKeyFileDB *self)
+{
+	if (!self)
+		return NULL;
+
+	g_return_val_if_fail (_IS_KEY_FILE_DB (self, FALSE, TRUE), NULL);
+
+	nm_assert (self->ref_count <= G_MAXUINT);
+	self->ref_count++;
+	return self;
+}
+
+void
+nm_key_file_db_unref (NMKeyFileDB *self)
+{
+	if (!self)
+		return;
+
+	g_return_if_fail (_IS_KEY_FILE_DB (self, FALSE, TRUE));
+
+	if (--self->ref_count > 0)
+		return;
+
+	g_key_file_unref (self->kf);
+
+	g_free (self);
+}
+
+/* destroy() is like unref, but it also makes the instance unusable.
+ * All changes afterwards fail with an assertion.
+ *
+ * The point is that NMKeyFileDB is ref-counted in principle. But there
+ * is a primary owner who also provides the log_fcn().
+ *
+ * When the primary owner goes out of scope and gives up the reference, it does
+ * not want to receive any log notifications anymore.
+ *
+ * The way NMKeyFileDB is intended to be used is in a very strict context:
+ * NMSettings owns the NMKeyFileDB instance and receives logging notifications.
+ * It's also the last one to persist the data to disk. Afterwards, no other user
+ * is supposed to be around and do anything with NMKeyFileDB. But since NMKeyFileDB
+ * is ref-counted it's hard to ensure that this is truly honored. So we start
+ * asserting at that point.
+ */
+void
+nm_key_file_db_destroy (NMKeyFileDB *self)
+{
+	if (!self)
+		return;
+
+	g_return_if_fail (_IS_KEY_FILE_DB (self, FALSE, FALSE));
+	g_return_if_fail (!self->destroyed);
+
+	self->destroyed = TRUE;
+	nm_key_file_db_unref (self);
+}
+
+/*****************************************************************************/
+
+/* nm_key_file_db_start() is supposed to be called right away, after creating the
+ * instance.
+ *
+ * It's not done as separate step after nm_key_file_db_new(), because we want to log,
+ * and the log_fcn returns the self pointer (which we should not expose before
+ * nm_key_file_db_new() returns. */
+void
+nm_key_file_db_start (NMKeyFileDB *self)
+{
+	int r;
+	gs_free char *contents = NULL;
+	gsize contents_len;
+	gs_free_error GError *error = NULL;
+
+	g_return_if_fail (_IS_KEY_FILE_DB (self, FALSE, FALSE));
+	g_return_if_fail (!self->is_started);
+
+	self->is_started = TRUE;
+
+	r = nm_utils_file_get_contents (-1,
+	                                self->filename,
+	                                20*1024*1024,
+	                                NM_UTILS_FILE_GET_CONTENTS_FLAG_NONE,
+	                                &contents,
+	                                &contents_len,
+	                                &error);
+	if (r < 0) {
+		_LOGD ("failed to read \"%s\": %s", self->filename, error->message);
+		return;
+	}
+
+	if (!g_key_file_load_from_data (self->kf,
+	                                contents,
+	                                contents_len,
+	                                G_KEY_FILE_KEEP_COMMENTS,
+	                                &error)) {
+		_LOGD ("failed to load keyfile \"%s\": %s", self->filename, error->message);
+		return;
+	}
+
+	_LOGD ("loaded keyfile-db for \"%s\"", self->filename);
+}
+
+/*****************************************************************************/
+
+const char *
+nm_key_file_db_get_filename (NMKeyFileDB *self)
+{
+	g_return_val_if_fail (_IS_KEY_FILE_DB (self, FALSE, TRUE), NULL);
+
+	return self->filename;
+}
+
+gboolean
+nm_key_file_db_is_dirty (NMKeyFileDB *self)
+{
+	g_return_val_if_fail (_IS_KEY_FILE_DB (self, FALSE, TRUE), FALSE);
+
+	return self->dirty;
+}
+
+/*****************************************************************************/
+
+char *
+nm_key_file_db_get_value (NMKeyFileDB *self,
+                          const char *key)
+{
+	g_return_val_if_fail (_IS_KEY_FILE_DB (self, TRUE, TRUE), NULL);
+
+	return g_key_file_get_value (self->kf, self->group_name, key, NULL);
+}
+
+char **
+nm_key_file_db_get_string_list (NMKeyFileDB *self,
+                                const char *key,
+                                gsize *out_len)
+{
+	g_return_val_if_fail (_IS_KEY_FILE_DB (self, TRUE, TRUE), NULL);
+
+	return g_key_file_get_string_list (self->kf, self->group_name, key, out_len, NULL);
+}
+
+/*****************************************************************************/
+
+static void
+_got_dirty (NMKeyFileDB *self,
+            const char *key)
+{
+	nm_assert (_IS_KEY_FILE_DB (self, TRUE, FALSE));
+	nm_assert (!self->dirty);
+
+	_LOGD ("updated entry for %s.%s", self->group_name, key);
+
+	self->dirty = TRUE;
+	if (self->got_dirty_fcn)
+		self->got_dirty_fcn (self, self->user_data);
+}
+
+/*****************************************************************************/
+
+void
+nm_key_file_db_remove_key (NMKeyFileDB *self,
+                           const char *key)
+{
+	gboolean got_dirty = FALSE;
+
+	g_return_if_fail (_IS_KEY_FILE_DB (self, TRUE, FALSE));
+
+	if (!key)
+		return;
+
+	if (!self->dirty) {
+		gs_free_error GError *error = NULL;
+
+		g_key_file_has_key (self->kf, self->group_name, key, &error);
+		got_dirty = (error != NULL);
+	}
+	g_key_file_remove_key (self->kf, self->group_name, key, NULL);
+
+	if (got_dirty)
+		_got_dirty (self, key);
+}
+
+void
+nm_key_file_db_set_value (NMKeyFileDB *self,
+                          const char *key,
+                          const char *value)
+{
+	gs_free char *old_value = NULL;
+	gboolean got_dirty = FALSE;
+
+	g_return_if_fail (_IS_KEY_FILE_DB (self, TRUE, FALSE));
+	g_return_if_fail (key);
+
+	if (!value) {
+		nm_key_file_db_remove_key (self, key);
+		return;
+	}
+
+	if (!self->dirty) {
+		gs_free_error GError *error = NULL;
+
+		old_value = g_key_file_get_value (self->kf, self->group_name, key, &error);
+		if (error)
+			got_dirty = TRUE;
+	}
+
+	g_key_file_set_value (self->kf, self->group_name, key, value);
+
+	if (   !self->dirty
+	    && !got_dirty) {
+		gs_free_error GError *error = NULL;
+		gs_free char *new_value = NULL;
+
+		new_value = g_key_file_get_value (self->kf, self->group_name, key, &error);
+		if (   error
+		    || !new_value
+		    || !nm_streq0 (old_value, new_value))
+			got_dirty = TRUE;
+	}
+
+	if (got_dirty)
+		_got_dirty (self, key);
+}
+
+void
+nm_key_file_db_set_string_list (NMKeyFileDB *self,
+                                const char *key,
+                                const char *const*value,
+                                gssize len)
+{
+	gs_free char *old_value = NULL;
+	gboolean got_dirty = FALSE;;
+
+	g_return_if_fail (_IS_KEY_FILE_DB (self, TRUE, FALSE));
+	g_return_if_fail (key);
+
+	if (!value) {
+		nm_key_file_db_remove_key (self, key);
+		return;
+	}
+
+	if (!self->dirty) {
+		gs_free_error GError *error = NULL;
+
+		old_value = g_key_file_get_value (self->kf, self->group_name, key, &error);
+		if (error)
+			got_dirty = TRUE;
+	}
+
+	if (len < 0)
+		len = NM_PTRARRAY_LEN (value);
+
+	g_key_file_set_string_list (self->kf, self->group_name, key, value, len);
+
+	if (   !self->dirty
+	    && !got_dirty) {
+		gs_free_error GError *error = NULL;
+		gs_free char *new_value = NULL;
+
+		new_value = g_key_file_get_value (self->kf, self->group_name, key, &error);
+		if (   error
+		    || !new_value
+		    || !nm_streq0 (old_value, new_value))
+			got_dirty = TRUE;
+	}
+
+	if (got_dirty)
+		_got_dirty (self, key);
+}
+
+/*****************************************************************************/
+
+void
+nm_key_file_db_to_file (NMKeyFileDB *self,
+                        gboolean force)
+{
+	gs_free_error GError *error = NULL;
+
+	g_return_if_fail (_IS_KEY_FILE_DB (self, TRUE, FALSE));
+
+	if (   !force
+	    && !self->dirty)
+		return;
+
+	self->dirty = FALSE;
+
+	if (!g_key_file_save_to_file (self->kf,
+	                              self->filename,
+	                              &error)) {
+		_LOGD ("failure to write keyfile \"%s\": %s", self->filename, error->message);
+	} else
+		_LOGD ("write keyfile: \"%s\"", self->filename);
+}
diff --git a/shared/nm-glib-aux/nm-keyfile-aux.h b/shared/nm-glib-aux/nm-keyfile-aux.h
new file mode 100644
index 00000000..8563f4d1
--- /dev/null
+++ b/shared/nm-glib-aux/nm-keyfile-aux.h
@@ -0,0 +1,78 @@
+/* NetworkManager -- Network link manager
+ *
+ * This library 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 of the License, or (at your option) any later version.
+ *
+ * This library 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 library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ *
+ * (C) Copyright 2019 Red Hat, Inc.
+ */
+
+#ifndef __NM_KEYFILE_AUX_H__
+#define __NM_KEYFILE_AUX_H__
+
+/*****************************************************************************/
+
+typedef struct _NMKeyFileDB NMKeyFileDB;
+
+typedef void (*NMKeyFileDBLogFcn) (NMKeyFileDB *self,
+                                   int syslog_level,
+                                   gpointer user_data,
+                                   const char *fmt,
+                                   ...) G_GNUC_PRINTF (4, 5);
+
+typedef void (*NMKeyFileDBGotDirtyFcn) (NMKeyFileDB *self,
+                                        gpointer user_data);
+
+NMKeyFileDB *nm_key_file_db_new (const char *filename,
+                                 const char *group,
+                                 NMKeyFileDBLogFcn log_fcn,
+                                 NMKeyFileDBGotDirtyFcn got_dirty_fcn,
+                                 gpointer user_data);
+
+void nm_key_file_db_start (NMKeyFileDB *self);
+
+NMKeyFileDB *nm_key_file_db_ref (NMKeyFileDB *self);
+void nm_key_file_db_unref (NMKeyFileDB *self);
+
+void nm_key_file_db_destroy (NMKeyFileDB *self);
+
+const char *nm_key_file_db_get_filename (NMKeyFileDB *self);
+
+gboolean nm_key_file_db_is_dirty (NMKeyFileDB *self);
+
+char *nm_key_file_db_get_value (NMKeyFileDB *self,
+                                const char *key);
+
+char **nm_key_file_db_get_string_list (NMKeyFileDB *self,
+                                       const char *key,
+                                       gsize *out_len);
+
+void nm_key_file_db_remove_key (NMKeyFileDB *self,
+                                const char *key);
+
+void nm_key_file_db_set_value (NMKeyFileDB *self,
+                               const char *key,
+                               const char *value);
+
+void nm_key_file_db_set_string_list (NMKeyFileDB *self,
+                                     const char *key,
+                                     const char *const*value,
+                                     gssize len);
+
+void nm_key_file_db_to_file (NMKeyFileDB *self,
+                             gboolean force);
+
+/*****************************************************************************/
+
+#endif /* __NM_KEYFILE_AUX_H__ */
diff --git a/shared/nm-glib-aux/nm-logging-fwd.h b/shared/nm-glib-aux/nm-logging-fwd.h
index 900dfff8..c60a20b5 100644
--- a/shared/nm-glib-aux/nm-logging-fwd.h
+++ b/shared/nm-glib-aux/nm-logging-fwd.h
@@ -110,4 +110,31 @@ void _nm_log_impl (const char *file,
                    const char *fmt,
                    ...) _nm_printf (10, 11);
 
+static inline NMLogLevel
+nm_log_level_from_syslog (int syslog_level)
+{
+	switch (syslog_level) {
+	case 0 /* LOG_EMERG */   : return LOGL_ERR;
+	case 1 /* LOG_ALERT */   : return LOGL_ERR;
+	case 2 /* LOG_CRIT */    : return LOGL_ERR;
+	case 3 /* LOG_ERR */     : return LOGL_ERR;
+	case 4 /* LOG_WARNING */ : return LOGL_WARN;
+	case 5 /* LOG_NOTICE */  : return LOGL_INFO;
+	case 6 /* LOG_INFO */    : return LOGL_DEBUG;
+	case 7 /* LOG_DEBUG */   : return LOGL_TRACE;
+	default:
+		return syslog_level >= 0 ? LOGL_TRACE : LOGL_ERR;
+	}
+}
+
+/*****************************************************************************/
+
+struct timespec;
+
+/* this function must be implemented to handle the notification when
+ * the first monotonic-timestamp is fetched. */
+extern void _nm_utils_monotonic_timestamp_initialized (const struct timespec *tp,
+                                                       gint64 offset_sec,
+                                                       gboolean is_boottime);
+
 #endif /* __NM_LOGGING_DEFINES_H__ */
diff --git a/shared/nm-glib-aux/nm-macros-internal.h b/shared/nm-glib-aux/nm-macros-internal.h
index 2e46cd2d..9502c442 100644
--- a/shared/nm-glib-aux/nm-macros-internal.h
+++ b/shared/nm-glib-aux/nm-macros-internal.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
@@ -635,8 +634,14 @@ NM_G_ERROR_MSG (GError *error)
  * It's useful to check the let the compiler ensure that @value is
  * of a certain type. */
 #define _NM_ENSURE_TYPE(type, value) (_Generic ((value), type: (value)))
+#define _NM_ENSURE_TYPE_CONST(type, value) (_Generic ((value), \
+                                                      const type      : ((const type) (value)), \
+                                                      const type const: ((const type) (value)), \
+                                                            type      : ((const type) (value)), \
+                                                            type const: ((const type) (value))))
 #else
 #define _NM_ENSURE_TYPE(type, value) (value)
+#define _NM_ENSURE_TYPE_CONST(type, value) ((const type) (value))
 #endif
 
 #if _NM_CC_SUPPORT_GENERIC
@@ -865,6 +870,28 @@ fcn (void) \
 
 /*****************************************************************************/
 
+static inline int
+nm_strcmp0 (const char *s1, const char *s2)
+{
+	int c;
+
+	/* like g_strcmp0(), but this is inlinable.
+	 *
+	 * Also, it is guaranteed to return either -1, 0, or 1. */
+	if (s1 == s2)
+		return 0;
+	if (!s1)
+		return -1;
+	if (!s2)
+		return 1;
+	c = strcmp (s1, s2);
+	if (c < 0)
+		return -1;
+	if (c > 0)
+		return 1;
+	return 0;
+}
+
 static inline gboolean
 nm_streq (const char *s1, const char *s2)
 {
@@ -879,14 +906,19 @@ nm_streq0 (const char *s1, const char *s2)
 }
 
 #define NM_STR_HAS_PREFIX(str, prefix) \
-	(strncmp ((str), ""prefix"", NM_STRLEN (prefix)) == 0)
+	({ \
+		const char *const _str = (str); \
+		\
+		_str && (strncmp ((str), ""prefix"", NM_STRLEN (prefix)) == 0); \
+	})
 
 #define NM_STR_HAS_SUFFIX(str, suffix) \
 	({ \
-		const char *_str = (str); \
-		gsize _l = strlen (_str); \
+		const char *_str; \
+		gsize _l; \
 		\
-		(   (_l >= NM_STRLEN (suffix)) \
+		(   (_str = (str)) \
+		 && ((_l = strlen (_str)) >= NM_STRLEN (suffix)) \
 		 && (memcmp (&_str[_l - NM_STRLEN (suffix)], \
 		             ""suffix"", \
 		             NM_STRLEN (suffix)) == 0)); \
@@ -989,10 +1021,9 @@ typedef enum { \
 } _PropertyEnums; \
 static GParamSpec *obj_properties[_PROPERTY_ENUMS_LAST] = { NULL, }
 
-#define NM_GOBJECT_PROPERTIES_DEFINE(obj_type, ...) \
-NM_GOBJECT_PROPERTIES_DEFINE_BASE (__VA_ARGS__); \
+#define NM_GOBJECT_PROPERTIES_DEFINE_NOTIFY(obj_type, obj_properties, property_enums_type, prop_0) \
 static inline void \
-_nm_gobject_notify_together_impl (obj_type *obj, guint n, const _PropertyEnums *props) \
+_nm_gobject_notify_together_impl (obj_type *obj, guint n, const property_enums_type *props) \
 { \
 	const gboolean freeze_thaw = (n > 1); \
 	\
@@ -1002,9 +1033,9 @@ _nm_gobject_notify_together_impl (obj_type *obj, guint n, const _PropertyEnums *
 	if (freeze_thaw) \
 		g_object_freeze_notify ((GObject *) obj); \
 	while (n-- > 0) { \
-		const _PropertyEnums prop = *props++; \
+		const property_enums_type prop = *props++; \
 		\
-		if (prop != PROP_0) { \
+		if (prop != prop_0) { \
 			nm_assert ((gsize) prop < G_N_ELEMENTS (obj_properties)); \
 			nm_assert (obj_properties[prop]); \
 			g_object_notify_by_pspec ((GObject *) obj, obj_properties[prop]); \
@@ -1015,11 +1046,15 @@ _nm_gobject_notify_together_impl (obj_type *obj, guint n, const _PropertyEnums *
 } \
 \
 static inline void \
-_notify (obj_type *obj, _PropertyEnums prop) \
+_notify (obj_type *obj, property_enums_type prop) \
 { \
 	_nm_gobject_notify_together_impl (obj, 1, &prop); \
 } \
 
+#define NM_GOBJECT_PROPERTIES_DEFINE(obj_type, ...) \
+NM_GOBJECT_PROPERTIES_DEFINE_BASE (__VA_ARGS__); \
+NM_GOBJECT_PROPERTIES_DEFINE_NOTIFY (obj_type, obj_properties, _PropertyEnums, PROP_0)
+
 /* invokes _notify() for all arguments (of type _PropertyEnums). Note, that if
  * there are more than one prop arguments, this will involve a freeze/thaw
  * of GObject property notifications. */
@@ -1130,6 +1165,30 @@ nm_g_object_unref (gpointer obj)
 #define nm_clear_g_object(pp) \
 	nm_clear_pointer (pp, g_object_unref)
 
+/**
+ * nm_clear_error:
+ * @err: a pointer to pointer to a #GError.
+ *
+ * This is like g_clear_error(). The only difference is
+ * that this is an inline function.
+ */
+static inline void
+nm_clear_error (GError **err)
+{
+	if (err && *err) {
+		g_error_free (*err);
+		*err = NULL;
+	}
+}
+
+/* Patch g_clear_error() to use nm_clear_error(), which is inlineable
+ * and visible to the compiler. For example gs_free_error attribute only
+ * frees the error after checking that it's not %NULL. So, in many cases
+ * the compiler knows that gs_free_error has no effect and can optimize
+ * the call away. By making g_clear_error() inlineable, we give the compiler
+ * more chance to detect that the function actually has no effect. */
+#define g_clear_error(ptr) nm_clear_error(ptr)
+
 static inline gboolean
 nm_clear_g_source (guint *id)
 {
@@ -1219,6 +1278,14 @@ nm_g_variant_ref (GVariant *v)
 	return v;
 }
 
+static inline GVariant *
+nm_g_variant_ref_sink (GVariant *v)
+{
+	if (v)
+		g_variant_ref_sink (v);
+	return v;
+}
+
 static inline void
 nm_g_variant_unref (GVariant *v)
 {
@@ -1226,6 +1293,14 @@ nm_g_variant_unref (GVariant *v)
 		g_variant_unref (v);
 }
 
+static inline GVariant *
+nm_g_variant_take_ref (GVariant *v)
+{
+	if (v)
+		g_variant_take_ref (v);
+	return v;
+}
+
 /*****************************************************************************/
 
 /* Determine whether @x is a power of two (@x being an integer type).
@@ -1494,6 +1569,11 @@ nm_strcmp_p (gconstpointer a, gconstpointer b)
 
 /*****************************************************************************/
 
+#define nm_g_slice_free(ptr) \
+	g_slice_free (typeof (*(ptr)), ptr)
+
+/*****************************************************************************/
+
 /* like g_memdup(). The difference is that the @size argument is of type
  * gsize, while g_memdup() has type guint. Since, the size of container types
  * like GArray is guint as well, this means trying to g_memdup() an
@@ -1523,15 +1603,72 @@ nm_memdup (gconstpointer data, gsize size)
 	return p;
 }
 
+#define nm_malloc_maybe_a(alloca_maxlen, bytes, to_free) \
+	({ \
+		const gsize _bytes = (bytes); \
+		typeof (to_free) _to_free = (to_free); \
+		typeof (*_to_free) _ptr; \
+		\
+		G_STATIC_ASSERT_EXPR ((alloca_maxlen) <= 500); \
+		nm_assert (_to_free && !*_to_free); \
+		\
+		if (_bytes <= (alloca_maxlen)) { \
+			_ptr = g_alloca (_bytes); \
+		} else { \
+			_ptr = g_malloc (_bytes); \
+			*_to_free = _ptr; \
+		}; \
+		\
+		_ptr; \
+	})
+
+#define nm_malloc0_maybe_a(alloca_maxlen, bytes, to_free) \
+	({ \
+		const gsize _bytes = (bytes); \
+		typeof (to_free) _to_free = (to_free); \
+		typeof (*_to_free) _ptr; \
+		\
+		G_STATIC_ASSERT_EXPR ((alloca_maxlen) <= 500); \
+		nm_assert (_to_free && !*_to_free); \
+		\
+		if (_bytes <= (alloca_maxlen)) { \
+			_ptr = g_alloca (_bytes); \
+			memset (_ptr, 0, _bytes); \
+		} else { \
+			_ptr = g_malloc0 (_bytes); \
+			*_to_free = _ptr; \
+		}; \
+		\
+		_ptr; \
+	})
+
+#define nm_memdup_maybe_a(alloca_maxlen, data, size, to_free) \
+	({ \
+		const gsize _size = (size); \
+		typeof (to_free) _to_free_md = (to_free); \
+		typeof (*_to_free_md) _ptr_md = NULL; \
+		\
+		nm_assert (_to_free_md && !*_to_free_md); \
+		\
+		if (_size > 0u) { \
+			_ptr_md = nm_malloc_maybe_a ((alloca_maxlen), _size, _to_free_md); \
+			memcpy (_ptr_md, (data), _size); \
+		} \
+		\
+		_ptr_md; \
+	})
+
 static inline char *
 _nm_strndup_a_step (char *s, const char *str, gsize len)
 {
 	NM_PRAGMA_WARNING_DISABLE ("-Wstringop-truncation");
+	NM_PRAGMA_WARNING_DISABLE ("-Wstringop-overflow");
 	if (len > 0)
 		strncpy (s, str, len);
 	s[len] = '\0';
 	return s;
 	NM_PRAGMA_WARNING_REENABLE;
+	NM_PRAGMA_WARNING_REENABLE;
 }
 
 /* Similar to g_strndup(), however, if the string (including the terminating
diff --git a/shared/nm-glib-aux/nm-obj.h b/shared/nm-glib-aux/nm-obj.h
index 4edd1f3e..06016bdd 100644
--- a/shared/nm-glib-aux/nm-obj.h
+++ b/shared/nm-glib-aux/nm-obj.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
diff --git a/shared/nm-glib-aux/nm-random-utils.c b/shared/nm-glib-aux/nm-random-utils.c
index d7c7da42..f56f8b99 100644
--- a/shared/nm-glib-aux/nm-random-utils.c
+++ b/shared/nm-glib-aux/nm-random-utils.c
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
diff --git a/shared/nm-glib-aux/nm-random-utils.h b/shared/nm-glib-aux/nm-random-utils.h
index 15a118d3..8e134ee9 100644
--- a/shared/nm-glib-aux/nm-random-utils.h
+++ b/shared/nm-glib-aux/nm-random-utils.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
diff --git a/shared/nm-glib-aux/nm-secret-utils.c b/shared/nm-glib-aux/nm-secret-utils.c
index 81f8b5ae..aeb88877 100644
--- a/shared/nm-glib-aux/nm-secret-utils.c
+++ b/shared/nm-glib-aux/nm-secret-utils.c
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
diff --git a/shared/nm-glib-aux/nm-secret-utils.h b/shared/nm-glib-aux/nm-secret-utils.h
index 034ef7bd..0fd1ac8b 100644
--- a/shared/nm-glib-aux/nm-secret-utils.h
+++ b/shared/nm-glib-aux/nm-secret-utils.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
diff --git a/shared/nm-glib-aux/nm-shared-utils.c b/shared/nm-glib-aux/nm-shared-utils.c
index cf08a77f..c8a253a6 100644
--- a/shared/nm-glib-aux/nm-shared-utils.c
+++ b/shared/nm-glib-aux/nm-shared-utils.c
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
@@ -734,10 +733,7 @@ _nm_utils_ascii_str_to_int64 (const char *str, guint base, gint64 min, gint64 ma
 	gint64 v;
 	const char *s = NULL;
 
-	if (str) {
-		while (g_ascii_isspace (str[0]))
-			str++;
-	}
+	str = nm_str_skip_leading_spaces (str);
 	if (!str || !str[0]) {
 		errno = EINVAL;
 		return fallback;
@@ -748,9 +744,9 @@ _nm_utils_ascii_str_to_int64 (const char *str, guint base, gint64 min, gint64 ma
 
 	if (errno != 0)
 		return fallback;
+
 	if (s[0] != '\0') {
-		while (g_ascii_isspace (s[0]))
-			s++;
+		s = nm_str_skip_leading_spaces (s);
 		if (s[0] != '\0') {
 			errno = EINVAL;
 			return fallback;
@@ -810,6 +806,15 @@ _nm_utils_ascii_str_to_uint64 (const char *str, guint base, guint64 min, guint64
 
 /*****************************************************************************/
 
+int
+nm_strcmp_with_data (gconstpointer a, gconstpointer b, gpointer user_data)
+{
+	const char *s1 = a;
+	const char *s2 = b;
+
+	return strcmp (s1, s2);
+}
+
 /* like nm_strcmp_p(), suitable for g_ptr_array_sort_with_data().
  * g_ptr_array_sort() just casts nm_strcmp_p() to a function of different
  * signature. I guess, in glib there are knowledgeable people that ensure
@@ -827,6 +832,15 @@ nm_strcmp_p_with_data (gconstpointer a, gconstpointer b, gpointer user_data)
 }
 
 int
+nm_strcmp0_p_with_data (gconstpointer a, gconstpointer b, gpointer user_data)
+{
+	const char *s1 = *((const char **) a);
+	const char *s2 = *((const char **) b);
+
+	return nm_strcmp0 (s1, s2);
+}
+
+int
 nm_cmp_uint32_p_with_data (gconstpointer p_a, gconstpointer p_b, gpointer user_data)
 {
 	const guint32 a = *((const guint32 *) p_a);
@@ -1350,6 +1364,8 @@ _nm_utils_strv_cleanup (char **strv,
 		return strv;
 
 	if (strip_whitespace) {
+		/* we only modify the strings pointed to by @strv if @strip_whitespace is
+		 * requested. Otherwise, the strings themselves are untouched. */
 		for (i = 0; strv[i]; i++)
 			g_strstrip (strv[i]);
 	}
@@ -2130,6 +2146,8 @@ nm_utils_fd_read_loop_exact (int fd, void *buf, size_t nbytes, bool do_poll)
 	return 0;
 }
 
+/*****************************************************************************/
+
 NMUtilsNamedValue *
 nm_utils_named_values_from_str_dict (GHashTable *hash, guint *out_len)
 {
@@ -2154,15 +2172,106 @@ nm_utils_named_values_from_str_dict (GHashTable *hash, guint *out_len)
 	values[i].name = NULL;
 	values[i].value_ptr = NULL;
 
-	if (len > 1) {
-		g_qsort_with_data (values, len, sizeof (values[0]),
-		                   nm_utils_named_entry_cmp_with_data, NULL);
-	}
+	nm_utils_named_value_list_sort (values, len, NULL, NULL);
 
 	NM_SET_OUT (out_len, len);
 	return values;
 }
 
+gssize
+nm_utils_named_value_list_find (const NMUtilsNamedValue *arr,
+                                gsize len,
+                                const char *name,
+                                gboolean sorted)
+{
+	gsize i;
+
+	nm_assert (name);
+
+#if NM_MORE_ASSERTS > 5
+	{
+		for (i = 0; i < len; i++) {
+			const NMUtilsNamedValue *v = &arr[i];
+
+			nm_assert (v->name);
+			if (   sorted
+			    && i > 0)
+				nm_assert (strcmp (arr[i - 1].name, v->name) < 0);
+		}
+	}
+
+	nm_assert (   !sorted
+	           || nm_utils_named_value_list_is_sorted (arr, len, FALSE, NULL, NULL));
+#endif
+
+	if (sorted) {
+		return nm_utils_array_find_binary_search (arr,
+		                                          sizeof (NMUtilsNamedValue),
+		                                          len,
+		                                          &name,
+		                                          nm_strcmp_p_with_data,
+		                                          NULL);
+	}
+	for (i = 0; i < len; i++) {
+		if (nm_streq (arr[i].name, name))
+			return i;
+	}
+	return ~((gssize) len);
+}
+
+gboolean
+nm_utils_named_value_list_is_sorted (const NMUtilsNamedValue *arr,
+                                     gsize len,
+                                     gboolean accept_duplicates,
+                                     GCompareDataFunc compare_func,
+                                     gpointer user_data)
+{
+	gsize i;
+	int c_limit;
+
+	if (len == 0)
+		return TRUE;
+
+	g_return_val_if_fail (arr, FALSE);
+
+	if (!compare_func)
+		compare_func = nm_strcmp_p_with_data;
+
+	c_limit = accept_duplicates ? 0 : -1;
+
+	for (i = 1; i < len; i++) {
+		int c;
+
+		c = compare_func (&arr[i - 1], &arr[i], user_data);
+		if (c > c_limit)
+			return FALSE;
+	}
+	return TRUE;
+}
+
+void
+nm_utils_named_value_list_sort (NMUtilsNamedValue *arr,
+                                gsize len,
+                                GCompareDataFunc compare_func,
+                                gpointer user_data)
+{
+	if (len == 0)
+		return;
+
+	g_return_if_fail (arr);
+
+	if (len == 1)
+		return;
+
+	g_qsort_with_data (arr,
+	                   len,
+	                   sizeof (NMUtilsNamedValue),
+	                   compare_func ?: nm_strcmp_p_with_data,
+	                   user_data);
+}
+
+/*****************************************************************************/
+
 gpointer *
 nm_utils_hash_keys_to_array (GHashTable *hash,
                              GCompareDataFunc compare_func,
@@ -2193,12 +2302,41 @@ nm_utils_hash_keys_to_array (GHashTable *hash,
 	return keys;
 }
 
+gboolean
+nm_utils_hashtable_same_keys (const GHashTable *a,
+                              const GHashTable *b)
+{
+	GHashTableIter h;
+	const char *k;
+
+	if (a == b)
+		return TRUE;
+	if (!a || !b)
+		return FALSE;
+	if (g_hash_table_size ((GHashTable *) a) != g_hash_table_size ((GHashTable *) b))
+		return FALSE;
+
+	g_hash_table_iter_init (&h, (GHashTable *) a);
+	while (g_hash_table_iter_next (&h, (gpointer) &k, NULL)) {
+		if (!g_hash_table_contains ((GHashTable *) b, k))
+			return FALSE;
+	}
+
+#if NM_MORE_ASSERTS > 5
+	g_hash_table_iter_init (&h, (GHashTable *) b);
+	while (g_hash_table_iter_next (&h, (gpointer) &k, NULL))
+		nm_assert (g_hash_table_contains ((GHashTable *) a, k));
+#endif
+
+	return TRUE;
+}
+
 char **
 nm_utils_strv_make_deep_copied (const char **strv)
 {
 	gsize i;
 
-	/* it takes a strv dictionary, and copies each
+	/* it takes a strv list, and copies each
 	 * strings. Note that this updates @strv *in-place*
 	 * and returns it. */
 
@@ -2210,6 +2348,79 @@ nm_utils_strv_make_deep_copied (const char **strv)
 	return (char **) strv;
 }
 
+char **
+nm_utils_strv_make_deep_copied_n (const char **strv, gsize len)
+{
+	gsize i;
+
+	/* it takes a strv array with len elements, and copies each
+	 * strings. Note that this updates @strv *in-place*
+	 * and returns it. */
+
+	if (!strv)
+		return NULL;
+	for (i = 0; i < len; i++)
+		strv[i] = g_strdup (strv[i]);
+
+	return (char **) strv;
+}
+
+/**
+ * @strv: the strv array to copy. It may be %NULL if @len
+ *   is negative or zero (in which case %NULL will be returned).
+ * @len: the length of strings in @str. If negative, strv is assumed
+ *   to be a NULL terminated array.
+ *
+ * Like g_strdupv(), with two differences:
+ *
+ * - accepts a @len parameter for non-null terminated strv array.
+ *
+ * - this never returns an empty strv array, but always %NULL if
+ *   there are no strings.
+ *
+ * Note that if @len is non-negative, then it still must not
+ * contain any %NULL pointers within the first @len elements.
+ * Otherwise you would leak elements if you try to free the
+ * array with g_strfreev(). Allowing that would be error prone.
+ *
+ * Returns: (transfer full): a clone of the strv array. Always
+ *   %NULL terminated.
+ */
+char **
+nm_utils_strv_dup (gpointer strv, gssize len)
+{
+	gsize i, l;
+	char **v;
+	const char *const *const src = strv;
+
+	if (len < 0)
+		l = NM_PTRARRAY_LEN (src);
+	else
+		l = len;
+	if (l == 0) {
+		/* this function never returns an empty strv array. If you
+		 * need that, handle it yourself. */
+		return NULL;
+	}
+
+	v = g_new (char *, l + 1);
+	for (i = 0; i < l; i++) {
+
+		if (G_UNLIKELY (!src[i])) {
+			/* NULL strings are not allowed. Clear the remainder of the array
+			 * and return it (with assertion failure). */
+			l++;
+			for (; i < l; i++)
+				v[i] = NULL;
+			g_return_val_if_reached (v);
+		}
+
+		v[i] = g_strdup (src[i]);
+	}
+	v[l] = NULL;
+	return v;
+}
+
 /*****************************************************************************/
 
 gssize
@@ -2499,8 +2710,8 @@ fail:
  * @len: the number of elements in strv. If negative,
  *   strv must be a NULL terminated array and the length
  *   will be calculated first. If @len is a positive
- *   number, all first @len elements in @strv must be
- *   non-NULL, valid strings.
+ *   number, @strv is allowed to contain %NULL strings
+ *   too.
  *
  * Ascending sort of the array @strv inplace, using plain strcmp() string
  * comparison.
@@ -2508,9 +2719,16 @@ fail:
 void
 _nm_utils_strv_sort (const char **strv, gssize len)
 {
+	GCompareDataFunc cmp;
 	gsize l;
 
-	l = len < 0 ? (gsize) NM_PTRARRAY_LEN (strv) : (gsize) len;
+	if (len < 0) {
+		l = NM_PTRARRAY_LEN (strv);
+		cmp = nm_strcmp_p_with_data;
+	} else {
+		l = len;
+		cmp = nm_strcmp0_p_with_data;
+	}
 
 	if (l <= 1)
 		return;
@@ -2520,7 +2738,7 @@ _nm_utils_strv_sort (const char **strv, gssize len)
 	g_qsort_with_data (strv,
 	                   l,
 	                   sizeof (const char *),
-	                   nm_strcmp_p_with_data,
+	                   cmp,
 	                   NULL);
 }
 
@@ -2577,6 +2795,53 @@ _nm_utils_strv_cmp_n (const char *const*strv1,
 
 /*****************************************************************************/
 
+/**
+ * nm_utils_g_slist_find_str:
+ * @list: the #GSList with NUL terminated strings to search
+ * @needle: the needle string to look for.
+ *
+ * Search the list for @needle and return the first found match
+ * (or %NULL if not found). Uses strcmp() for finding the first matching
+ * element.
+ *
+ * Returns: the #GSList element with @needle as string value or
+ *   %NULL if not found.
+ */
+GSList *
+nm_utils_g_slist_find_str (const GSList *list,
+                           const char *needle)
+{
+	nm_assert (needle);
+
+	for (; list; list = list->next) {
+		nm_assert (list->data);
+		if (nm_streq (list->data, needle))
+			return (GSList *) list;
+	}
+	return NULL;
+}
+
+/**
+ * nm_utils_g_slist_strlist_cmp:
+ * @a: the left #GSList of strings
+ * @b: the right #GSList of strings to compare.
+ *
+ * Compares two string lists. The data elements are compared with
+ * strcmp(), alloing %NULL elements.
+ *
+ * Returns: 0, 1, or -1, depending on how the lists compare.
+ */
+int
+nm_utils_g_slist_strlist_cmp (const GSList *a, const GSList *b)
+{
+	for (; a && b; a = a->next, b = b->next)
+		NM_CMP_DIRECT_STRCMP0 (a->data, b->data);
+	NM_CMP_SELF (a, b);
+	return 0;
+}
+
+/*****************************************************************************/
+
 gpointer
 _nm_utils_user_data_pack (int nargs, gconstpointer *args)
 {
@@ -2753,10 +3018,13 @@ nm_utils_memeqzero (gconstpointer data, gsize length)
  *   be returned and must be freed by the caller.
  *   If not %NULL, the buffer must already be preallocated and contain
  *   at least (@length*2+1) or (@length*3) bytes, depending on the delimiter.
+ *   If @length is zero, then of course at least one byte will be allocated
+ *   or @out (if given) must contain at least room for the trailing NUL byte.
  *
  * Returns: the binary value converted to a hex string. If @out is given,
  *   this always returns @out. If @out is %NULL, a newly allocated string
- *   is returned.
+ *   is returned. This never returns %NULL, for buffers of length zero
+ *   an empty string is returend.
  */
 char *
 nm_utils_bin2hexstr_full (gconstpointer addr,
@@ -2772,9 +3040,11 @@ nm_utils_bin2hexstr_full (gconstpointer addr,
 	if (out)
 		out0 = out;
 	else {
-		out0 = out = g_new (char, delimiter == '\0'
-		                          ? length * 2 + 1
-		                          : length * 3);
+		out0 = out = g_new (char, length == 0
+		                          ? 1u
+		                          : (  delimiter == '\0'
+		                             ? length * 2u + 1u
+		                             : length * 3u));
 	}
 
 	/* @out must contain at least @length*3 bytes if @delimiter is set,
@@ -2939,3 +3209,64 @@ fail:
 	NM_SET_OUT (out_len, 0);
 	return NULL;
 }
+
+/*****************************************************************************/
+
+GVariant *
+nm_utils_gvariant_vardict_filter (GVariant *src,
+                                  gboolean (*filter_fcn) (const char *key,
+                                                          GVariant *val,
+                                                          char **out_key,
+                                                          GVariant **out_val,
+                                                          gpointer user_data),
+                                  gpointer user_data)
+{
+	GVariantIter iter;
+	GVariantBuilder builder;
+	const char *key;
+	GVariant *val;
+
+	g_return_val_if_fail (src && g_variant_is_of_type (src, G_VARIANT_TYPE_VARDICT), NULL);
+	g_return_val_if_fail (filter_fcn, NULL);
+
+	g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT);
+
+	g_variant_iter_init (&iter, src);
+	while (g_variant_iter_next (&iter, "{&sv}", &key, &val)) {
+		_nm_unused gs_unref_variant GVariant *val_free = val;
+		gs_free char *key2 = NULL;
+		gs_unref_variant GVariant *val2 = NULL;
+
+		if (filter_fcn (key,
+		                val,
+		                &key2,
+		                &val2,
+		                user_data)) {
+			g_variant_builder_add (&builder,
+			                       "{sv}",
+			                       key2 ?: key,
+			                       val2 ?: val);
+		}
+	}
+
+	return g_variant_builder_end (&builder);
+}
+
+static gboolean
+_gvariant_vardict_filter_drop_one (const char *key,
+                                   GVariant *val,
+                                   char **out_key,
+                                   GVariant **out_val,
+                                   gpointer user_data)
+{
+	return !nm_streq (key, user_data);
+}
+
+GVariant *
+nm_utils_gvariant_vardict_filter_drop_one (GVariant *src,
+                                           const char *key)
+{
+	return nm_utils_gvariant_vardict_filter (src,
+	                                         _gvariant_vardict_filter_drop_one,
+	                                         (gpointer) key);
+}
diff --git a/shared/nm-glib-aux/nm-shared-utils.h b/shared/nm-glib-aux/nm-shared-utils.h
index af3c2f83..d9c430d4 100644
--- a/shared/nm-glib-aux/nm-shared-utils.h
+++ b/shared/nm-glib-aux/nm-shared-utils.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
@@ -171,6 +170,13 @@ nm_ip4_addr_is_localhost (in_addr_t addr4)
             return _cc < 0 ? -1 : 1; \
     } G_STMT_END
 
+#define NM_CMP_RETURN_DIRECT(c) \
+    G_STMT_START { \
+        const int _cc = (c); \
+        if (_cc) \
+            return _cc; \
+    } G_STMT_END
+
 #define NM_CMP_SELF(a, b) \
     G_STMT_START { \
         typeof (a) _a = (a); \
@@ -196,8 +202,11 @@ nm_ip4_addr_is_localhost (in_addr_t addr4)
 #define NM_CMP_DIRECT_MEMCMP(a, b, size) \
     NM_CMP_RETURN (memcmp ((a), (b), (size)))
 
+#define NM_CMP_DIRECT_STRCMP(a, b) \
+    NM_CMP_RETURN_DIRECT (strcmp ((a), (b)))
+
 #define NM_CMP_DIRECT_STRCMP0(a, b) \
-    NM_CMP_RETURN (g_strcmp0 ((a), (b)))
+    NM_CMP_RETURN_DIRECT (nm_strcmp0 ((a), (b)))
 
 #define NM_CMP_DIRECT_IN6ADDR(a, b) \
     G_STMT_START { \
@@ -229,16 +238,16 @@ nm_ip4_addr_is_localhost (in_addr_t addr4)
         const char *_b = ((b)->field); \
         \
         if (_a != _b) { \
-            NM_CMP_RETURN (g_strcmp0 (_a, _b)); \
+            NM_CMP_RETURN_DIRECT (nm_strcmp0 (_a, _b)); \
         } \
     } G_STMT_END
 
 #define NM_CMP_FIELD_STR0(a, b, field) \
-    NM_CMP_RETURN (g_strcmp0 (((a)->field), ((b)->field)))
+    NM_CMP_RETURN_DIRECT (nm_strcmp0 (((a)->field), ((b)->field)))
 
 #define NM_CMP_FIELD_MEMCMP_LEN(a, b, field, len) \
     NM_CMP_RETURN (memcmp (&((a)->field), &((b)->field), \
-                           MIN (len, sizeof ((a)->field))))
+                           NM_MIN (len, sizeof ((a)->field))))
 
 #define NM_CMP_FIELD_MEMCMP(a, b, field) \
     NM_CMP_RETURN (memcmp (&((a)->field), \
@@ -305,6 +314,20 @@ GVariant *nm_utils_gbytes_to_variant_ay (GBytes *bytes);
 
 /*****************************************************************************/
 
+GVariant *nm_utils_gvariant_vardict_filter (GVariant *src,
+                                            gboolean (*filter_fcn) (const char *key,
+                                                                    GVariant *val,
+                                                                    char **out_key,
+                                                                    GVariant **out_val,
+                                                                    gpointer user_data),
+                                            gpointer user_data);
+
+GVariant *
+nm_utils_gvariant_vardict_filter_drop_one (GVariant *src,
+                                           const char *key);
+
+/*****************************************************************************/
+
 static inline int
 nm_utils_hexchar_to_int (char ch)
 {
@@ -693,6 +716,8 @@ _nm_g_slice_free_fcn_define (16)
  * @NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY: the profile is currently not
  *   available/compatible with the device, but this may be only temporary.
  *
+ * @NM_UTILS_ERROR_SETTING_MISSING: the setting is missing
+ *
  * @NM_UTILS_ERROR_INVALID_ARGUMENT: invalid argument.
  */
 typedef enum {
@@ -715,6 +740,8 @@ typedef enum {
 	NM_UTILS_ERROR_CONNECTION_AVAILABLE_UNMANAGED_DEVICE,
 	NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
 
+	NM_UTILS_ERROR_SETTING_MISSING,
+
 } NMUtilsError;
 
 #define NM_UTILS_ERROR (nm_utils_error_quark ())
@@ -735,7 +762,13 @@ nm_utils_error_set_literal (GError **error, int error_code, const char *literal)
 }
 
 #define nm_utils_error_set(error, error_code, ...) \
-	g_set_error ((error), NM_UTILS_ERROR, error_code, __VA_ARGS__)
+	G_STMT_START { \
+		if (NM_NARG (__VA_ARGS__) == 1) { \
+			g_set_error_literal ((error), NM_UTILS_ERROR, (error_code), _NM_UTILS_MACRO_FIRST (__VA_ARGS__)); \
+		} else { \
+			g_set_error ((error), NM_UTILS_ERROR, (error_code), __VA_ARGS__); \
+		} \
+	} G_STMT_END
 
 #define nm_utils_error_set_errno(error, errsv, fmt, ...) \
 	G_STMT_START { \
@@ -885,7 +918,9 @@ nm_utf8_collate0 (const char *a, const char *b)
 	return g_utf8_collate (a, b);
 }
 
+int nm_strcmp_with_data (gconstpointer a, gconstpointer b, gpointer user_data);
 int nm_strcmp_p_with_data (gconstpointer a, gconstpointer b, gpointer user_data);
+int nm_strcmp0_p_with_data (gconstpointer a, gconstpointer b, gpointer user_data);
 int nm_cmp_uint32_p_with_data (gconstpointer p_a, gconstpointer p_b, gpointer user_data);
 int nm_cmp_int2ptr_p_with_data (gconstpointer p_a, gconstpointer p_b, gpointer user_data);
 
@@ -906,11 +941,26 @@ typedef struct {
 	};
 } NMUtilsNamedValue;
 
-#define nm_utils_named_entry_cmp           nm_strcmp_p
-#define nm_utils_named_entry_cmp_with_data nm_strcmp_p_with_data
-
 NMUtilsNamedValue *nm_utils_named_values_from_str_dict (GHashTable *hash, guint *out_len);
 
+gssize nm_utils_named_value_list_find (const NMUtilsNamedValue *arr,
+                                       gsize len,
+                                       const char *name,
+                                       gboolean sorted);
+
+gboolean nm_utils_named_value_list_is_sorted (const NMUtilsNamedValue *arr,
+                                              gsize len,
+                                              gboolean accept_duplicates,
+                                              GCompareDataFunc compare_func,
+                                              gpointer user_data);
+
+void nm_utils_named_value_list_sort (NMUtilsNamedValue *arr,
+                                     gsize len,
+                                     GCompareDataFunc compare_func,
+                                     gpointer user_data);
+
+/*****************************************************************************/
+
 gpointer *nm_utils_hash_keys_to_array (GHashTable *hash,
                                        GCompareDataFunc compare_func,
                                        gpointer user_data,
@@ -927,14 +977,28 @@ nm_utils_strdict_get_keys (const GHashTable *hash,
 	                                                    out_length);
 }
 
+gboolean nm_utils_hashtable_same_keys (const GHashTable *a,
+                                       const GHashTable *b);
+
 char **nm_utils_strv_make_deep_copied (const char **strv);
 
+char **nm_utils_strv_make_deep_copied_n (const char **strv, gsize len);
+
 static inline char **
 nm_utils_strv_make_deep_copied_nonnull (const char **strv)
 {
 	return nm_utils_strv_make_deep_copied (strv) ?: g_new0 (char *, 1);
 }
 
+char **nm_utils_strv_dup (gpointer strv, gssize len);
+
+/*****************************************************************************/
+
+GSList *nm_utils_g_slist_find_str (const GSList *list,
+                                   const char *needle);
+
+int nm_utils_g_slist_strlist_cmp (const GSList *a, const GSList *b);
+
 /*****************************************************************************/
 
 gssize nm_utils_ptrarray_find_binary_search (gconstpointer *list,
diff --git a/shared/nm-glib-aux/nm-time-utils.c b/shared/nm-glib-aux/nm-time-utils.c
index ae526c34..7735f29d 100644
--- a/shared/nm-glib-aux/nm-time-utils.c
+++ b/shared/nm-glib-aux/nm-time-utils.c
@@ -22,6 +22,8 @@
 
 #include "nm-time-utils.h"
 
+#include "nm-logging-fwd.h"
+
 /*****************************************************************************/
 
 typedef struct {
@@ -229,15 +231,15 @@ nm_utils_get_monotonic_timestamp_s (void)
 /**
  * nm_utils_monotonic_timestamp_as_boottime:
  * @timestamp: the monotonic-timestamp that should be converted into CLOCK_BOOTTIME.
- * @timestamp_ns_per_tick: How many nano seconds make one unit of @timestamp? E.g. if
- * @timestamp is in unit seconds, pass %NM_UTILS_NS_PER_SECOND; @timestamp in nano
- * seconds, pass 1; @timestamp in milli seconds, pass %NM_UTILS_NS_PER_SECOND/1000; etc.
+ * @timestamp_ns_per_tick: How many nanoseconds make one unit of @timestamp? E.g. if
+ *   @timestamp is in unit seconds, pass %NM_UTILS_NS_PER_SECOND; if @timestamp is
+ *   in nanoseconds, pass 1; if @timestamp is in milliseconds, pass %NM_UTILS_NS_PER_SECOND/1000.
  *
  * Returns: the monotonic-timestamp as CLOCK_BOOTTIME, as returned by clock_gettime().
- * The unit is the same as the passed in @timestamp basd on @timestamp_ns_per_tick.
- * E.g. if you passed @timestamp in as seconds, it will return boottime in seconds.
- * If @timestamp is a non-positive, it returns -1. Note that a (valid) monotonic-timestamp
- * is always positive.
+ *   The unit is the same as the passed in @timestamp based on @timestamp_ns_per_tick.
+ *   E.g. if you passed @timestamp in as seconds, it will return boottime in seconds.
+ *   If @timestamp is non-positive, it returns -1. Note that a (valid) monotonic-timestamp
+ *   is always positive.
  *
  * On older kernels that don't support CLOCK_BOOTTIME, the returned time is instead CLOCK_MONOTONIC.
  **/
@@ -263,6 +265,8 @@ nm_utils_monotonic_timestamp_as_boottime (gint64 timestamp, gint64 timestamp_ns_
 
 	p = _t_get_global_state ();
 
+	nm_assert (p->offset_sec <= 0);
+
 	/* calculate the offset of monotonic-timestamp to boottime. offset_s is <= 1. */
 	offset = p->offset_sec * (NM_UTILS_NS_PER_SECOND / timestamp_ns_per_tick);
 
@@ -271,3 +275,23 @@ nm_utils_monotonic_timestamp_as_boottime (gint64 timestamp, gint64 timestamp_ns_
 
 	return timestamp - offset;
 }
+
+gint64
+nm_utils_clock_gettime_ns (clockid_t clockid)
+{
+	struct timespec tp;
+
+	if (clock_gettime (clockid, &tp) != 0)
+		return -NM_ERRNO_NATIVE (errno);
+	return nm_utils_timespec_to_ns (&tp);
+}
+
+gint64
+nm_utils_clock_gettime_ms (clockid_t clockid)
+{
+	struct timespec tp;
+
+	if (clock_gettime (clockid, &tp) != 0)
+		return -NM_ERRNO_NATIVE (errno);
+	return nm_utils_timespec_to_ms (&tp);
+}
diff --git a/shared/nm-glib-aux/nm-time-utils.h b/shared/nm-glib-aux/nm-time-utils.h
index 7e4f4f25..52d6637d 100644
--- a/shared/nm-glib-aux/nm-time-utils.h
+++ b/shared/nm-glib-aux/nm-time-utils.h
@@ -21,6 +21,22 @@
 #ifndef __NM_TIME_UTILS_H__
 #define __NM_TIME_UTILS_H__
 
+#include <time.h>
+
+static inline gint64
+nm_utils_timespec_to_ns (const struct timespec *ts)
+{
+	return   (((gint64) ts->tv_sec) * ((gint64) NM_UTILS_NS_PER_SECOND))
+	       +  ((gint64) ts->tv_nsec);
+}
+
+static inline gint64
+nm_utils_timespec_to_ms (const struct timespec *ts)
+{
+	return   (((gint64) ts->tv_sec)  * ((gint64) 1000))
+	       + (((gint64) ts->tv_nsec) / ((gint64) NM_UTILS_NS_PER_SECOND / 1000));
+}
+
 gint64 nm_utils_get_monotonic_timestamp_ns (void);
 gint64 nm_utils_get_monotonic_timestamp_us (void);
 gint64 nm_utils_get_monotonic_timestamp_ms (void);
@@ -34,12 +50,7 @@ nm_utils_get_monotonic_timestamp_ns_cached (gint64 *cache_now)
 	       ?: (*cache_now = nm_utils_get_monotonic_timestamp_ns ());
 }
 
-struct timespec;
-
-/* this function must be implemented to handle the notification when
- * the first monotonic-timestamp is fetched. */
-extern void _nm_utils_monotonic_timestamp_initialized (const struct timespec *tp,
-                                                       gint64 offset_sec,
-                                                       gboolean is_boottime);
+gint64 nm_utils_clock_gettime_ns (clockid_t clockid);
+gint64 nm_utils_clock_gettime_ms (clockid_t clockid);
 
 #endif /* __NM_TIME_UTILS_H__ */
diff --git a/shared/nm-glib-aux/nm-value-type.h b/shared/nm-glib-aux/nm-value-type.h
new file mode 100644
index 00000000..b4d6898f
--- /dev/null
+++ b/shared/nm-glib-aux/nm-value-type.h
@@ -0,0 +1,208 @@
+/*
+ * This library 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 of the License, or (at your option) any later version.
+ *
+ * This library 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 library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ *
+ * Copyright 2019 Red Hat, Inc.
+ */
+
+#ifndef __NM_VALUE_TYPE_H__
+#define __NM_VALUE_TYPE_H__
+
+typedef enum {
+	NM_VALUE_TYPE_UNSPEC = 1,
+	NM_VALUE_TYPE_BOOL   = 2,
+	NM_VALUE_TYPE_INT32  = 3,
+	NM_VALUE_TYPE_INT    = 4,
+	NM_VALUE_TYPE_STRING = 5,
+} NMValueType;
+
+/*****************************************************************************/
+
+#ifdef NM_VALUE_TYPE_DEFINE_FUNCTIONS
+
+typedef union {
+	bool             v_bool;
+	gint32           v_int32;
+	int              v_int;
+	const char      *v_string;
+
+	/* for convenience, also let the union contain other pointer types. These are
+	 * for NM_VALUE_TYPE_UNSPEC. */
+	gconstpointer   *v_ptr;
+	const GPtrArray *v_ptrarray;
+
+} NMValueTypUnion;
+
+/* Set the NMValueTypUnion. You can also assign the member directly.
+ * The only purpose of this is that it also returns a pointer to the
+ * union. So, you can do
+ *
+ *   ptr = NM_VALUE_TYP_UNION_SET (&value_typ_union_storage, v_bool, TRUE);
+ */
+#define NM_VALUE_TYP_UNION_SET(_arg, _type, _val) \
+	({ \
+		NMValueTypUnion *const _arg2 = (_arg); \
+		\
+		*_arg2 = (NMValueTypUnion) { \
+			._type = (_val), \
+		}; \
+		_arg2; \
+	})
+
+typedef struct {
+	bool has;
+	NMValueTypUnion val;
+} NMValueTypUnioMaybe;
+
+#define NM_VALUE_TYP_UNIO_MAYBE_SET(_arg, _type, _val) \
+	({ \
+		NMValueTypUnioMaybe *const _arg2 = (_arg); \
+		\
+		*_arg2 = (NMValueTypUnioMaybe) { \
+			.has       = TRUE, \
+			.val._type = (_val), \
+		}; \
+		_arg2; \
+	})
+
+/*****************************************************************************/
+
+static inline int
+nm_value_type_cmp (NMValueType value_type,
+                   gconstpointer p_a,
+                   gconstpointer p_b)
+{
+	switch (value_type) {
+	case NM_VALUE_TYPE_BOOL:   NM_CMP_DIRECT (*((const bool   *) p_a), *((const bool   *) p_b)); return 0;
+	case NM_VALUE_TYPE_INT32:  NM_CMP_DIRECT (*((const gint32 *) p_a), *((const gint32 *) p_b)); return 0;
+	case NM_VALUE_TYPE_INT:    NM_CMP_DIRECT (*((const int    *) p_a), *((const int    *) p_b)); return 0;
+	case NM_VALUE_TYPE_STRING: return nm_strcmp0 (*((const char *const*) p_a), *((const char *const*) p_b));
+	case NM_VALUE_TYPE_UNSPEC:
+		break;
+	}
+	nm_assert_not_reached ();
+	return 0;
+}
+
+static inline gboolean
+nm_value_type_equal (NMValueType value_type,
+                     gconstpointer p_a,
+                     gconstpointer p_b)
+{
+	return nm_value_type_cmp (value_type, p_a, p_b) == 0;
+}
+
+static inline void
+nm_value_type_copy (NMValueType value_type,
+                    gpointer dst,
+                    gconstpointer src)
+{
+	switch (value_type) {
+	case NM_VALUE_TYPE_BOOL:   (*((bool   *) dst) = *((const bool   *) src)); return;
+	case NM_VALUE_TYPE_INT32:  (*((gint32 *) dst) = *((const gint32 *) src)); return;
+	case NM_VALUE_TYPE_INT:    (*((int    *) dst) = *((const int    *) src)); return;
+	case NM_VALUE_TYPE_STRING:
+		/* self assignment safe! */
+		if (*((char **) dst) != *((const char *const*) src)) {
+			g_free (*((char **) dst));
+			*((char **) dst) = g_strdup (*((const char *const*) src));
+		}
+		return;
+	case NM_VALUE_TYPE_UNSPEC:
+		break;
+	}
+	nm_assert_not_reached ();
+}
+
+static inline void
+nm_value_type_get_from_variant (NMValueType value_type,
+                                gpointer dst,
+                                GVariant *variant,
+                                gboolean clone)
+{
+	switch (value_type) {
+	case NM_VALUE_TYPE_BOOL:   *((bool   *) dst) = g_variant_get_boolean (variant); return;
+	case NM_VALUE_TYPE_INT32:  *((gint32 *) dst) = g_variant_get_int32 (variant);   return;
+	case NM_VALUE_TYPE_STRING:
+		if (clone) {
+			g_free (*((char **) dst));
+			*((char **) dst) = g_variant_dup_string (variant, NULL);
+		} else {
+			/* we don't clone the string, nor free the previous value. */
+			*((const char **) dst) = g_variant_get_string (variant, NULL);
+		}
+		return;
+
+	case NM_VALUE_TYPE_INT:
+		/* "int" also does not have a define variant type, because it's not
+		 * clear how many bits we would need. */
+
+		/* fall-through */
+	case NM_VALUE_TYPE_UNSPEC:
+		break;
+	}
+	nm_assert_not_reached ();
+}
+
+static inline GVariant *
+nm_value_type_to_variant (NMValueType value_type,
+                          gconstpointer src)
+{
+	const char *v_string;
+
+	switch (value_type) {
+	case NM_VALUE_TYPE_BOOL:   return g_variant_new_boolean (*((const bool   *) src));
+	case NM_VALUE_TYPE_INT32:  return g_variant_new_int32   (*((const gint32 *) src));;
+	case NM_VALUE_TYPE_STRING:
+		v_string = *((const char *const*) src);
+		return v_string ? g_variant_new_string (v_string) : NULL;
+
+	case NM_VALUE_TYPE_INT:
+		/* "int" also does not have a define variant type, because it's not
+		 * clear how many bits we would need. */
+
+		/* fall-through */
+	case NM_VALUE_TYPE_UNSPEC:
+		break;
+	}
+	nm_assert_not_reached ();
+	return NULL;
+}
+
+static inline const GVariantType *
+nm_value_type_get_variant_type (NMValueType value_type)
+{
+	switch (value_type) {
+	case NM_VALUE_TYPE_BOOL:   return G_VARIANT_TYPE_BOOLEAN;
+	case NM_VALUE_TYPE_INT32:  return G_VARIANT_TYPE_INT32;
+	case NM_VALUE_TYPE_STRING: return G_VARIANT_TYPE_STRING;
+
+	case NM_VALUE_TYPE_INT:
+		/* "int" also does not have a define variant type, because it's not
+		 * clear how many bits we would need. */
+
+		/* fall-through */
+	case NM_VALUE_TYPE_UNSPEC:
+		break;
+	}
+	nm_assert_not_reached ();
+	return NULL;
+}
+
+/*****************************************************************************/
+
+#endif /* NM_VALUE_TYPE_DEFINE_FUNCTIONS */
+
+#endif  /* __NM_VALUE_TYPE_H__ */
diff --git a/shared/nm-libnm-core-aux/nm-dispatcher-api.h b/shared/nm-libnm-core-aux/nm-dispatcher-api.h
index e6d0d92f..0ee0f0a8 100644
--- a/shared/nm-libnm-core-aux/nm-dispatcher-api.h
+++ b/shared/nm-libnm-core-aux/nm-dispatcher-api.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This program is free software; you can redistribute it and/or modify
@@ -21,11 +20,6 @@
 #ifndef __NM_DISPACHER_API_H__
 #define __NM_DISPACHER_API_H__
 
-#define NMD_SCRIPT_DIR_DEFAULT  NMCONFDIR "/dispatcher.d"
-#define NMD_SCRIPT_DIR_PRE_UP   NMD_SCRIPT_DIR_DEFAULT "/pre-up.d"
-#define NMD_SCRIPT_DIR_PRE_DOWN NMD_SCRIPT_DIR_DEFAULT "/pre-down.d"
-#define NMD_SCRIPT_DIR_NO_WAIT  NMD_SCRIPT_DIR_DEFAULT "/no-wait.d"
-
 #define NM_DISPATCHER_DBUS_SERVICE   "org.freedesktop.nm_dispatcher"
 #define NM_DISPATCHER_DBUS_INTERFACE "org.freedesktop.nm_dispatcher"
 #define NM_DISPATCHER_DBUS_PATH      "/org/freedesktop/nm_dispatcher"
diff --git a/shared/nm-libnm-core-aux/nm-libnm-core-aux.c b/shared/nm-libnm-core-aux/nm-libnm-core-aux.c
new file mode 100644
index 00000000..a04256b7
--- /dev/null
+++ b/shared/nm-libnm-core-aux/nm-libnm-core-aux.c
@@ -0,0 +1,373 @@
+/* NetworkManager -- Network link manager
+ *
+ * This library 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 of the License, or (at your option) any later version.
+ *
+ * This library 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 library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ *
+ * (C) Copyright 2019 Red Hat, Inc.
+ */
+
+#include "nm-default.h"
+
+#include "nm-libnm-core-aux.h"
+
+#include "nm-libnm-core-intern/nm-libnm-core-utils.h"
+
+/*****************************************************************************/
+
+typedef enum {
+	KEY_TYPE_STRING,
+	KEY_TYPE_INT,
+	KEY_TYPE_BOOL,
+} KeyType;
+
+typedef struct {
+	const char *str_val;
+	union {
+		int vint;
+		bool vbool;
+	} typ_val;
+} ParseData;
+
+typedef struct {
+	const char *name;
+	NMTeamLinkWatcherType watcher_type;
+	KeyType key_type;
+	union {
+		int         (*fint)    (const NMTeamLinkWatcher *watcher);
+		gboolean    (*fbool)   (const NMTeamLinkWatcher *watcher);
+		const char *(*fstring) (const NMTeamLinkWatcher *watcher);
+	} get_fcn;
+	union {
+		int vint;
+		bool vbool;
+	} def_val;
+} TeamLinkWatcherKeyInfo;
+
+static gboolean
+_team_link_watcher_validate_active (const NMTeamLinkWatcher *watcher)
+{
+	return NM_FLAGS_HAS (nm_team_link_watcher_get_flags (watcher), NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_VALIDATE_ACTIVE);
+}
+
+static gboolean
+_team_link_watcher_validate_inactive (const NMTeamLinkWatcher *watcher)
+{
+	return NM_FLAGS_HAS (nm_team_link_watcher_get_flags (watcher), NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_VALIDATE_INACTIVE);
+}
+
+static gboolean
+_team_link_watcher_send_always (const NMTeamLinkWatcher *watcher)
+{
+	return NM_FLAGS_HAS (nm_team_link_watcher_get_flags (watcher), NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_SEND_ALWAYS);
+}
+
+static const TeamLinkWatcherKeyInfo _team_link_watcher_key_infos[_NM_TEAM_LINK_WATCHER_KEY_NUM] = {
+
+#define _KEY_INFO(key_id, _name, _watcher_type, _key_type, ...) \
+	[key_id] = { .name = ""_name"", .watcher_type = (_watcher_type), .key_type = _key_type, ##__VA_ARGS__ }
+
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_NAME,              "name",              NM_TEAM_LINK_WATCHER_TYPE_ETHTOOL | NM_TEAM_LINK_WATCHER_TYPE_NSNAPING | NM_TEAM_LINK_WATCHER_TYPE_ARPING, KEY_TYPE_STRING, .get_fcn.fstring = nm_team_link_watcher_get_name,        ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_DELAY_UP,          "delay-up",          NM_TEAM_LINK_WATCHER_TYPE_ETHTOOL,                                                                         KEY_TYPE_INT,    .get_fcn.fint    = nm_team_link_watcher_get_delay_up,    ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_DELAY_DOWN,        "delay-down",        NM_TEAM_LINK_WATCHER_TYPE_ETHTOOL,                                                                         KEY_TYPE_INT,    .get_fcn.fint    = nm_team_link_watcher_get_delay_down,  ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_INIT_WAIT,         "init-wait",         NM_TEAM_LINK_WATCHER_TYPE_NSNAPING | NM_TEAM_LINK_WATCHER_TYPE_ARPING,                                     KEY_TYPE_INT,    .get_fcn.fint    = nm_team_link_watcher_get_init_wait,   ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_INTERVAL,          "interval",          NM_TEAM_LINK_WATCHER_TYPE_NSNAPING | NM_TEAM_LINK_WATCHER_TYPE_ARPING,                                     KEY_TYPE_INT,    .get_fcn.fint    = nm_team_link_watcher_get_interval,    ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_MISSED_MAX,        "missed-max",        NM_TEAM_LINK_WATCHER_TYPE_NSNAPING | NM_TEAM_LINK_WATCHER_TYPE_ARPING,                                     KEY_TYPE_INT,    .get_fcn.fint    = nm_team_link_watcher_get_missed_max,  .def_val.vint = 3, ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_TARGET_HOST,       "target-host",       NM_TEAM_LINK_WATCHER_TYPE_NSNAPING | NM_TEAM_LINK_WATCHER_TYPE_ARPING,                                     KEY_TYPE_STRING, .get_fcn.fstring = nm_team_link_watcher_get_target_host, ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_VLANID,            "vlanid",            NM_TEAM_LINK_WATCHER_TYPE_ARPING,                                                                          KEY_TYPE_INT,    .get_fcn.fint    = nm_team_link_watcher_get_vlanid,      .def_val.vint = -1, ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_SOURCE_HOST,       "source-host",       NM_TEAM_LINK_WATCHER_TYPE_ARPING,                                                                          KEY_TYPE_STRING, .get_fcn.fstring = nm_team_link_watcher_get_source_host, ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_VALIDATE_ACTIVE,   "validate-active",   NM_TEAM_LINK_WATCHER_TYPE_ARPING,                                                                          KEY_TYPE_BOOL,   .get_fcn.fbool   = _team_link_watcher_validate_active,   ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_VALIDATE_INACTIVE, "validate-inactive", NM_TEAM_LINK_WATCHER_TYPE_ARPING,                                                                          KEY_TYPE_BOOL,   .get_fcn.fbool   = _team_link_watcher_validate_inactive, ),
+	_KEY_INFO (NM_TEAM_LINK_WATCHER_KEY_SEND_ALWAYS,       "send-always",       NM_TEAM_LINK_WATCHER_TYPE_ARPING,                                                                          KEY_TYPE_BOOL,   .get_fcn.fbool   = _team_link_watcher_send_always,       ),
+
+};
+
+static NMTeamLinkWatcherType
+_team_link_watcher_get_watcher_type_from_name (const char *name)
+{
+	if (name) {
+		if (nm_streq (name, NM_TEAM_LINK_WATCHER_ETHTOOL))
+			return NM_TEAM_LINK_WATCHER_TYPE_ETHTOOL;
+		if (nm_streq (name, NM_TEAM_LINK_WATCHER_NSNA_PING))
+			return NM_TEAM_LINK_WATCHER_TYPE_NSNAPING;
+		if (nm_streq (name, NM_TEAM_LINK_WATCHER_ARP_PING))
+			return NM_TEAM_LINK_WATCHER_TYPE_ARPING;
+	}
+	return NM_TEAM_LINK_WATCHER_TYPE_NONE;
+}
+
+static const char *
+_parse_data_get_str (const ParseData parse_data[static _NM_TEAM_LINK_WATCHER_KEY_NUM],
+                     NMTeamLinkWatcherKeyId key_id)
+{
+	nm_assert (_NM_INT_NOT_NEGATIVE (key_id) && key_id < _NM_TEAM_LINK_WATCHER_KEY_NUM);
+	nm_assert (_team_link_watcher_key_infos[key_id].key_type == KEY_TYPE_STRING);
+
+	return parse_data[key_id].str_val;
+}
+
+static int
+_parse_data_get_int (const ParseData parse_data[static _NM_TEAM_LINK_WATCHER_KEY_NUM],
+                     NMTeamLinkWatcherKeyId key_id)
+{
+	nm_assert (_NM_INT_NOT_NEGATIVE (key_id) && key_id < _NM_TEAM_LINK_WATCHER_KEY_NUM);
+	nm_assert (_team_link_watcher_key_infos[key_id].key_type == KEY_TYPE_INT);
+
+	if (parse_data[key_id].str_val)
+		return parse_data[key_id].typ_val.vint;
+	return _team_link_watcher_key_infos[key_id].def_val.vint;
+}
+
+static int
+_parse_data_get_bool (const ParseData parse_data[static _NM_TEAM_LINK_WATCHER_KEY_NUM],
+                      NMTeamLinkWatcherKeyId key_id)
+{
+	nm_assert (_NM_INT_NOT_NEGATIVE (key_id) && key_id < _NM_TEAM_LINK_WATCHER_KEY_NUM);
+	nm_assert (_team_link_watcher_key_infos[key_id].key_type == KEY_TYPE_BOOL);
+
+	if (parse_data[key_id].str_val)
+		return parse_data[key_id].typ_val.vbool;
+	return _team_link_watcher_key_infos[key_id].def_val.vbool;
+}
+
+char *
+nm_utils_team_link_watcher_to_string (const NMTeamLinkWatcher *watcher)
+{
+	nm_auto_free_gstring GString *str = NULL;
+	const char *name;
+	NMTeamLinkWatcherType watcher_type;
+	NMTeamLinkWatcherKeyId key_id;
+
+	if (!watcher)
+		return NULL;
+
+	str = g_string_new (NULL);
+
+	name = nm_team_link_watcher_get_name (watcher);
+	g_string_append_printf (str, "name=%s", name ?: "");
+
+	watcher_type = _team_link_watcher_get_watcher_type_from_name (name);
+
+	for (key_id = 0; key_id < _NM_TEAM_LINK_WATCHER_KEY_NUM; key_id++) {
+		const TeamLinkWatcherKeyInfo *info = &_team_link_watcher_key_infos[key_id];
+		const char *vstr;
+		int vint;
+		bool vbool;
+
+		nm_assert (info->name && info->name && NM_STRCHAR_ALL (info->name, ch,((ch >= 'a' && ch <= 'z') || NM_IN_SET (ch, '-'))));
+		nm_assert (NM_IN_SET (info->key_type, KEY_TYPE_STRING,
+		                                      KEY_TYPE_INT,
+		                                      KEY_TYPE_BOOL));
+
+		if (key_id == NM_TEAM_LINK_WATCHER_KEY_NAME)
+			continue;
+
+		if (!NM_FLAGS_ALL (info->watcher_type, watcher_type))
+			continue;
+
+		switch (info->key_type) {
+		case KEY_TYPE_STRING:
+			vstr = info->get_fcn.fstring (watcher);
+			if (vstr) {
+				g_string_append_printf (nm_gstring_add_space_delimiter (str),
+				                        "%s=%s", info->name, vstr);
+			}
+			break;
+		case KEY_TYPE_INT:
+			vint = info->get_fcn.fint (watcher);
+			if (vint != info->def_val.vint) {
+				g_string_append_printf (nm_gstring_add_space_delimiter (str),
+				                        "%s=%d", info->name, vint);
+			}
+			break;
+		case KEY_TYPE_BOOL:
+			vbool = info->get_fcn.fbool (watcher);
+			if (vbool != info->def_val.vbool) {
+				g_string_append_printf (nm_gstring_add_space_delimiter (str),
+				                        "%s=%s", info->name, vbool ? "true" : "false");
+			}
+			break;
+		}
+	}
+
+	return g_string_free (g_steal_pointer (&str), FALSE);
+}
+
+NMTeamLinkWatcher *
+nm_utils_team_link_watcher_from_string (const char *str,
+                                        GError **error)
+{
+	gs_free const char **tokens = NULL;
+	ParseData parse_data[_NM_TEAM_LINK_WATCHER_KEY_NUM] = { };
+	NMTeamLinkWatcherType watcher_type;
+	NMTeamLinkWatcherKeyId key_id;
+	gsize i_token;
+	NMTeamLinkWatcher *watcher;
+	int errsv;
+
+	g_return_val_if_fail (str, NULL);
+	g_return_val_if_fail (!error || !*error, NULL);
+
+	tokens = nm_utils_escaped_tokens_split (str, NM_ASCII_SPACES);
+	if (!tokens) {
+		g_set_error (error, 1, 0, "'%s' is not valid", str);
+		return NULL;
+	}
+
+	for (i_token = 0; tokens[i_token]; i_token++) {
+		const TeamLinkWatcherKeyInfo *info;
+		const char *key = tokens[i_token];
+		const char *val;
+
+		val = strchr (key, '=');
+		if (!val) {
+			nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+			                    _("'%s' is not valid: properties should be specified as 'key=value'"),
+			                    key);
+			return NULL;
+		}
+		((char *) val)[0] = '\0';
+		val++;
+
+		for (key_id = 0; key_id < _NM_TEAM_LINK_WATCHER_KEY_NUM; key_id++) {
+			info = &_team_link_watcher_key_infos[key_id];
+			if (nm_streq (key, info->name))
+				break;
+		}
+
+		if (key_id == _NM_TEAM_LINK_WATCHER_KEY_NUM) {
+			nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+			                    _("'%s' is not a valid key"), key);
+			return NULL;
+		}
+
+		if (parse_data[key_id].str_val) {
+			nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+			                    _("duplicate key '%s'"), key);
+			return NULL;
+		}
+
+		parse_data[key_id].str_val = val;
+
+		if (info->key_type == KEY_TYPE_INT) {
+			gint64 v64;
+
+			v64 = _nm_utils_ascii_str_to_int64 (val, 10, G_MININT, G_MAXINT, G_MAXINT64);
+			if (   v64 == G_MAXINT64
+			    && ((errsv = errno) != 0)) {
+				if (errsv == ERANGE) {
+					nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+					                    _("number for '%s' is out of range"), key);
+				} else {
+					nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+					                    _("value for '%s' must be a number"), key);
+				}
+				return NULL;
+			}
+			parse_data[key_id].typ_val.vint = v64;
+		} else if (info->key_type == KEY_TYPE_BOOL) {
+			int vbool;
+
+			vbool = _nm_utils_ascii_str_to_bool (val, -1);
+			if (vbool == -1) {
+				nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+				                    _("value for '%s' must be a boolean"), key);
+				return NULL;
+			}
+			parse_data[key_id].typ_val.vbool = vbool;
+		}
+	}
+
+	if (!parse_data[NM_TEAM_LINK_WATCHER_KEY_NAME].str_val) {
+		nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+		                    _("missing 'name' attribute"));
+		return NULL;
+	}
+
+	watcher_type = _team_link_watcher_get_watcher_type_from_name (parse_data[NM_TEAM_LINK_WATCHER_KEY_NAME].str_val);
+	if (watcher_type == NM_TEAM_LINK_WATCHER_TYPE_NONE) {
+		nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+		                    _("invalid 'name' \"%s\""),
+		                    parse_data[NM_TEAM_LINK_WATCHER_KEY_NAME].str_val);
+		return NULL;
+	}
+
+	for (key_id = 0; key_id < _NM_TEAM_LINK_WATCHER_KEY_NUM; key_id++) {
+		const TeamLinkWatcherKeyInfo *info = &_team_link_watcher_key_infos[key_id];
+
+		if (!parse_data[key_id].str_val)
+			continue;
+		if (!NM_FLAGS_ALL (info->watcher_type, watcher_type)) {
+			nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+			                    _("attribute '%s' is invalid for \"%s\""),
+			                    info->name,
+			                    parse_data[NM_TEAM_LINK_WATCHER_KEY_NAME].str_val);
+			return NULL;
+		}
+	}
+
+	switch (watcher_type) {
+	case NM_TEAM_LINK_WATCHER_TYPE_ETHTOOL:
+		watcher = nm_team_link_watcher_new_ethtool (_parse_data_get_int (parse_data, NM_TEAM_LINK_WATCHER_KEY_DELAY_UP),
+		                                            _parse_data_get_int (parse_data, NM_TEAM_LINK_WATCHER_KEY_DELAY_DOWN),
+		                                            error);
+		break;
+	case NM_TEAM_LINK_WATCHER_TYPE_NSNAPING:
+		watcher = nm_team_link_watcher_new_nsna_ping (_parse_data_get_int (parse_data, NM_TEAM_LINK_WATCHER_KEY_INIT_WAIT),
+		                                              _parse_data_get_int (parse_data, NM_TEAM_LINK_WATCHER_KEY_INTERVAL),
+		                                              _parse_data_get_int (parse_data, NM_TEAM_LINK_WATCHER_KEY_MISSED_MAX),
+		                                              _parse_data_get_str (parse_data, NM_TEAM_LINK_WATCHER_KEY_TARGET_HOST),
+		                                              error);
+		break;
+	default:
+		nm_assert (watcher_type == NM_TEAM_LINK_WATCHER_TYPE_ARPING);
+		watcher = nm_team_link_watcher_new_arp_ping2 (_parse_data_get_int (parse_data, NM_TEAM_LINK_WATCHER_KEY_INIT_WAIT),
+		                                              _parse_data_get_int (parse_data, NM_TEAM_LINK_WATCHER_KEY_INTERVAL),
+		                                              _parse_data_get_int (parse_data, NM_TEAM_LINK_WATCHER_KEY_MISSED_MAX),
+		                                              _parse_data_get_int (parse_data, NM_TEAM_LINK_WATCHER_KEY_VLANID),
+		                                              _parse_data_get_str (parse_data, NM_TEAM_LINK_WATCHER_KEY_TARGET_HOST),
+		                                              _parse_data_get_str (parse_data, NM_TEAM_LINK_WATCHER_KEY_SOURCE_HOST),
+		                                              (  NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_NONE
+		                                               | (_parse_data_get_bool (parse_data, NM_TEAM_LINK_WATCHER_KEY_VALIDATE_ACTIVE)   ? NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_VALIDATE_ACTIVE   : NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_NONE)
+		                                               | (_parse_data_get_bool (parse_data, NM_TEAM_LINK_WATCHER_KEY_VALIDATE_INACTIVE) ? NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_VALIDATE_INACTIVE : NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_NONE)
+		                                               | (_parse_data_get_bool (parse_data, NM_TEAM_LINK_WATCHER_KEY_SEND_ALWAYS)       ? NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_SEND_ALWAYS       : NM_TEAM_LINK_WATCHER_ARP_PING_FLAG_NONE)
+		                                              ),
+		                                              error);
+		break;
+	}
+
+#if NM_MORE_ASSERTS > 5
+	if (watcher) {
+		gs_free char *str2 = NULL;
+		nm_auto_unref_team_link_watcher NMTeamLinkWatcher *watcher2 = NULL;
+		static _nm_thread_local int recursive;
+
+		nm_assert (!error || !*error);
+		if (recursive == 0) {
+			recursive = 1;
+			str2 = nm_utils_team_link_watcher_to_string (watcher);
+			nm_assert (str2);
+			watcher2 = nm_utils_team_link_watcher_from_string (str2, NULL);
+			nm_assert (watcher2);
+			nm_assert (nm_team_link_watcher_equal (watcher, watcher2));
+			nm_assert (nm_team_link_watcher_equal (watcher2, watcher));
+			nm_assert (recursive == 1);
+			recursive = 0;
+		}
+	} else
+		nm_assert (!error || *error);
+#endif
+
+	return watcher;
+}
diff --git a/shared/nm-libnm-core-aux/nm-libnm-core-aux.h b/shared/nm-libnm-core-aux/nm-libnm-core-aux.h
new file mode 100644
index 00000000..d8960ad5
--- /dev/null
+++ b/shared/nm-libnm-core-aux/nm-libnm-core-aux.h
@@ -0,0 +1,54 @@
+/* NetworkManager -- Network link manager
+ *
+ * This library 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 of the License, or (at your option) any later version.
+ *
+ * This library 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 library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ *
+ * (C) Copyright 2019 Red Hat, Inc.
+ */
+
+#ifndef __NM_LIBNM_CORE_AUX_H__
+#define __NM_LIBNM_CORE_AUX_H__
+
+#include "nm-setting-team.h"
+
+typedef enum {
+	NM_TEAM_LINK_WATCHER_TYPE_NONE     = 0,
+	NM_TEAM_LINK_WATCHER_TYPE_ETHTOOL  = (1u << 0),
+	NM_TEAM_LINK_WATCHER_TYPE_NSNAPING = (1u << 1),
+	NM_TEAM_LINK_WATCHER_TYPE_ARPING   = (1u << 2),
+} NMTeamLinkWatcherType;
+
+typedef enum {
+	NM_TEAM_LINK_WATCHER_KEY_NAME,
+	NM_TEAM_LINK_WATCHER_KEY_DELAY_UP,
+	NM_TEAM_LINK_WATCHER_KEY_DELAY_DOWN,
+	NM_TEAM_LINK_WATCHER_KEY_INIT_WAIT,
+	NM_TEAM_LINK_WATCHER_KEY_INTERVAL,
+	NM_TEAM_LINK_WATCHER_KEY_MISSED_MAX,
+	NM_TEAM_LINK_WATCHER_KEY_TARGET_HOST,
+	NM_TEAM_LINK_WATCHER_KEY_VLANID,
+	NM_TEAM_LINK_WATCHER_KEY_SOURCE_HOST,
+	NM_TEAM_LINK_WATCHER_KEY_VALIDATE_ACTIVE,
+	NM_TEAM_LINK_WATCHER_KEY_VALIDATE_INACTIVE,
+	NM_TEAM_LINK_WATCHER_KEY_SEND_ALWAYS,
+	_NM_TEAM_LINK_WATCHER_KEY_NUM,
+} NMTeamLinkWatcherKeyId;
+
+char *nm_utils_team_link_watcher_to_string (const NMTeamLinkWatcher *watcher);
+
+NMTeamLinkWatcher *nm_utils_team_link_watcher_from_string (const char *str,
+                                                           GError **error);
+
+#endif /* __NM_LIBNM_CORE_AUX_H__ */
diff --git a/shared/nm-libnm-core-intern/nm-common-macros.h b/shared/nm-libnm-core-intern/nm-common-macros.h
index f5aa3a1e..8352e405 100644
--- a/shared/nm-libnm-core-intern/nm-common-macros.h
+++ b/shared/nm-libnm-core-intern/nm-common-macros.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
diff --git a/shared/nm-libnm-core-intern/nm-ethtool-utils.c b/shared/nm-libnm-core-intern/nm-ethtool-utils.c
index 3313274a..635d77c5 100644
--- a/shared/nm-libnm-core-intern/nm-ethtool-utils.c
+++ b/shared/nm-libnm-core-intern/nm-ethtool-utils.c
@@ -1,5 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
-
 /*
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
diff --git a/shared/nm-libnm-core-intern/nm-ethtool-utils.h b/shared/nm-libnm-core-intern/nm-ethtool-utils.h
index 5f22a9a0..71ad9860 100644
--- a/shared/nm-libnm-core-intern/nm-ethtool-utils.h
+++ b/shared/nm-libnm-core-intern/nm-ethtool-utils.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /*
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
diff --git a/shared/nm-meta-setting.c b/shared/nm-meta-setting.c
index 8d1d4ecd..d50739ba 100644
--- a/shared/nm-meta-setting.c
+++ b/shared/nm-meta-setting.c
@@ -1,5 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
-
 /*
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -48,6 +46,7 @@
 #include "nm-setting-olpc-mesh.h"
 #include "nm-setting-ovs-bridge.h"
 #include "nm-setting-ovs-interface.h"
+#include "nm-setting-ovs-dpdk.h"
 #include "nm-setting-ovs-patch.h"
 #include "nm-setting-ovs-port.h"
 #include "nm-setting-ppp.h"
@@ -73,87 +72,96 @@
 /*****************************************************************************/
 
 const NMSetting8021xSchemeVtable nm_setting_8021x_scheme_vtable[] = {
-	[NM_SETTING_802_1X_SCHEME_TYPE_CA_CERT] = {
-		.setting_key            = NM_SETTING_802_1X_CA_CERT,
-		.scheme_func            = nm_setting_802_1x_get_ca_cert_scheme,
-		.format_func            = NULL,
-		.path_func              = nm_setting_802_1x_get_ca_cert_path,
-		.blob_func              = nm_setting_802_1x_get_ca_cert_blob,
-		.uri_func               = nm_setting_802_1x_get_ca_cert_uri,
-		.passwd_func            = nm_setting_802_1x_get_ca_cert_password,
-		.pwflag_func            = nm_setting_802_1x_get_ca_cert_password_flags,
-		.set_cert_func          = nm_setting_802_1x_set_ca_cert,
-		.file_suffix            = "ca-cert",
-	},
-
-	[NM_SETTING_802_1X_SCHEME_TYPE_PHASE2_CA_CERT] = {
-		.setting_key            = NM_SETTING_802_1X_PHASE2_CA_CERT,
-		.scheme_func            = nm_setting_802_1x_get_phase2_ca_cert_scheme,
-		.format_func            = NULL,
-		.path_func              = nm_setting_802_1x_get_phase2_ca_cert_path,
-		.blob_func              = nm_setting_802_1x_get_phase2_ca_cert_blob,
-		.uri_func               = nm_setting_802_1x_get_phase2_ca_cert_uri,
-		.passwd_func            = nm_setting_802_1x_get_phase2_ca_cert_password,
-		.pwflag_func            = nm_setting_802_1x_get_phase2_ca_cert_password_flags,
-		.set_cert_func          = nm_setting_802_1x_set_phase2_ca_cert,
-		.file_suffix            = "inner-ca-cert",
-	},
-
-	[NM_SETTING_802_1X_SCHEME_TYPE_CLIENT_CERT] = {
-		.setting_key            = NM_SETTING_802_1X_CLIENT_CERT,
-		.scheme_func            = nm_setting_802_1x_get_client_cert_scheme,
-		.format_func            = NULL,
-		.path_func              = nm_setting_802_1x_get_client_cert_path,
-		.blob_func              = nm_setting_802_1x_get_client_cert_blob,
-		.uri_func               = nm_setting_802_1x_get_client_cert_uri,
-		.passwd_func            = nm_setting_802_1x_get_client_cert_password,
-		.pwflag_func            = nm_setting_802_1x_get_client_cert_password_flags,
-		.set_cert_func          = nm_setting_802_1x_set_client_cert,
-		.file_suffix            = "client-cert",
-	},
-
-	[NM_SETTING_802_1X_SCHEME_TYPE_PHASE2_CLIENT_CERT] = {
-		.setting_key            = NM_SETTING_802_1X_PHASE2_CLIENT_CERT,
-		.scheme_func            = nm_setting_802_1x_get_phase2_client_cert_scheme,
-		.format_func            = NULL,
-		.path_func              = nm_setting_802_1x_get_phase2_client_cert_path,
-		.blob_func              = nm_setting_802_1x_get_phase2_client_cert_blob,
-		.uri_func               = nm_setting_802_1x_get_phase2_client_cert_uri,
-		.passwd_func            = nm_setting_802_1x_get_phase2_client_cert_password,
-		.pwflag_func            = nm_setting_802_1x_get_phase2_client_cert_password_flags,
-		.set_cert_func          = nm_setting_802_1x_set_phase2_client_cert,
-		.file_suffix            = "inner-client-cert",
-	},
-
-	[NM_SETTING_802_1X_SCHEME_TYPE_PRIVATE_KEY] = {
-		.setting_key            = NM_SETTING_802_1X_PRIVATE_KEY,
-		.scheme_func            = nm_setting_802_1x_get_private_key_scheme,
-		.format_func            = nm_setting_802_1x_get_private_key_format,
-		.path_func              = nm_setting_802_1x_get_private_key_path,
-		.blob_func              = nm_setting_802_1x_get_private_key_blob,
-		.uri_func               = nm_setting_802_1x_get_private_key_uri,
-		.passwd_func            = nm_setting_802_1x_get_private_key_password,
-		.pwflag_func            = nm_setting_802_1x_get_private_key_password_flags,
-		.set_private_key_func   = nm_setting_802_1x_set_private_key,
-		.file_suffix            = "private-key",
-		.is_secret              = TRUE,
-	},
-
-	[NM_SETTING_802_1X_SCHEME_TYPE_PHASE2_PRIVATE_KEY] = {
-		.setting_key            = NM_SETTING_802_1X_PHASE2_PRIVATE_KEY,
-		.scheme_func            = nm_setting_802_1x_get_phase2_private_key_scheme,
-		.format_func            = nm_setting_802_1x_get_phase2_private_key_format,
-		.path_func              = nm_setting_802_1x_get_phase2_private_key_path,
-		.blob_func              = nm_setting_802_1x_get_phase2_private_key_blob,
-		.uri_func               = nm_setting_802_1x_get_phase2_private_key_uri,
-		.passwd_func            = nm_setting_802_1x_get_phase2_private_key_password,
-		.pwflag_func            = nm_setting_802_1x_get_phase2_private_key_password_flags,
-		.set_private_key_func   = nm_setting_802_1x_set_phase2_private_key,
-		.file_suffix            = "inner-private-key",
-		.is_secret              = TRUE,
-	},
-
-	[NM_SETTING_802_1X_SCHEME_TYPE_UNKNOWN] = { NULL },
+
+#define _D(_scheme_type, ...) \
+	[(_scheme_type)] = { \
+		.scheme_type = (_scheme_type), \
+		__VA_ARGS__ \
+	}
+
+	_D (NM_SETTING_802_1X_SCHEME_TYPE_UNKNOWN),
+
+	_D (NM_SETTING_802_1X_SCHEME_TYPE_CA_CERT,
+		.setting_key          = NM_SETTING_802_1X_CA_CERT,
+		.scheme_func          = nm_setting_802_1x_get_ca_cert_scheme,
+		.format_func          = NULL,
+		.path_func            = nm_setting_802_1x_get_ca_cert_path,
+		.blob_func            = nm_setting_802_1x_get_ca_cert_blob,
+		.uri_func             = nm_setting_802_1x_get_ca_cert_uri,
+		.passwd_func          = nm_setting_802_1x_get_ca_cert_password,
+		.pwflag_func          = nm_setting_802_1x_get_ca_cert_password_flags,
+		.set_cert_func        = nm_setting_802_1x_set_ca_cert,
+		.file_suffix          = "ca-cert",
+	),
+
+	_D (NM_SETTING_802_1X_SCHEME_TYPE_PHASE2_CA_CERT,
+		.setting_key          = NM_SETTING_802_1X_PHASE2_CA_CERT,
+		.scheme_func          = nm_setting_802_1x_get_phase2_ca_cert_scheme,
+		.format_func          = NULL,
+		.path_func            = nm_setting_802_1x_get_phase2_ca_cert_path,
+		.blob_func            = nm_setting_802_1x_get_phase2_ca_cert_blob,
+		.uri_func             = nm_setting_802_1x_get_phase2_ca_cert_uri,
+		.passwd_func          = nm_setting_802_1x_get_phase2_ca_cert_password,
+		.pwflag_func          = nm_setting_802_1x_get_phase2_ca_cert_password_flags,
+		.set_cert_func        = nm_setting_802_1x_set_phase2_ca_cert,
+		.file_suffix          = "inner-ca-cert",
+	),
+
+	_D (NM_SETTING_802_1X_SCHEME_TYPE_CLIENT_CERT,
+		.setting_key          = NM_SETTING_802_1X_CLIENT_CERT,
+		.scheme_func          = nm_setting_802_1x_get_client_cert_scheme,
+		.format_func          = NULL,
+		.path_func            = nm_setting_802_1x_get_client_cert_path,
+		.blob_func            = nm_setting_802_1x_get_client_cert_blob,
+		.uri_func             = nm_setting_802_1x_get_client_cert_uri,
+		.passwd_func          = nm_setting_802_1x_get_client_cert_password,
+		.pwflag_func          = nm_setting_802_1x_get_client_cert_password_flags,
+		.set_cert_func        = nm_setting_802_1x_set_client_cert,
+		.file_suffix          = "client-cert",
+	),
+
+	_D (NM_SETTING_802_1X_SCHEME_TYPE_PHASE2_CLIENT_CERT,
+		.setting_key          = NM_SETTING_802_1X_PHASE2_CLIENT_CERT,
+		.scheme_func          = nm_setting_802_1x_get_phase2_client_cert_scheme,
+		.format_func          = NULL,
+		.path_func            = nm_setting_802_1x_get_phase2_client_cert_path,
+		.blob_func            = nm_setting_802_1x_get_phase2_client_cert_blob,
+		.uri_func             = nm_setting_802_1x_get_phase2_client_cert_uri,
+		.passwd_func          = nm_setting_802_1x_get_phase2_client_cert_password,
+		.pwflag_func          = nm_setting_802_1x_get_phase2_client_cert_password_flags,
+		.set_cert_func        = nm_setting_802_1x_set_phase2_client_cert,
+		.file_suffix          = "inner-client-cert",
+	),
+
+	_D (NM_SETTING_802_1X_SCHEME_TYPE_PRIVATE_KEY,
+		.setting_key          = NM_SETTING_802_1X_PRIVATE_KEY,
+		.scheme_func          = nm_setting_802_1x_get_private_key_scheme,
+		.format_func          = nm_setting_802_1x_get_private_key_format,
+		.path_func            = nm_setting_802_1x_get_private_key_path,
+		.blob_func            = nm_setting_802_1x_get_private_key_blob,
+		.uri_func             = nm_setting_802_1x_get_private_key_uri,
+		.passwd_func          = nm_setting_802_1x_get_private_key_password,
+		.pwflag_func          = nm_setting_802_1x_get_private_key_password_flags,
+		.set_private_key_func = nm_setting_802_1x_set_private_key,
+		.file_suffix          = "private-key",
+		.is_secret            = TRUE,
+	),
+
+	_D (NM_SETTING_802_1X_SCHEME_TYPE_PHASE2_PRIVATE_KEY,
+		.setting_key          = NM_SETTING_802_1X_PHASE2_PRIVATE_KEY,
+		.scheme_func          = nm_setting_802_1x_get_phase2_private_key_scheme,
+		.format_func          = nm_setting_802_1x_get_phase2_private_key_format,
+		.path_func            = nm_setting_802_1x_get_phase2_private_key_path,
+		.blob_func            = nm_setting_802_1x_get_phase2_private_key_blob,
+		.uri_func             = nm_setting_802_1x_get_phase2_private_key_uri,
+		.passwd_func          = nm_setting_802_1x_get_phase2_private_key_password,
+		.pwflag_func          = nm_setting_802_1x_get_phase2_private_key_password_flags,
+		.set_private_key_func = nm_setting_802_1x_set_phase2_private_key,
+		.file_suffix          = "inner-private-key",
+		.is_secret            = TRUE,
+	),
+
+#undef _D
 };
 
 /*****************************************************************************/
@@ -297,6 +305,12 @@ const NMMetaSettingInfo nm_meta_setting_infos[] = {
 		.setting_name =             NM_SETTING_OVS_BRIDGE_SETTING_NAME,
 		.get_setting_gtype =        nm_setting_ovs_bridge_get_type,
 	},
+	[NM_META_SETTING_TYPE_OVS_DPDK] = {
+		.meta_type =                NM_META_SETTING_TYPE_OVS_DPDK,
+		.setting_priority =         NM_SETTING_PRIORITY_HW_BASE,
+		.setting_name =             NM_SETTING_OVS_DPDK_SETTING_NAME,
+		.get_setting_gtype =        nm_setting_ovs_dpdk_get_type,
+	},
 	[NM_META_SETTING_TYPE_OVS_INTERFACE] = {
 		.meta_type =                NM_META_SETTING_TYPE_OVS_INTERFACE,
 		.setting_priority =         NM_SETTING_PRIORITY_HW_BASE,
diff --git a/shared/nm-meta-setting.h b/shared/nm-meta-setting.h
index 73ee103e..157a715f 100644
--- a/shared/nm-meta-setting.h
+++ b/shared/nm-meta-setting.h
@@ -1,5 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
-
 /*
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -101,6 +99,7 @@ typedef struct {
 	                                  NMSetting8021xCKFormat *out_format,
 	                                  GError **error);
 	const char *file_suffix;
+	NMSetting8021xSchemeType scheme_type;
 	bool is_secret:1;
 } NMSetting8021xSchemeVtable;
 
@@ -141,6 +140,7 @@ typedef enum {
 	NM_META_SETTING_TYPE_MACVLAN,
 	NM_META_SETTING_TYPE_MATCH,
 	NM_META_SETTING_TYPE_OVS_BRIDGE,
+	NM_META_SETTING_TYPE_OVS_DPDK,
 	NM_META_SETTING_TYPE_OVS_INTERFACE,
 	NM_META_SETTING_TYPE_OVS_PATCH,
 	NM_META_SETTING_TYPE_OVS_PORT,
diff --git a/shared/nm-std-aux/c-list-util.c b/shared/nm-std-aux/c-list-util.c
index 44ca26a5..eb545b2d 100644
--- a/shared/nm-std-aux/c-list-util.c
+++ b/shared/nm-std-aux/c-list-util.c
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
diff --git a/shared/nm-std-aux/c-list-util.h b/shared/nm-std-aux/c-list-util.h
index 648bacc7..a1fe7169 100644
--- a/shared/nm-std-aux/c-list-util.h
+++ b/shared/nm-std-aux/c-list-util.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
diff --git a/shared/nm-std-aux/nm-dbus-compat.h b/shared/nm-std-aux/nm-dbus-compat.h
index dd97b5fd..8528bd07 100644
--- a/shared/nm-std-aux/nm-dbus-compat.h
+++ b/shared/nm-std-aux/nm-dbus-compat.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /*
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
diff --git a/shared/nm-test-libnm-utils.h b/shared/nm-test-libnm-utils.h
index 2b4fa600..4c921f4c 100644
--- a/shared/nm-test-libnm-utils.h
+++ b/shared/nm-test-libnm-utils.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /*
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -22,22 +21,11 @@
 
 #include "nm-utils/nm-test-utils.h"
 
-#if (NETWORKMANAGER_COMPILATION) & NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_GLIB
-#include "nm-dbus-glib-types.h"
-#endif
-
-/*****************************************************************************/
-
 typedef struct {
 	GDBusConnection *bus;
 	GDBusProxy *proxy;
 	GPid pid;
 	int keepalive_fd;
-#if (NETWORKMANAGER_COMPILATION) & NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_GLIB
-	struct {
-		DBusGConnection *bus;
-	} libdbus;
-#endif
 } NMTstcServiceInfo;
 
 NMTstcServiceInfo *nmtstc_service_init (void);
@@ -62,18 +50,6 @@ static inline void _nmtstc_auto_service_cleanup (NMTstcServiceInfo **info)
 	}); \
 	NM_PRAGMA_WARNING_REENABLE
 
-/*****************************************************************************/
-
-#if (NETWORKMANAGER_COMPILATION) & NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_GLIB
-
-#include "nm-client.h"
-#include "nm-remote-settings.h"
-
-NMClient *nmtstc_nm_client_new (void);
-NMRemoteSettings *nmtstc_nm_remote_settings_new (void);
-
-#else
-
 NMDevice *nmtstc_service_add_device (NMTstcServiceInfo *info,
                                      NMClient *client,
                                      const char *method,
@@ -85,10 +61,6 @@ NMDevice * nmtstc_service_add_wired_device (NMTstcServiceInfo *sinfo,
                                             const char *hwaddr,
                                             const char **subchannels);
 
-#endif
-
-/*****************************************************************************/
-
 void nmtstc_service_add_connection (NMTstcServiceInfo *sinfo,
                                     NMConnection *connection,
                                     gboolean verify_connection,
diff --git a/shared/nm-test-utils-impl.c b/shared/nm-test-utils-impl.c
index 02d71593..eca037ba 100644
--- a/shared/nm-test-utils-impl.c
+++ b/shared/nm-test-utils-impl.c
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /*
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -56,24 +55,6 @@ name_exists (GDBusConnection *c, const char *name)
 	return exists;
 }
 
-#if (NETWORKMANAGER_COMPILATION) & NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_GLIB
-static DBusGProxy *
-_libdbus_create_proxy_test (DBusGConnection *bus)
-{
-	DBusGProxy *proxy;
-
-	proxy = dbus_g_proxy_new_for_name (bus,
-	                                   NM_DBUS_SERVICE,
-	                                   NM_DBUS_PATH,
-	                                   "org.freedesktop.NetworkManager.LibnmGlibTest");
-	g_assert (proxy);
-
-	dbus_g_proxy_set_default_timeout (proxy, G_MAXINT);
-
-	return proxy;
-}
-#endif
-
 typedef struct {
 	GMainLoop *mainloop;
 	GDBusConnection *bus;
@@ -201,11 +182,6 @@ nmtstc_service_init (void)
 	                                     NULL, &error);
 	g_assert_no_error (error);
 
-#if (NETWORKMANAGER_COMPILATION) & NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_GLIB
-	info->libdbus.bus = dbus_g_bus_get (DBUS_BUS_SESSION, &error);
-	g_assert_no_error (error);
-	g_assert (info->libdbus.bus);
-#endif
 	return info;
 }
 
@@ -223,10 +199,6 @@ nmtstc_service_cleanup (NMTstcServiceInfo *info)
 
 	g_clear_object (&info->proxy);
 
-#if (NETWORKMANAGER_COMPILATION) & NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_GLIB
-	g_clear_pointer (&info->libdbus.bus, dbus_g_connection_unref);
-#endif
-
 	if (info->pid != NM_PID_T_INVAL) {
 		kill (info->pid, SIGTERM);
 
@@ -255,7 +227,6 @@ again_wait:
 	g_free (info);
 }
 
-#if !((NETWORKMANAGER_COMPILATION) & NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_GLIB)
 typedef struct {
 	GMainLoop *loop;
 	const char *ifname;
@@ -368,7 +339,6 @@ nmtstc_service_add_wired_device (NMTstcServiceInfo *sinfo, NMClient *client,
 {
 	return add_device_common (sinfo, client, "AddWiredDevice", ifname, hwaddr, subchannels);
 }
-#endif
 
 void
 nmtstc_service_add_connection (NMTstcServiceInfo *sinfo,
@@ -376,41 +346,10 @@ nmtstc_service_add_connection (NMTstcServiceInfo *sinfo,
                                gboolean verify_connection,
                                char **out_path)
 {
-#if (NETWORKMANAGER_COMPILATION) & NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_GLIB
-	gs_unref_hashtable GHashTable *new_settings = NULL;
-	gboolean success;
-	gs_free_error GError *error = NULL;
-	gs_free char *path = NULL;
-	gs_unref_object DBusGProxy *proxy = NULL;
-
-	g_assert (sinfo);
-	g_assert (NM_IS_CONNECTION (connection));
-
-	new_settings = nm_connection_to_hash (connection, NM_SETTING_HASH_FLAG_ALL);
-
-	proxy = _libdbus_create_proxy_test (sinfo->libdbus.bus);
-
-	success = dbus_g_proxy_call (proxy,
-	                             "AddConnection",
-	                             &error,
-	                             DBUS_TYPE_G_MAP_OF_MAP_OF_VARIANT, new_settings,
-	                             G_TYPE_BOOLEAN, verify_connection,
-	                             G_TYPE_INVALID,
-	                             DBUS_TYPE_G_OBJECT_PATH, &path,
-	                             G_TYPE_INVALID);
-	g_assert_no_error (error);
-	g_assert (success);
-
-	g_assert (path && *path);
-
-	if (out_path)
-		*out_path = g_strdup (path);
-#else
 	nmtstc_service_add_connection_variant (sinfo,
 	                                       nm_connection_to_dbus (connection, NM_CONNECTION_SERIALIZE_ALL),
 	                                       verify_connection,
 	                                       out_path);
-#endif
 }
 
 void
@@ -450,37 +389,10 @@ nmtstc_service_update_connection (NMTstcServiceInfo *sinfo,
 		path = nm_connection_get_path (connection);
 	g_assert (path);
 
-#if (NETWORKMANAGER_COMPILATION) & NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_GLIB
-	{
-		gs_unref_hashtable GHashTable *new_settings = NULL;
-		gboolean success;
-		gs_free_error GError *error = NULL;
-		gs_unref_object DBusGProxy *proxy = NULL;
-
-		g_assert (sinfo);
-		g_assert (NM_IS_CONNECTION (connection));
-
-		new_settings = nm_connection_to_hash (connection, NM_SETTING_HASH_FLAG_ALL);
-
-		proxy = _libdbus_create_proxy_test (sinfo->libdbus.bus);
-
-		success = dbus_g_proxy_call (proxy,
-		                             "UpdateConnection",
-		                             &error,
-		                             DBUS_TYPE_G_OBJECT_PATH, path,
-		                             DBUS_TYPE_G_MAP_OF_MAP_OF_VARIANT, new_settings,
-		                             G_TYPE_BOOLEAN, verify_connection,
-		                             G_TYPE_INVALID,
-		                             G_TYPE_INVALID);
-		g_assert_no_error (error);
-		g_assert (success);
-	}
-#else
 	nmtstc_service_update_connection_variant (sinfo,
 	                                          path,
 	                                          nm_connection_to_dbus (connection, NM_CONNECTION_SERIALIZE_ALL),
 	                                          verify_connection);
-#endif
 }
 
 void
@@ -508,55 +420,3 @@ nmtstc_service_update_connection_variant (NMTstcServiceInfo *sinfo,
 	g_assert (g_variant_is_of_type (result, G_VARIANT_TYPE ("()")));
 	g_variant_unref (result);
 }
-
-/*****************************************************************************/
-
-#if (NETWORKMANAGER_COMPILATION) & NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_GLIB
-NMClient *
-nmtstc_nm_client_new (void)
-{
-	NMClient *client;
-	DBusGConnection *bus;
-	GError *error = NULL;
-	gboolean success;
-
-	bus = dbus_g_bus_get (DBUS_BUS_SESSION, &error);
-	g_assert_no_error (error);
-	g_assert (bus);
-
-	client = g_object_new (NM_TYPE_CLIENT,
-	                       NM_OBJECT_DBUS_CONNECTION, bus,
-	                       NM_OBJECT_DBUS_PATH, NM_DBUS_PATH,
-	                       NULL);
-	g_assert (client != NULL);
-
-	dbus_g_connection_unref (bus);
-
-	success = g_initable_init (G_INITABLE (client), NULL, &error);
-	g_assert_no_error (error);
-	g_assert (success == TRUE);
-
-	return client;
-}
-
-NMRemoteSettings *
-nmtstc_nm_remote_settings_new (void)
-{
-	NMRemoteSettings *settings;
-	DBusGConnection *bus;
-	GError *error = NULL;
-
-	bus = dbus_g_bus_get (DBUS_BUS_SESSION, &error);
-	g_assert_no_error (error);
-	g_assert (bus);
-
-	settings = nm_remote_settings_new (bus);
-	g_assert (settings);
-
-	dbus_g_connection_unref (bus);
-
-	return settings;
-}
-#endif
-
-/*****************************************************************************/
diff --git a/shared/nm-udev-aux/nm-udev-utils.c b/shared/nm-udev-aux/nm-udev-utils.c
index 5d0919b3..e9dfd8d0 100644
--- a/shared/nm-udev-aux/nm-udev-utils.c
+++ b/shared/nm-udev-aux/nm-udev-utils.c
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* nm-udev-utils.c - udev utils functions
  *
  * This program is free software; you can redistribute it and/or modify
diff --git a/shared/nm-udev-aux/nm-udev-utils.h b/shared/nm-udev-aux/nm-udev-utils.h
index 911e8a27..0e5895ef 100644
--- a/shared/nm-udev-aux/nm-udev-utils.h
+++ b/shared/nm-udev-aux/nm-udev-utils.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* nm-udev-utils.h - udev utils functions
  *
  * This program is free software; you can redistribute it and/or modify
diff --git a/shared/nm-utils/nm-compat.c b/shared/nm-utils/nm-compat.c
index aa7c42f1..ea3e5392 100644
--- a/shared/nm-utils/nm-compat.c
+++ b/shared/nm-utils/nm-compat.c
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
diff --git a/shared/nm-utils/nm-compat.h b/shared/nm-utils/nm-compat.h
index 52341690..a8e3ee97 100644
--- a/shared/nm-utils/nm-compat.h
+++ b/shared/nm-utils/nm-compat.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This library is free software; you can redistribute it and/or
diff --git a/shared/nm-utils/nm-test-utils.h b/shared/nm-utils/nm-test-utils.h
index d0ec7d9f..b2d103f1 100644
--- a/shared/nm-utils/nm-test-utils.h
+++ b/shared/nm-utils/nm-test-utils.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /*
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -710,18 +709,10 @@ nmtst_test_quick (void)
 
 #define NMTST_EXPECT(domain, level, msg)        g_test_expect_message (domain, level, msg)
 
-#if (NETWORKMANAGER_COMPILATION) & NM_NETWORKMANAGER_COMPILATION_WITH_LIBNM_UTIL
-#define NMTST_EXPECT_LIBNM_U(level, msg)        NMTST_EXPECT ("libnm-util", level, msg)
-#define NMTST_EXPECT_LIBNM_G(level, msg)        NMTST_EXPECT ("libnm-glib", level, msg)
-
-#define NMTST_EXPECT_LIBNM_U_CRITICAL(msg)      NMTST_EXPECT_LIBNM_U (G_LOG_LEVEL_CRITICAL, msg)
-#define NMTST_EXPECT_LIBNM_G_CRITICAL(msg)      NMTST_EXPECT_LIBNM_G (G_LOG_LEVEL_CRITICAL, msg)
-#else
 #define NMTST_EXPECT_LIBNM(level, msg)          NMTST_EXPECT ("libnm", level, msg)
 
 #define NMTST_EXPECT_LIBNM_WARNING(msg)         NMTST_EXPECT_LIBNM (G_LOG_LEVEL_WARNING, msg)
 #define NMTST_EXPECT_LIBNM_CRITICAL(msg)        NMTST_EXPECT_LIBNM (G_LOG_LEVEL_CRITICAL, msg)
-#endif
 
 /*****************************************************************************/
 
@@ -866,15 +857,22 @@ nmtst_get_rand (void)
 }
 
 static inline guint32
-nmtst_get_rand_int (void)
+nmtst_get_rand_uint32 (void)
 {
 	return g_rand_int (nmtst_get_rand ());
 }
 
+static inline guint
+nmtst_get_rand_uint (void)
+{
+	G_STATIC_ASSERT_EXPR (sizeof (guint32) == sizeof (guint));
+	return nmtst_get_rand_uint32 ();
+}
+
 static inline gboolean
 nmtst_get_rand_bool (void)
 {
-	return nmtst_get_rand_int () % 2;
+	return nmtst_get_rand_uint32 () % 2;
 }
 
 static inline gpointer
@@ -909,7 +907,7 @@ nmtst_rand_buf (GRand *rand, gpointer buffer, gsize buffer_length)
 	({ \
 		typeof (v0) NM_UNIQ_T (UNIQ, uniq)[1 + NM_NARG (__VA_ARGS__)] = { (v0), __VA_ARGS__ }; \
 		\
-		NM_UNIQ_T (UNIQ, uniq)[nmtst_get_rand_int () % G_N_ELEMENTS (NM_UNIQ_T (UNIQ, uniq))]; \
+		NM_UNIQ_T (UNIQ, uniq)[nmtst_get_rand_uint32 () % G_N_ELEMENTS (NM_UNIQ_T (UNIQ, uniq))]; \
 	})
 
 #define nmtst_rand_select(...) \
@@ -1935,6 +1933,69 @@ nmtst_assert_setting_verify_fails (NMSetting *setting,
 	g_clear_error (&error);
 }
 
+static inline void
+nmtst_assert_setting_is_equal (gconstpointer /* const NMSetting * */ a,
+                               gconstpointer /* const NMSetting * */ b,
+                               NMSettingCompareFlags flags)
+{
+	gs_unref_hashtable GHashTable *hash = NULL;
+	guint32 r = nmtst_get_rand_uint32 ();
+
+	g_assert (NM_IS_SETTING (a));
+	g_assert (NM_IS_SETTING (b));
+
+	if (NM_FLAGS_HAS (r, 0x4))
+		NMTST_SWAP (a, b);
+
+	g_assert (nm_setting_compare ((NMSetting *) a,
+	                              (NMSetting *) b,
+	                              flags));
+
+	if (NM_FLAGS_HAS (r, 0x8))
+		NMTST_SWAP (a, b);
+
+	g_assert (nm_setting_diff ((NMSetting *) a,
+	                           (NMSetting *) b,
+	                           flags,
+	                           NM_FLAGS_HAS (r, 0x1),
+	                           &hash));
+	g_assert (!hash);
+}
+#endif
+
+#ifdef __NM_SETTING_PRIVATE_H__
+static inline NMSetting *
+nmtst_assert_setting_dbus_new (GType gtype, GVariant *variant)
+{
+	NMSetting *setting;
+	gs_free_error GError *error = NULL;
+
+	g_assert (g_type_is_a (gtype, NM_TYPE_SETTING));
+	g_assert (gtype != NM_TYPE_SETTING);
+	g_assert (variant);
+	g_assert (g_variant_is_of_type (variant, NM_VARIANT_TYPE_SETTING));
+
+	setting = _nm_setting_new_from_dbus (gtype,
+	                                     variant,
+	                                     NULL,
+	                                     NM_SETTING_PARSE_FLAGS_STRICT,
+	                                     &error);
+	nmtst_assert_success (setting, error);
+	return setting;
+}
+
+static inline void
+nmtst_assert_setting_dbus_roundtrip (gconstpointer /* const NMSetting * */ setting)
+{
+	gs_unref_object NMSetting *setting2 = NULL;
+	gs_unref_variant GVariant *variant = NULL;
+
+	g_assert (NM_IS_SETTING (setting));
+
+	variant = _nm_setting_to_dbus ((NMSetting *) setting, NULL, NM_CONNECTION_SERIALIZE_ALL, NULL);
+	setting2 = nmtst_assert_setting_dbus_new (G_OBJECT_TYPE (setting), variant);
+	nmtst_assert_setting_is_equal (setting, setting2, NM_SETTING_COMPARE_FLAG_EXACT);
+}
 #endif
 
 #ifdef __NM_UTILS_H__
@@ -2148,6 +2209,25 @@ typedef enum {
 
 #endif /* __NM_CONNECTION_H__ */
 
+static inline GVariant *
+nmtst_variant_from_string (const GVariantType *variant_type,
+                           const char *variant_str)
+{
+	GVariant *variant;
+	GError *error = NULL;
+
+	g_assert (variant_type);
+	g_assert (variant_str);
+
+	variant = g_variant_parse (variant_type,
+	                           variant_str,
+	                           NULL,
+	                           NULL,
+	                           &error);
+	nmtst_assert_success (variant, error);
+	return variant;
+}
+
 /*****************************************************************************/
 
 static inline void
diff --git a/shared/nm-utils/nm-vpn-editor-plugin-call.h b/shared/nm-utils/nm-vpn-editor-plugin-call.h
index fd982acf..ea5e27b0 100644
--- a/shared/nm-utils/nm-vpn-editor-plugin-call.h
+++ b/shared/nm-utils/nm-vpn-editor-plugin-call.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* NetworkManager -- Network link manager
  *
  * This program is free software; you can redistribute it and/or modify
diff --git a/shared/nm-utils/nm-vpn-plugin-macros.h b/shared/nm-utils/nm-vpn-plugin-macros.h
index acc549f2..97260f5f 100644
--- a/shared/nm-utils/nm-vpn-plugin-macros.h
+++ b/shared/nm-utils/nm-vpn-plugin-macros.h
@@ -1,5 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
-
 /*
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
diff --git a/shared/nm-utils/nm-vpn-plugin-utils.c b/shared/nm-utils/nm-vpn-plugin-utils.c
index 353a2817..afc05bdf 100644
--- a/shared/nm-utils/nm-vpn-plugin-utils.c
+++ b/shared/nm-utils/nm-vpn-plugin-utils.c
@@ -1,5 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
-
 /*
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
diff --git a/shared/nm-utils/nm-vpn-plugin-utils.h b/shared/nm-utils/nm-vpn-plugin-utils.h
index f3928d1e..961e0187 100644
--- a/shared/nm-utils/nm-vpn-plugin-utils.h
+++ b/shared/nm-utils/nm-vpn-plugin-utils.h
@@ -1,5 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
-
 /*
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
diff --git a/shared/nm-utils/tests/test-shared-general.c b/shared/nm-utils/tests/test-shared-general.c
index 83cffd7f..24affba7 100644
--- a/shared/nm-utils/tests/test-shared-general.c
+++ b/shared/nm-utils/tests/test-shared-general.c
@@ -29,24 +29,10 @@
 
 /*****************************************************************************/
 
-static int _monotonic_timestamp_initialized;
-
-void
-_nm_utils_monotonic_timestamp_initialized (const struct timespec *tp,
-                                           gint64 offset_sec,
-                                           gboolean is_boottime)
-{
-	g_assert (!_monotonic_timestamp_initialized);
-	_monotonic_timestamp_initialized = 1;
-}
-
-/*****************************************************************************/
-
 static void
 test_monotonic_timestamp (void)
 {
 	g_assert (nm_utils_get_monotonic_timestamp_s () > 0);
-	g_assert (_monotonic_timestamp_initialized);
 }
 
 /*****************************************************************************/
@@ -163,10 +149,10 @@ test_nm_strndup_a (void)
 		char ch;
 		gsize i, l;
 
-		input = g_strnfill (nmtst_get_rand_int () % 20, 'x');
+		input = g_strnfill (nmtst_get_rand_uint32 () % 20, 'x');
 
 		for (i = 0; input[i]; i++) {
-			while ((ch = ((char) nmtst_get_rand_int ())) == '\0') {
+			while ((ch = ((char) nmtst_get_rand_uint32 ())) == '\0') {
 				/* repeat. */
 			}
 			input[i] = ch;
@@ -189,7 +175,7 @@ test_nm_strndup_a (void)
 			gs_free char *dup_free = NULL;
 			const char *dup;
 
-			l = nmtst_get_rand_int () % 23;
+			l = nmtst_get_rand_uint32 () % 23;
 			dup = nm_strndup_a (10, input, l, &dup_free);
 			g_assert (strncmp (dup, input, l) == 0);
 			g_assert (strlen (dup) <= l);
@@ -228,7 +214,7 @@ test_unaligned (void)
 		guint8 val = 0;
 
 		while (val == 0)
-			val = nmtst_get_rand_int () % 256;
+			val = nmtst_get_rand_uint32 () % 256;
 
 		buf[shift] = val;
 
@@ -441,6 +427,87 @@ test_strstrip_avoid_copy (void)
 	_do_strstrip_avoid_copy (" 01234567890 ");
 	_do_strstrip_avoid_copy (" 012345678901 ");
 }
+
+/*****************************************************************************/
+
+static void
+test_nm_utils_bin2hexstr (void)
+{
+	int n_run;
+
+	for (n_run = 0; n_run < 100; n_run++) {
+		guint8 buf[100];
+		guint8 buf2[G_N_ELEMENTS (buf) + 1];
+		gsize len = nmtst_get_rand_uint32 () % (G_N_ELEMENTS (buf) + 1);
+		char strbuf1[G_N_ELEMENTS (buf) * 3];
+		gboolean allocate = nmtst_get_rand_bool ();
+		char delimiter = nmtst_get_rand_bool () ? ':' : '\0';
+		gboolean upper_case = nmtst_get_rand_bool ();
+		gsize expected_strlen;
+		char *str_hex;
+		gsize required_len;
+		gboolean outlen_set;
+		gsize outlen;
+		guint8 *bin2;
+
+		nmtst_rand_buf (NULL, buf, len);
+
+		if (len == 0)
+			expected_strlen = 0;
+		else if (delimiter != '\0')
+			expected_strlen = (len * 3u) - 1;
+		else
+			expected_strlen = len * 2u;
+
+		g_assert_cmpint (expected_strlen, <, G_N_ELEMENTS (strbuf1));
+
+		str_hex = nm_utils_bin2hexstr_full (buf, len, delimiter, upper_case, !allocate ? strbuf1 : NULL);
+
+		g_assert (str_hex);
+		if (!allocate)
+			g_assert (str_hex == strbuf1);
+		g_assert_cmpint (strlen (str_hex), ==, expected_strlen);
+
+		g_assert (NM_STRCHAR_ALL (str_hex, ch,    (ch >= '0' && ch <= '9')
+		                                       || ch == delimiter
+		                                       || (  upper_case
+		                                           ? (ch >= 'A' && ch <= 'F')
+		                                           : (ch >= 'a' && ch <= 'f'))));
+
+		required_len = nmtst_get_rand_bool () ? len : 0u;
+
+		outlen_set = required_len == 0 || nmtst_get_rand_bool ();
+
+		memset (buf2, 0, sizeof (buf2));
+
+		bin2 = nm_utils_hexstr2bin_full (str_hex,
+		                                 nmtst_get_rand_bool (),
+		                                 delimiter != '\0' && nmtst_get_rand_bool (),
+		                                   delimiter != '\0'
+		                                 ? nmtst_rand_select ((const char *) ":", ":-")
+		                                 : nmtst_rand_select ((const char *) ":", ":-", "", NULL),
+		                                 required_len,
+		                                 buf2,
+		                                 len,
+		                                 outlen_set ? &outlen : NULL);
+		if (len > 0) {
+			g_assert (bin2);
+			g_assert (bin2 == buf2);
+		} else
+			g_assert (!bin2);
+
+		if (outlen_set)
+			g_assert_cmpint (outlen, ==, len);
+
+		g_assert_cmpmem (buf, len, buf2, len);
+
+		g_assert (buf2[len] == '\0');
+
+		if (allocate)
+			g_free (str_hex);
+	}
+}
+
 /*****************************************************************************/
 
 NMTST_DEFINE ();
@@ -458,7 +525,7 @@ int main (int argc, char **argv)
 	g_test_add_func ("/general/test_unaligned", test_unaligned);
 	g_test_add_func ("/general/test_strv_cmp", test_strv_cmp);
 	g_test_add_func ("/general/test_strstrip_avoid_copy", test_strstrip_avoid_copy);
+	g_test_add_func ("/general/test_nm_utils_bin2hexstr", test_nm_utils_bin2hexstr);
 
 	return g_test_run ();
 }
-
diff --git a/shared/nm-version-macros.h b/shared/nm-version-macros.h
index 6c5e8557..c8cc7de1 100644
--- a/shared/nm-version-macros.h
+++ b/shared/nm-version-macros.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /*
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -37,7 +36,7 @@
  * Evaluates to the minor version number of NetworkManager which this source
  * is compiled against.
  */
-#define NM_MINOR_VERSION (18)
+#define NM_MINOR_VERSION (19)
 
 /**
  * NM_MICRO_VERSION:
@@ -45,7 +44,7 @@
  * Evaluates to the micro version number of NetworkManager which this source
  * compiled against.
  */
-#define NM_MICRO_VERSION (0)
+#define NM_MICRO_VERSION (90)
 
 /**
  * NM_CHECK_VERSION:
@@ -76,6 +75,7 @@
 #define NM_VERSION_1_14   (NM_ENCODE_VERSION (1, 14, 0))
 #define NM_VERSION_1_16   (NM_ENCODE_VERSION (1, 16, 0))
 #define NM_VERSION_1_18   (NM_ENCODE_VERSION (1, 18, 0))
+#define NM_VERSION_1_20   (NM_ENCODE_VERSION (1, 20, 0))
 
 /* For releases, NM_API_VERSION is equal to NM_VERSION.
  *
diff --git a/shared/nm-version-macros.h.in b/shared/nm-version-macros.h.in
index 4b57529a..a546b100 100644
--- a/shared/nm-version-macros.h.in
+++ b/shared/nm-version-macros.h.in
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /*
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -76,6 +75,7 @@
 #define NM_VERSION_1_14   (NM_ENCODE_VERSION (1, 14, 0))
 #define NM_VERSION_1_16   (NM_ENCODE_VERSION (1, 16, 0))
 #define NM_VERSION_1_18   (NM_ENCODE_VERSION (1, 18, 0))
+#define NM_VERSION_1_20   (NM_ENCODE_VERSION (1, 20, 0))
 
 /* For releases, NM_API_VERSION is equal to NM_VERSION.
  *
diff --git a/shared/systemd/nm-logging-stub.c b/shared/systemd/nm-logging-stub.c
index 59699228..95ff51e5 100644
--- a/shared/systemd/nm-logging-stub.c
+++ b/shared/systemd/nm-logging-stub.c
@@ -45,3 +45,10 @@ _nm_log_impl (const char *file,
               ...)
 {
 }
+
+void
+_nm_utils_monotonic_timestamp_initialized (const struct timespec *tp,
+                                           gint64 offset_sec,
+                                           gboolean is_boottime)
+{
+}
diff --git a/shared/systemd/nm-sd-utils-shared.c b/shared/systemd/nm-sd-utils-shared.c
index ecd27492..7206fa52 100644
--- a/shared/systemd/nm-sd-utils-shared.c
+++ b/shared/systemd/nm-sd-utils-shared.c
@@ -24,6 +24,11 @@
 
 #include "path-util.h"
 #include "hexdecoct.h"
+#include "dns-domain.h"
+
+/*****************************************************************************/
+
+const bool mempool_use_allowed = true;
 
 /*****************************************************************************/
 
@@ -83,3 +88,11 @@ nm_sd_utils_unbase64mem (const char *p,
 {
 	return unbase64mem_full (p, l, secure, (void **) mem, len);
 }
+
+int nm_sd_dns_name_to_wire_format (const char *domain,
+                                   guint8 *buffer,
+                                   size_t len,
+                                   gboolean canonical)
+{
+	return dns_name_to_wire_format (domain, buffer, len, canonical);
+}
diff --git a/shared/systemd/nm-sd-utils-shared.h b/shared/systemd/nm-sd-utils-shared.h
index b3b77c88..48a8cf37 100644
--- a/shared/systemd/nm-sd-utils-shared.h
+++ b/shared/systemd/nm-sd-utils-shared.h
@@ -39,4 +39,9 @@ int nm_sd_utils_unbase64mem (const char *p,
 
 /*****************************************************************************/
 
+int nm_sd_dns_name_to_wire_format (const char *domain,
+                                   guint8 *buffer,
+                                   size_t len,
+                                   gboolean canonical);
+
 #endif /* __NM_SD_UTILS_SHARED_H__ */
diff --git a/shared/systemd/sd-adapt-shared/format-util.h b/shared/systemd/sd-adapt-shared/format-util.h
deleted file mode 100644
index 637892c2..00000000
--- a/shared/systemd/sd-adapt-shared/format-util.h
+++ /dev/null
@@ -1,3 +0,0 @@
-#pragma once
-
-/* dummy header */
diff --git a/shared/systemd/sd-adapt-shared/nm-sd-adapt-shared.h b/shared/systemd/sd-adapt-shared/nm-sd-adapt-shared.h
index a285c3cd..8dd93341 100644
--- a/shared/systemd/sd-adapt-shared/nm-sd-adapt-shared.h
+++ b/shared/systemd/sd-adapt-shared/nm-sd-adapt-shared.h
@@ -1,4 +1,3 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */
 /* This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  * the Free Software Foundation; either version 2, or (at your option)
@@ -21,8 +20,6 @@
 
 #include "nm-default.h"
 
-#include <syslog.h>
-
 #include "nm-glib-aux/nm-logging-fwd.h"
 
 /*****************************************************************************/
@@ -32,32 +29,29 @@
 
 /*****************************************************************************/
 
-static inline NMLogLevel
-_slog_level_to_nm (int slevel)
-{
-	switch (LOG_PRI (slevel)) {
-	case LOG_DEBUG:   return LOGL_DEBUG;
-	case LOG_WARNING: return LOGL_WARN;
-	case LOG_CRIT:
-	case LOG_ERR:     return LOGL_ERR;
-	case LOG_INFO:
-	case LOG_NOTICE:
-	default:          return LOGL_INFO;
-	}
-}
+/* systemd detects whether compiler supports "-Wstringop-truncation" to disable
+ * the warning at particular places. Since we anyway build with -Wno-pragma,
+ * we don't do that and just let systemd call
+ *
+ *   _Pragma("GCC diagnostic ignored \"-Wstringop-truncation\"")
+ *
+ * regadless whether that would result in a -Wpragma warning. */
+#define HAVE_WSTRINGOP_TRUNCATION 1
+
+/*****************************************************************************/
 
 static inline int
 _nm_log_get_max_level_realm (void)
 {
 	/* inline function, to avoid coverity warning about constant expression. */
-	return LOG_DEBUG;
+	return 7 /* LOG_DEBUG */;
 }
 #define log_get_max_level_realm(realm) _nm_log_get_max_level_realm ()
 
 #define log_internal_realm(level, error, file, line, func, format, ...) \
 ({ \
 	const int _nm_e = (error); \
-	const NMLogLevel _nm_l = _slog_level_to_nm ((level)); \
+	const NMLogLevel _nm_l = nm_log_level_from_syslog (LOG_PRI (level)); \
 	\
 	if (_nm_log_enabled_impl (!(NM_THREAD_SAFE_ON_MAIN_THREAD), _nm_l, LOGD_SYSTEMD)) { \
 		const char *_nm_location = strrchr ((""file), '/'); \
@@ -102,6 +96,14 @@ G_STMT_START { \
 #include <sys/syscall.h>
 #include <sys/ioctl.h>
 
+/*****************************************************************************/
+
+/* systemd cannot be compiled with "-Wdeclaration-after-statement". In particular
+ * in combintation with assert_cc(). */
+NM_PRAGMA_WARNING_DISABLE ("-Wdeclaration-after-statement")
+
+/*****************************************************************************/
+
 static inline pid_t
 raw_getpid (void) {
 #if defined(__alpha__)
diff --git a/shared/systemd/sd-adapt-shared/strxcpyx.h b/shared/systemd/sd-adapt-shared/strxcpyx.h
deleted file mode 100644
index 637892c2..00000000
--- a/shared/systemd/sd-adapt-shared/strxcpyx.h
+++ /dev/null
@@ -1,3 +0,0 @@
-#pragma once
-
-/* dummy header */
diff --git a/shared/systemd/src/basic/alloc-util.c b/shared/systemd/src/basic/alloc-util.c
index 92b350bc..c97d8700 100644
--- a/shared/systemd/src/basic/alloc-util.c
+++ b/shared/systemd/src/basic/alloc-util.c
@@ -66,8 +66,31 @@ void* greedy_realloc(void **p, size_t *allocated, size_t need, size_t size) {
         if (!q)
                 return NULL;
 
+        if (size > 0) {
+                size_t bn;
+
+                /* Adjust for the 64 byte minimum */
+                newalloc = a / size;
+
+                bn = malloc_usable_size(q) / size;
+                if (bn > newalloc) {
+                        void *qq;
+
+                        /* The actual size allocated is larger than what we asked for. Let's call realloc() again to
+                         * take possession of the extra space. This should be cheap, since libc doesn't have to move
+                         * the memory for this. */
+
+                        qq = realloc(q, bn * size);
+                        if (_likely_(qq)) {
+                                *p = qq;
+                                *allocated = bn;
+                                return qq;
+                        }
+                }
+        }
+
         *p = q;
-        *allocated = _unlikely_(size == 0) ? newalloc : malloc_usable_size(q) / size;
+        *allocated = newalloc;
         return q;
 }
 
diff --git a/shared/systemd/src/basic/alloc-util.h b/shared/systemd/src/basic/alloc-util.h
index 9b20be47..64d9e003 100644
--- a/shared/systemd/src/basic/alloc-util.h
+++ b/shared/systemd/src/basic/alloc-util.h
@@ -58,7 +58,7 @@ static inline void *mfree(void *memory) {
         })
 
 void* memdup(const void *p, size_t l) _alloc_(2);
-void* memdup_suffix0(const void *p, size_t l) _alloc_(2);
+void* memdup_suffix0(const void *p, size_t l); /* We can't use _alloc_() here, since we return a buffer one byte larger than the specified size */
 
 #define memdupa(p, l)                           \
         ({                                      \
@@ -112,7 +112,9 @@ _alloc_(2, 3) static inline void *memdup_multiply(const void *p, size_t size, si
         return memdup(p, size * need);
 }
 
-_alloc_(2, 3) static inline void *memdup_suffix0_multiply(const void *p, size_t size, size_t need) {
+/* Note that we can't decorate this function with _alloc_() since the returned memory area is one byte larger
+ * than the product of its parameters. */
+static inline void *memdup_suffix0_multiply(const void *p, size_t size, size_t need) {
         if (size_multiply_overflow(size, need))
                 return NULL;
 
diff --git a/shared/systemd/src/basic/env-file.c b/shared/systemd/src/basic/env-file.c
index 4a0f9c39..5860b050 100644
--- a/shared/systemd/src/basic/env-file.c
+++ b/shared/systemd/src/basic/env-file.c
@@ -2,8 +2,6 @@
 
 #include "nm-sd-adapt-shared.h"
 
-#include <stdio_ext.h>
-
 #include "alloc-util.h"
 #include "env-file.h"
 #include "env-util.h"
@@ -548,7 +546,6 @@ int write_env_file(const char *fname, char **l) {
         if (r < 0)
                 return r;
 
-        (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
         (void) fchmod_umask(fileno(f), 0644);
 
         STRV_FOREACH(i, l)
diff --git a/shared/systemd/src/basic/env-util.c b/shared/systemd/src/basic/env-util.c
index dc10362d..cd9d3176 100644
--- a/shared/systemd/src/basic/env-util.c
+++ b/shared/systemd/src/basic/env-util.c
@@ -75,7 +75,7 @@ bool env_value_is_valid(const char *e) {
          * either. Discounting the shortest possible variable name of
          * length 1, the equal sign and trailing NUL this hence leaves
          * ARG_MAX-3 as longest possible variable value. */
-        if (strlen(e) > (size_t) sysconf(_SC_ARG_MAX) - 3)
+        if (strlen(e) > sc_arg_max() - 3)
                 return false;
 
         return true;
@@ -98,7 +98,7 @@ bool env_assignment_is_valid(const char *e) {
          * be > ARG_MAX, hence the individual variable assignments
          * cannot be either, but let's leave room for one trailing NUL
          * byte. */
-        if (strlen(e) > (size_t) sysconf(_SC_ARG_MAX) - 1)
+        if (strlen(e) > sc_arg_max() - 1)
                 return false;
 
         return true;
@@ -570,7 +570,7 @@ char *replace_env_n(const char *format, size_t n, char **env, unsigned flags) {
 
                                 t = strv_env_get_n(env, word+2, e-word-2, flags);
 
-                                k = strappend(r, t);
+                                k = strjoin(r, t);
                                 if (!k)
                                         return NULL;
 
@@ -626,7 +626,7 @@ char *replace_env_n(const char *format, size_t n, char **env, unsigned flags) {
                                 else if (!t && state == DEFAULT_VALUE)
                                         t = v = replace_env_n(test_value, e-test_value, env, flags);
 
-                                k = strappend(r, t);
+                                k = strjoin(r, t);
                                 if (!k)
                                         return NULL;
 
@@ -645,7 +645,7 @@ char *replace_env_n(const char *format, size_t n, char **env, unsigned flags) {
 
                                 t = strv_env_get_n(env, word+1, e-word-1, flags);
 
-                                k = strappend(r, t);
+                                k = strjoin(r, t);
                                 if (!k)
                                         return NULL;
 
@@ -664,7 +664,7 @@ char *replace_env_n(const char *format, size_t n, char **env, unsigned flags) {
                 assert(flags & REPLACE_ENV_ALLOW_BRACELESS);
 
                 t = strv_env_get_n(env, word+1, e-word-1, flags);
-                return strappend(r, t);
+                return strjoin(r, t);
         } else
                 return strnappend(r, word, e-word);
 }
@@ -691,7 +691,7 @@ char **replace_env_argv(char **argv, char **env) {
                         if (e) {
                                 int r;
 
-                                r = strv_split_extract(&m, e, WHITESPACE, EXTRACT_RELAX|EXTRACT_QUOTES);
+                                r = strv_split_extract(&m, e, WHITESPACE, EXTRACT_RELAX|EXTRACT_UNQUOTE);
                                 if (r < 0) {
                                         ret[k] = NULL;
                                         strv_free(ret);
diff --git a/shared/systemd/src/basic/env-util.h b/shared/systemd/src/basic/env-util.h
index d54f9965..92802ed7 100644
--- a/shared/systemd/src/basic/env-util.h
+++ b/shared/systemd/src/basic/env-util.h
@@ -4,10 +4,17 @@
 #include <stdbool.h>
 #include <stddef.h>
 #include <stdio.h>
+#include <unistd.h>
 
 #include "macro.h"
 #include "string.h"
 
+static inline size_t sc_arg_max(void) {
+        long l = sysconf(_SC_ARG_MAX);
+        assert(l > 0);
+        return (size_t) l;
+}
+
 bool env_name_is_valid(const char *e);
 bool env_value_is_valid(const char *e);
 bool env_assignment_is_valid(const char *e);
diff --git a/shared/systemd/src/basic/errno-util.h b/shared/systemd/src/basic/errno-util.h
index d7a5ea77..6053cde6 100644
--- a/shared/systemd/src/basic/errno-util.h
+++ b/shared/systemd/src/basic/errno-util.h
@@ -1,6 +1,9 @@
 /* SPDX-License-Identifier: LGPL-2.1+ */
 #pragma once
 
+#include <stdlib.h>
+#include <string.h>
+
 #include "macro.h"
 
 static inline void _reset_errno_(int *saved_errno) {
@@ -28,6 +31,22 @@ static inline int negative_errno(void) {
         return -errno;
 }
 
+static inline const char *strerror_safe(int error) {
+        /* 'safe' here does NOT mean thread safety. */
+        return strerror(abs(error));
+}
+
+static inline int errno_or_else(int fallback) {
+        /* To be used when invoking library calls where errno handling is not defined clearly: we return
+         * errno if it is set, and the specified error otherwise. The idea is that the caller initializes
+         * errno to zero before doing an API call, and then uses this helper to retrieve a somewhat useful
+         * error code */
+        if (errno > 0)
+                return -errno;
+
+        return -abs(fallback);
+}
+
 /* Hint #1: ENETUNREACH happens if we try to connect to "non-existing" special IP addresses, such as ::5.
  *
  * Hint #2: The kernel sends e.g., EHOSTUNREACH or ENONET to userspace in some ICMP error cases.  See the
diff --git a/shared/systemd/src/basic/escape.c b/shared/systemd/src/basic/escape.c
index 8f7a1b33..06d823c7 100644
--- a/shared/systemd/src/basic/escape.c
+++ b/shared/systemd/src/basic/escape.c
@@ -370,34 +370,81 @@ int cunescape(const char *s, UnescapeFlags flags, char **ret) {
         return cunescape_length(s, strlen(s), flags, ret);
 }
 
-char *xescape(const char *s, const char *bad) {
-        char *r, *t;
+#if 0 /* NM_IGNORED */
+char *xescape_full(const char *s, const char *bad, size_t console_width, bool eight_bits) {
+        char *ans, *t, *prev, *prev2;
         const char *f;
 
-        /* Escapes all chars in bad, in addition to \ and all special
-         * chars, in \xFF style escaping. May be reversed with
-         * cunescape(). */
+        /* Escapes all chars in bad, in addition to \ and all special chars, in \xFF style escaping. May be
+         * reversed with cunescape(). If eight_bits is true, characters >= 127 are let through unchanged.
+         * This corresponds to non-ASCII printable characters in pre-unicode encodings.
+         *
+         * If console_width is reached, output is truncated and "..." is appended. */
 
-        r = new(char, strlen(s) * 4 + 1);
-        if (!r)
+        if (console_width == 0)
+                return strdup("");
+
+        ans = new(char, MIN(strlen(s), console_width) * 4 + 1);
+        if (!ans)
                 return NULL;
 
-        for (f = s, t = r; *f; f++) {
+        memset(ans, '_', MIN(strlen(s), console_width) * 4);
+        ans[MIN(strlen(s), console_width) * 4] = 0;
+
+        for (f = s, t = prev = prev2 = ans; ; f++) {
+                char *tmp_t = t;
+
+                if (!*f) {
+                        *t = 0;
+                        return ans;
+                }
+
+                if ((unsigned char) *f < ' ' || (!eight_bits && (unsigned char) *f >= 127) ||
+                    *f == '\\' || strchr(bad, *f)) {
+                        if ((size_t) (t - ans) + 4 > console_width)
+                                break;
 
-                if ((*f < ' ') || (*f >= 127) ||
-                    (*f == '\\') || strchr(bad, *f)) {
                         *(t++) = '\\';
                         *(t++) = 'x';
                         *(t++) = hexchar(*f >> 4);
                         *(t++) = hexchar(*f);
-                } else
+                } else {
+                        if ((size_t) (t - ans) + 1 > console_width)
+                                break;
+
                         *(t++) = *f;
+                }
+
+                /* We might need to go back two cycles to fit three dots, so remember two positions */
+                prev2 = prev;
+                prev = tmp_t;
         }
 
-        *t = 0;
+        /* We can just write where we want, since chars are one-byte */
+        size_t c = MIN(console_width, 3u); /* If the console is too narrow, write fewer dots */
+        size_t off;
+        if (console_width - c >= (size_t) (t - ans))
+                off = (size_t) (t - ans);
+        else if (console_width - c >= (size_t) (prev - ans))
+                off = (size_t) (prev - ans);
+        else if (console_width - c >= (size_t) (prev2 - ans))
+                off = (size_t) (prev2 - ans);
+        else
+                off = console_width - c;
+        assert(off <= (size_t) (t - ans));
 
-        return r;
+        memcpy(ans + off, "...", c);
+        ans[off + c] = '\0';
+        return ans;
+}
+
+char *escape_non_printable_full(const char *str, size_t console_width, bool eight_bit) {
+        if (eight_bit)
+                return xescape_full(str, "", console_width, true);
+        else
+                return utf8_escape_non_printable_full(str, console_width);
 }
+#endif /* NM_IGNORED */
 
 char *octescape(const char *s, size_t len) {
         char *r, *t;
diff --git a/shared/systemd/src/basic/escape.h b/shared/systemd/src/basic/escape.h
index 51562099..b26054c5 100644
--- a/shared/systemd/src/basic/escape.h
+++ b/shared/systemd/src/basic/escape.h
@@ -46,8 +46,12 @@ int cunescape_length(const char *s, size_t length, UnescapeFlags flags, char **r
 int cunescape_length_with_prefix(const char *s, size_t length, const char *prefix, UnescapeFlags flags, char **ret);
 int cunescape_one(const char *p, size_t length, char32_t *ret, bool *eight_bit);
 
-char *xescape(const char *s, const char *bad);
+char *xescape_full(const char *s, const char *bad, size_t console_width, bool eight_bits);
+static inline char *xescape(const char *s, const char *bad) {
+        return xescape_full(s, bad, SIZE_MAX, false);
+}
 char *octescape(const char *s, size_t len);
+char *escape_non_printable_full(const char *str, size_t console_width, bool eight_bit);
 
 char *shell_escape(const char *s, const char *bad);
 char* shell_maybe_quote(const char *s, EscapeStyle style);
diff --git a/shared/systemd/src/basic/extract-word.c b/shared/systemd/src/basic/extract-word.c
index 782c868b..15cbaafb 100644
--- a/shared/systemd/src/basic/extract-word.c
+++ b/shared/systemd/src/basic/extract-word.c
@@ -30,6 +30,8 @@ int extract_first_word(const char **p, char **ret, const char *separators, Extra
 
         assert(p);
         assert(ret);
+        /* Those two don't make sense together. */
+        assert(!FLAGS_SET(flags, EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE));
 
         /* Bail early if called after last value or with no input */
         if (!*p)
@@ -137,7 +139,7 @@ int extract_first_word(const char **p, char **ret, const char *separators, Extra
                         for (;; (*p)++, c = **p) {
                                 if (c == 0)
                                         goto finish_force_terminate;
-                                else if (IN_SET(c, '\'', '"') && (flags & EXTRACT_QUOTES)) {
+                                else if (IN_SET(c, '\'', '"') && (flags & EXTRACT_UNQUOTE)) {
                                         quote = c;
                                         break;
                                 } else if (c == '\\' && !(flags & EXTRACT_RETAIN_ESCAPE)) {
diff --git a/shared/systemd/src/basic/extract-word.h b/shared/systemd/src/basic/extract-word.h
index 705ebbe9..e2d43389 100644
--- a/shared/systemd/src/basic/extract-word.h
+++ b/shared/systemd/src/basic/extract-word.h
@@ -7,7 +7,7 @@ typedef enum ExtractFlags {
         EXTRACT_RELAX                    = 1 << 0,
         EXTRACT_CUNESCAPE                = 1 << 1,
         EXTRACT_CUNESCAPE_RELAX          = 1 << 2,
-        EXTRACT_QUOTES                   = 1 << 3,
+        EXTRACT_UNQUOTE                  = 1 << 3,
         EXTRACT_DONT_COALESCE_SEPARATORS = 1 << 4,
         EXTRACT_RETAIN_ESCAPE            = 1 << 5,
 } ExtractFlags;
diff --git a/shared/systemd/src/basic/fileio.c b/shared/systemd/src/basic/fileio.c
index 0dfb4574..bd2afbbe 100644
--- a/shared/systemd/src/basic/fileio.c
+++ b/shared/systemd/src/basic/fileio.c
@@ -23,6 +23,7 @@
 #include "log.h"
 #include "macro.h"
 #include "missing.h"
+#include "mkdir.h"
 #include "parse-util.h"
 #include "path-util.h"
 #include "stdio-util.h"
@@ -31,6 +32,52 @@
 
 #define READ_FULL_BYTES_MAX (4U*1024U*1024U)
 
+int fopen_unlocked(const char *path, const char *options, FILE **ret) {
+        assert(ret);
+
+        FILE *f = fopen(path, options);
+        if (!f)
+                return -errno;
+
+        (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
+
+        *ret = f;
+        return 0;
+}
+
+int fdopen_unlocked(int fd, const char *options, FILE **ret) {
+        assert(ret);
+
+        FILE *f = fdopen(fd, options);
+        if (!f)
+                return -errno;
+
+        (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
+
+        *ret = f;
+        return 0;
+}
+
+FILE* open_memstream_unlocked(char **ptr, size_t *sizeloc) {
+        FILE *f = open_memstream(ptr, sizeloc);
+        if (!f)
+                return NULL;
+
+        (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
+
+        return f;
+}
+
+FILE* fmemopen_unlocked(void *buf, size_t size, const char *mode) {
+        FILE *f = fmemopen(buf, size, mode);
+        if (!f)
+                return NULL;
+
+        (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
+
+        return f;
+}
+
 #if 0 /* NM_IGNORED */
 int write_string_stream_ts(
                 FILE *f,
@@ -98,7 +145,6 @@ static int write_string_file_atomic(
         if (r < 0)
                 return r;
 
-        (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
         (void) fchmod_umask(fileno(f), 0644);
 
         r = write_string_stream_ts(f, line, flags, ts);
@@ -132,6 +178,12 @@ int write_string_file_ts(
         /* We don't know how to verify whether the file contents was already on-disk. */
         assert(!((flags & WRITE_STRING_FILE_VERIFY_ON_FAILURE) && (flags & WRITE_STRING_FILE_SYNC)));
 
+        if (flags & WRITE_STRING_FILE_MKDIR_0755) {
+                r = mkdir_parents(fn, 0755);
+                if (r < 0)
+                        return r;
+        }
+
         if (flags & WRITE_STRING_FILE_ATOMIC) {
                 assert(flags & WRITE_STRING_FILE_CREATE);
 
@@ -144,11 +196,9 @@ int write_string_file_ts(
                 assert(!ts);
 
         if (flags & WRITE_STRING_FILE_CREATE) {
-                f = fopen(fn, "we");
-                if (!f) {
-                        r = -errno;
+                r = fopen_unlocked(fn, "we", &f);
+                if (r < 0)
                         goto fail;
-                }
         } else {
                 int fd;
 
@@ -160,16 +210,13 @@ int write_string_file_ts(
                         goto fail;
                 }
 
-                f = fdopen(fd, "w");
-                if (!f) {
-                        r = -errno;
+                r = fdopen_unlocked(fd, "w", &f);
+                if (r < 0) {
                         safe_close(fd);
                         goto fail;
                 }
         }
 
-        (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
-
         if (flags & WRITE_STRING_FILE_DISABLE_BUFFER)
                 setvbuf(f, NULL, _IONBF, 0);
 
@@ -216,15 +263,14 @@ int write_string_filef(
 
 int read_one_line_file(const char *fn, char **line) {
         _cleanup_fclose_ FILE *f = NULL;
+        int r;
 
         assert(fn);
         assert(line);
 
-        f = fopen(fn, "re");
-        if (!f)
-                return -errno;
-
-        (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
+        r = fopen_unlocked(fn, "re", &f);
+        if (r < 0)
+                return r;
 
         return read_line(f, LONG_LINE_MAX, line);
 }
@@ -233,6 +279,7 @@ int verify_file(const char *fn, const char *blob, bool accept_extra_nl) {
         _cleanup_fclose_ FILE *f = NULL;
         _cleanup_free_ char *buf = NULL;
         size_t l, k;
+        int r;
 
         assert(fn);
         assert(blob);
@@ -246,17 +293,15 @@ int verify_file(const char *fn, const char *blob, bool accept_extra_nl) {
         if (!buf)
                 return -ENOMEM;
 
-        f = fopen(fn, "re");
-        if (!f)
-                return -errno;
-
-        (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
+        r = fopen_unlocked(fn, "re", &f);
+        if (r < 0)
+                return r;
 
         /* We try to read one byte more than we need, so that we know whether we hit eof */
         errno = 0;
         k = fread(buf, 1, l + accept_extra_nl + 1, f);
         if (ferror(f))
-                return errno > 0 ? -errno : -EIO;
+                return errno_or_else(EIO);
 
         if (k != l && k != l + accept_extra_nl)
                 return 0;
@@ -283,7 +328,8 @@ int read_full_stream_full(
 
         assert(f);
         assert(ret_contents);
-        assert(!(flags & READ_FULL_FILE_UNBASE64) || ret_size);
+        assert(!FLAGS_SET(flags, READ_FULL_FILE_UNBASE64 | READ_FULL_FILE_UNHEX));
+        assert(!(flags & (READ_FULL_FILE_UNBASE64 | READ_FULL_FILE_UNHEX)) || ret_size);
 
         n_next = LINE_MAX; /* Start size */
 
@@ -324,6 +370,7 @@ int read_full_stream_full(
                         }
                         memcpy_safe(t, buf, n);
                         explicit_bzero_safe(buf, n);
+                        buf = mfree(buf);
                 } else {
                         t = realloc(buf, n_next + 1);
                         if (!t)
@@ -339,7 +386,7 @@ int read_full_stream_full(
                         l += k;
 
                 if (ferror(f)) {
-                        r = errno > 0 ? -errno : -EIO;
+                        r = errno_or_else(EIO);
                         goto finalize;
                 }
 
@@ -360,9 +407,12 @@ int read_full_stream_full(
                 n_next = MIN(n * 2, READ_FULL_BYTES_MAX);
         }
 
-        if (flags & READ_FULL_FILE_UNBASE64) {
+        if (flags & (READ_FULL_FILE_UNBASE64 | READ_FULL_FILE_UNHEX)) {
                 buf[l++] = 0;
-                r = unbase64mem_full(buf, l, flags & READ_FULL_FILE_SECURE, (void **) ret_contents, ret_size);
+                if (flags & READ_FULL_FILE_UNBASE64)
+                        r = unbase64mem_full(buf, l, flags & READ_FULL_FILE_SECURE, (void **) ret_contents, ret_size);
+                else
+                        r = unhexmem_full(buf, l, flags & READ_FULL_FILE_SECURE, (void **) ret_contents, ret_size);
                 goto finalize;
         }
 
@@ -394,15 +444,14 @@ finalize:
 
 int read_full_file_full(const char *filename, ReadFullFileFlags flags, char **contents, size_t *size) {
         _cleanup_fclose_ FILE *f = NULL;
+        int r;
 
         assert(filename);
         assert(contents);
 
-        f = fopen(filename, "re");
-        if (!f)
-                return -errno;
-
-        (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
+        r = fopen_unlocked(filename, "re", &f);
+        if (r < 0)
+                return r;
 
         return read_full_stream_full(f, filename, flags, contents, size);
 }
@@ -547,10 +596,7 @@ static int search_and_fopen_internal(const char *path, const char *mode, const c
                 _cleanup_free_ char *p = NULL;
                 FILE *f;
 
-                if (root)
-                        p = strjoin(root, *i, "/", path);
-                else
-                        p = strjoin(*i, "/", path);
+                p = path_join(root, *i, path);
                 if (!p)
                         return -ENOMEM;
 
@@ -623,7 +669,7 @@ int fflush_and_check(FILE *f) {
         fflush(f);
 
         if (ferror(f))
-                return errno > 0 ? -errno : -EIO;
+                return errno_or_else(EIO);
 
         return 0;
 }
@@ -741,7 +787,7 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(FILE*, funlockfile);
 int read_line_full(FILE *f, size_t limit, ReadLineFlags flags, char **ret) {
         size_t n = 0, allocated = 0, count = 0;
         _cleanup_free_ char *buffer = NULL;
-        int r;
+        int r, tty = -1;
 
         assert(f);
 
@@ -816,6 +862,17 @@ int read_line_full(FILE *f, size_t limit, ReadLineFlags flags, char **ret) {
                         count++;
 
                         if (eol != EOL_NONE) {
+                                /* If we are on a tty, we can't wait for more input. But we expect only
+                                 * \n as the single EOL marker, so there is no need to wait. We check
+                                 * this condition last to avoid isatty() check if not necessary. */
+
+                                if (tty < 0)
+                                        tty = isatty(fileno(f));
+                                if (tty > 0)
+                                        break;
+                        }
+
+                        if (eol != EOL_NONE) {
                                 previous_eol |= eol;
                                 continue;
                         }
@@ -853,7 +910,7 @@ int safe_fgetc(FILE *f, char *ret) {
         k = fgetc(f);
         if (k == EOF) {
                 if (ferror(f))
-                        return errno > 0 ? -errno : -EIO;
+                        return errno_or_else(EIO);
 
                 if (ret)
                         *ret = 0;
diff --git a/shared/systemd/src/basic/fileio.h b/shared/systemd/src/basic/fileio.h
index 760e7386..05f6c89d 100644
--- a/shared/systemd/src/basic/fileio.h
+++ b/shared/systemd/src/basic/fileio.h
@@ -21,6 +21,7 @@ typedef enum {
         WRITE_STRING_FILE_SYNC              = 1 << 4,
         WRITE_STRING_FILE_DISABLE_BUFFER    = 1 << 5,
         WRITE_STRING_FILE_NOFOLLOW          = 1 << 6,
+        WRITE_STRING_FILE_MKDIR_0755        = 1 << 7,
 
         /* And before you wonder, why write_string_file_atomic_label_ts() is a separate function instead of just one
            more flag here: it's about linking: we don't want to pull -lselinux into all users of write_string_file()
@@ -31,8 +32,14 @@ typedef enum {
 typedef enum {
         READ_FULL_FILE_SECURE   = 1 << 0,
         READ_FULL_FILE_UNBASE64 = 1 << 1,
+        READ_FULL_FILE_UNHEX    = 1 << 2,
 } ReadFullFileFlags;
 
+int fopen_unlocked(const char *path, const char *options, FILE **ret);
+int fdopen_unlocked(int fd, const char *options, FILE **ret);
+FILE* open_memstream_unlocked(char **ptr, size_t *sizeloc);
+FILE* fmemopen_unlocked(void *buf, size_t size, const char *mode);
+
 int write_string_stream_ts(FILE *f, const char *line, WriteStringFileFlags flags, struct timespec *ts);
 static inline int write_string_stream(FILE *f, const char *line, WriteStringFileFlags flags) {
         return write_string_stream_ts(f, line, flags, NULL);
diff --git a/shared/systemd/src/basic/format-util.c b/shared/systemd/src/basic/format-util.c
new file mode 100644
index 00000000..7a3e735b
--- /dev/null
+++ b/shared/systemd/src/basic/format-util.c
@@ -0,0 +1,72 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "nm-sd-adapt-shared.h"
+
+#include <stdio.h>
+
+#include "format-util.h"
+#include "memory-util.h"
+
+char *format_ifname(int ifindex, char buf[static IF_NAMESIZE + 1]) {
+        /* Buffer is always cleared */
+        memzero(buf, IF_NAMESIZE + 1);
+        return if_indextoname(ifindex, buf);
+}
+
+char *format_bytes_full(char *buf, size_t l, uint64_t t, FormatBytesFlag flag) {
+        typedef struct {
+                const char *suffix;
+                uint64_t factor;
+        } suffix_table;
+        static const suffix_table table_iec[] = {
+                { "E", UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024) },
+                { "P", UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024) },
+                { "T", UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024) },
+                { "G", UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024) },
+                { "M", UINT64_C(1024)*UINT64_C(1024) },
+                { "K", UINT64_C(1024) },
+        }, table_si[] = {
+                { "E", UINT64_C(1000)*UINT64_C(1000)*UINT64_C(1000)*UINT64_C(1000)*UINT64_C(1000)*UINT64_C(1000) },
+                { "P", UINT64_C(1000)*UINT64_C(1000)*UINT64_C(1000)*UINT64_C(1000)*UINT64_C(1000) },
+                { "T", UINT64_C(1000)*UINT64_C(1000)*UINT64_C(1000)*UINT64_C(1000) },
+                { "G", UINT64_C(1000)*UINT64_C(1000)*UINT64_C(1000) },
+                { "M", UINT64_C(1000)*UINT64_C(1000) },
+                { "K", UINT64_C(1000) },
+        };
+        const suffix_table *table;
+        size_t n, i;
+
+        assert_cc(ELEMENTSOF(table_iec) == ELEMENTSOF(table_si));
+
+        if (t == (uint64_t) -1)
+                return NULL;
+
+        table = flag & FORMAT_BYTES_USE_IEC ? table_iec : table_si;
+        n = ELEMENTSOF(table_iec);
+
+        for (i = 0; i < n; i++)
+                if (t >= table[i].factor) {
+                        if (flag & FORMAT_BYTES_BELOW_POINT) {
+                                snprintf(buf, l,
+                                         "%" PRIu64 ".%" PRIu64 "%s",
+                                         t / table[i].factor,
+                                         i != n - 1 ?
+                                         (t / table[i + 1].factor * UINT64_C(10) / table[n - 1].factor) % UINT64_C(10):
+                                         (t * UINT64_C(10) / table[i].factor) % UINT64_C(10),
+                                         table[i].suffix);
+                        } else
+                                snprintf(buf, l,
+                                         "%" PRIu64 "%s",
+                                         t / table[i].factor,
+                                         table[i].suffix);
+
+                        goto finish;
+                }
+
+        snprintf(buf, l, "%" PRIu64 "%s", t, flag & FORMAT_BYTES_TRAILING_B ? "B" : "");
+
+finish:
+        buf[l-1] = 0;
+        return buf;
+
+}
diff --git a/shared/systemd/src/basic/format-util.h b/shared/systemd/src/basic/format-util.h
new file mode 100644
index 00000000..e0d184a5
--- /dev/null
+++ b/shared/systemd/src/basic/format-util.h
@@ -0,0 +1,83 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include <inttypes.h>
+#include <net/if.h>
+#include <stdbool.h>
+
+#if SIZEOF_PID_T == 4
+#  define PID_PRI PRIi32
+#elif SIZEOF_PID_T == 2
+#  define PID_PRI PRIi16
+#else
+#  error Unknown pid_t size
+#endif
+#define PID_FMT "%" PID_PRI
+
+#if SIZEOF_UID_T == 4
+#  define UID_FMT "%" PRIu32
+#elif SIZEOF_UID_T == 2
+#  define UID_FMT "%" PRIu16
+#else
+#  error Unknown uid_t size
+#endif
+
+#if SIZEOF_GID_T == 4
+#  define GID_FMT "%" PRIu32
+#elif SIZEOF_GID_T == 2
+#  define GID_FMT "%" PRIu16
+#else
+#  error Unknown gid_t size
+#endif
+
+#if SIZEOF_TIME_T == 8
+#  define PRI_TIME PRIi64
+#elif SIZEOF_TIME_T == 4
+#  define PRI_TIME "li"
+#else
+#  error Unknown time_t size
+#endif
+
+#if defined __x86_64__ && defined __ILP32__
+#  define PRI_TIMEX PRIi64
+#else
+#  define PRI_TIMEX "li"
+#endif
+
+#if SIZEOF_RLIM_T == 8
+#  define RLIM_FMT "%" PRIu64
+#elif SIZEOF_RLIM_T == 4
+#  define RLIM_FMT "%" PRIu32
+#else
+#  error Unknown rlim_t size
+#endif
+
+#if SIZEOF_DEV_T == 8
+#  define DEV_FMT "%" PRIu64
+#elif SIZEOF_DEV_T == 4
+#  define DEV_FMT "%" PRIu32
+#else
+#  error Unknown dev_t size
+#endif
+
+#if SIZEOF_INO_T == 8
+#  define INO_FMT "%" PRIu64
+#elif SIZEOF_INO_T == 4
+#  define INO_FMT "%" PRIu32
+#else
+#  error Unknown ino_t size
+#endif
+
+char *format_ifname(int ifindex, char buf[static IF_NAMESIZE + 1]);
+
+typedef enum {
+        FORMAT_BYTES_USE_IEC     = 1 << 0,
+        FORMAT_BYTES_BELOW_POINT = 1 << 1,
+        FORMAT_BYTES_TRAILING_B  = 1 << 2,
+} FormatBytesFlag;
+
+#define FORMAT_BYTES_MAX 8
+char *format_bytes_full(char *buf, size_t l, uint64_t t, FormatBytesFlag flag);
+static inline char *format_bytes(char *buf, size_t l, uint64_t t) {
+        return format_bytes_full(buf, l, t, FORMAT_BYTES_USE_IEC | FORMAT_BYTES_BELOW_POINT | FORMAT_BYTES_TRAILING_B);
+}
diff --git a/shared/systemd/src/basic/fs-util.c b/shared/systemd/src/basic/fs-util.c
index be85eef1..56385fa2 100644
--- a/shared/systemd/src/basic/fs-util.c
+++ b/shared/systemd/src/basic/fs-util.c
@@ -218,113 +218,65 @@ int readlink_and_make_absolute(const char *p, char **r) {
 }
 
 int chmod_and_chown(const char *path, mode_t mode, uid_t uid, gid_t gid) {
-        char fd_path[STRLEN("/proc/self/fd/") + DECIMAL_STR_MAX(int) + 1];
         _cleanup_close_ int fd = -1;
-        bool st_valid = false;
-        struct stat st;
-        int r;
 
         assert(path);
 
-        /* Under the assumption that we are running privileged we first change the access mode and only then
-         * hand out ownership to avoid a window where access is too open. */
-
         fd = open(path, O_PATH|O_CLOEXEC|O_NOFOLLOW); /* Let's acquire an O_PATH fd, as precaution to change
                                                        * mode/owner on the same file */
         if (fd < 0)
                 return -errno;
 
-        xsprintf(fd_path, "/proc/self/fd/%i", fd);
-
-        if (mode != MODE_INVALID) {
-                if ((mode & S_IFMT) != 0) {
-
-                        if (stat(fd_path, &st) < 0)
-                                return -errno;
-
-                        if ((mode & S_IFMT) != (st.st_mode & S_IFMT))
-                                return -EINVAL;
-
-                        st_valid = true;
-                }
-
-                if (chmod(fd_path, mode & 07777) < 0) {
-                        r = -errno;
-
-                        if (!st_valid && stat(fd_path, &st) < 0)
-                                return -errno;
-
-                        if ((mode & 07777) != (st.st_mode & 07777))
-                                return r;
-
-                        st_valid = true;
-                }
-        }
-
-        if (uid != UID_INVALID || gid != GID_INVALID) {
-                if (chown(fd_path, uid, gid) < 0) {
-                        r = -errno;
-
-                        if (!st_valid && stat(fd_path, &st) < 0)
-                                return -errno;
-
-                        if (uid != UID_INVALID && st.st_uid != uid)
-                                return r;
-                        if (gid != GID_INVALID && st.st_gid != gid)
-                                return r;
-                }
-        }
-
-        return 0;
+        return fchmod_and_chown(fd, mode, uid, gid);
 }
 
 int fchmod_and_chown(int fd, mode_t mode, uid_t uid, gid_t gid) {
-        bool st_valid = false;
+        bool do_chown, do_chmod;
         struct stat st;
-        int r;
 
-        /* Under the assumption that we are running privileged we first change the access mode and only then hand out
-         * ownership to avoid a window where access is too open. */
+        /* Change ownership and access mode of the specified fd. Tries to do so safely, ensuring that at no
+         * point in time the access mode is above the old access mode under the old ownership or the new
+         * access mode under the new ownership. Note: this call tries hard to leave the access mode
+         * unaffected if the uid/gid is changed, i.e. it undoes implicit suid/sgid dropping the kernel does
+         * on chown().
+         *
+         * This call is happy with O_PATH fds. */
 
-        if (mode != MODE_INVALID) {
-                if ((mode & S_IFMT) != 0) {
+        if (fstat(fd, &st) < 0)
+                return -errno;
 
-                        if (fstat(fd, &st) < 0)
-                                return -errno;
+        do_chown =
+                (uid != UID_INVALID && st.st_uid != uid) ||
+                (gid != GID_INVALID && st.st_gid != gid);
 
-                        if ((mode & S_IFMT) != (st.st_mode & S_IFMT))
-                                return -EINVAL;
+        do_chmod =
+                !S_ISLNK(st.st_mode) && /* chmod is not defined on symlinks */
+                ((mode != MODE_INVALID && ((st.st_mode ^ mode) & 07777) != 0) ||
+                 do_chown); /* If we change ownership, make sure we reset the mode afterwards, since chown()
+                             * modifies the access mode too */
 
-                        st_valid = true;
-                }
+        if (mode == MODE_INVALID)
+                mode = st.st_mode; /* If we only shall do a chown(), save original mode, since chown() might break it. */
+        else if ((mode & S_IFMT) != 0 && ((mode ^ st.st_mode) & S_IFMT) != 0)
+                return -EINVAL; /* insist on the right file type if it was specified */
 
-                if (fchmod(fd, mode & 07777) < 0) {
-                        r = -errno;
+        if (do_chown && do_chmod) {
+                mode_t minimal = st.st_mode & mode; /* the subset of the old and the new mask */
 
-                        if (!st_valid && fstat(fd, &st) < 0)
+                if (((minimal ^ st.st_mode) & 07777) != 0)
+                        if (fchmod_opath(fd, minimal & 07777) < 0)
                                 return -errno;
-
-                        if ((mode & 07777) != (st.st_mode & 07777))
-                                return r;
-
-                        st_valid = true;
-                }
         }
 
-        if (uid != UID_INVALID || gid != GID_INVALID)
-                if (fchown(fd, uid, gid) < 0) {
-                        r = -errno;
-
-                        if (!st_valid && fstat(fd, &st) < 0)
-                                return -errno;
+        if (do_chown)
+                if (fchownat(fd, "", uid, gid, AT_EMPTY_PATH) < 0)
+                        return -errno;
 
-                        if (uid != UID_INVALID && st.st_uid != uid)
-                                return r;
-                        if (gid != GID_INVALID && st.st_gid != gid)
-                                return r;
-                }
+        if (do_chmod)
+                if (fchmod_opath(fd, mode & 07777) < 0)
+                        return -errno;
 
-        return 0;
+        return do_chown || do_chmod;
 }
 #endif /* NM_IGNORED */
 
@@ -411,13 +363,7 @@ int touch_file(const char *path, bool parents, usec_t stamp, uid_t uid, gid_t gi
          * something fchown(), fchmod(), futimensat() don't allow. */
         xsprintf(fdpath, "/proc/self/fd/%i", fd);
 
-        if (mode != MODE_INVALID)
-                if (chmod(fdpath, mode) < 0)
-                        ret = -errno;
-
-        if (uid_is_valid(uid) || gid_is_valid(gid))
-                if (chown(fdpath, uid, gid) < 0 && ret >= 0)
-                        ret = -errno;
+        ret = fchmod_and_chown(fd, mode, uid, gid);
 
         if (stamp != USEC_INFINITY) {
                 struct timespec ts[2];
@@ -1043,9 +989,9 @@ int chase_symlinks(const char *path, const char *original_root, unsigned flags,
 
                                 /* Prefix what's left to do with what we just read, and start the loop again, but
                                  * remain in the current directory. */
-                                joined = strjoin(destination, todo);
+                                joined = path_join(destination, todo);
                         } else
-                                joined = strjoin("/", destination, todo);
+                                joined = path_join("/", destination, todo);
                         if (!joined)
                                 return -ENOMEM;
 
diff --git a/shared/systemd/src/basic/fs-util.h b/shared/systemd/src/basic/fs-util.h
index b9651205..c5527cc4 100644
--- a/shared/systemd/src/basic/fs-util.h
+++ b/shared/systemd/src/basic/fs-util.h
@@ -7,12 +7,20 @@
 #include <stdbool.h>
 #include <stdint.h>
 #include <sys/inotify.h>
+#include <sys/stat.h>
 #include <sys/types.h>
 #include <unistd.h>
 
 #include "errno-util.h"
 #include "time-util.h"
 
+#define MODE_INVALID ((mode_t) -1)
+
+/* The following macros add 1 when converting things, since 0 is a valid mode, while the pointer
+ * NULL is special */
+#define PTR_TO_MODE(p) ((mode_t) ((uintptr_t) (p)-1))
+#define MODE_TO_PTR(u) ((void *) ((uintptr_t) (u)+1))
+
 int unlink_noerrno(const char *path);
 
 int rmdir_parents(const char *path, const char *stop);
diff --git a/shared/systemd/src/basic/hash-funcs.c b/shared/systemd/src/basic/hash-funcs.c
index ec4f1de6..03695098 100644
--- a/shared/systemd/src/basic/hash-funcs.c
+++ b/shared/systemd/src/basic/hash-funcs.c
@@ -13,6 +13,9 @@ void string_hash_func(const char *p, struct siphash *state) {
 
 #if 0 /* NM_IGNORED */
 DEFINE_HASH_OPS(string_hash_ops, char, string_hash_func, string_compare_func);
+DEFINE_HASH_OPS_FULL(string_hash_ops_free_free,
+                     char, string_hash_func, string_compare_func, free,
+                     char, free);
 
 void path_hash_func(const char *q, struct siphash *state) {
         size_t n;
diff --git a/shared/systemd/src/basic/hash-funcs.h b/shared/systemd/src/basic/hash-funcs.h
index 3d2ae4b5..0d2d4283 100644
--- a/shared/systemd/src/basic/hash-funcs.h
+++ b/shared/systemd/src/basic/hash-funcs.h
@@ -76,6 +76,7 @@ struct hash_ops {
 void string_hash_func(const char *p, struct siphash *state);
 #define string_compare_func strcmp
 extern const struct hash_ops string_hash_ops;
+extern const struct hash_ops string_hash_ops_free_free;
 
 void path_hash_func(const char *p, struct siphash *state);
 int path_compare_func(const char *a, const char *b) _pure_;
diff --git a/shared/systemd/src/basic/hashmap.c b/shared/systemd/src/basic/hashmap.c
index 9418dbd8..b1ae08cd 100644
--- a/shared/systemd/src/basic/hashmap.c
+++ b/shared/systemd/src/basic/hashmap.c
@@ -13,6 +13,7 @@
 #include "macro.h"
 #include "memory-util.h"
 #include "mempool.h"
+#include "missing.h"
 #include "process-util.h"
 #include "random-util.h"
 #include "set.h"
@@ -287,7 +288,11 @@ _destructor_ static void cleanup_pools(void) {
         /* The pool is only allocated by the main thread, but the memory can
          * be passed to other threads. Let's clean up if we are the main thread
          * and no other threads are live. */
-        if (!is_main_thread())
+        /* We build our own is_main_thread() here, which doesn't use C11
+         * TLS based caching of the result. That's because valgrind apparently
+         * doesn't like malloc() (which C11 TLS internally uses) to be called
+         * from a GCC destructors. */
+        if (getpid() != gettid())
                 return;
 
         r = get_proc_field("/proc/self/status", "Threads", WHITESPACE, &t);
@@ -730,8 +735,8 @@ bool internal_hashmap_iterate(HashmapBase *h, Iterator *i, void **value, const v
         return true;
 }
 
-bool set_iterate(Set *s, Iterator *i, void **value) {
-        return internal_hashmap_iterate(HASHMAP_BASE(s), i, value, NULL);
+bool set_iterate(const Set *s, Iterator *i, void **value) {
+        return internal_hashmap_iterate(HASHMAP_BASE((Set*) s), i, value, NULL);
 }
 
 #define HASHMAP_FOREACH_IDX(idx, h, i) \
@@ -1765,6 +1770,34 @@ int set_consume(Set *s, void *value) {
         return r;
 }
 
+#if 0 /* NM_IGNORED */
+int hashmap_put_strdup(Hashmap **h, const char *k, const char *v) {
+        int r;
+
+        r = hashmap_ensure_allocated(h, &string_hash_ops_free_free);
+        if (r < 0)
+                return r;
+
+        _cleanup_free_ char *kdup = NULL, *vdup = NULL;
+        kdup = strdup(k);
+        vdup = strdup(v);
+        if (!kdup || !vdup)
+                return -ENOMEM;
+
+        r = hashmap_put(*h, kdup, vdup);
+        if (r < 0) {
+                if (r == -EEXIST && streq(v, hashmap_get(*h, kdup)))
+                        return 0;
+                return r;
+        }
+
+        assert(r > 0); /* 0 would mean vdup is already in the hashmap, which cannot be */
+        kdup = vdup = NULL;
+
+        return 0;
+}
+#endif /* NM_IGNORED */
+
 int set_put_strdup(Set *s, const char *p) {
         char *c;
 
diff --git a/shared/systemd/src/basic/hashmap.h b/shared/systemd/src/basic/hashmap.h
index 41c8adb1..65adc925 100644
--- a/shared/systemd/src/basic/hashmap.h
+++ b/shared/systemd/src/basic/hashmap.h
@@ -76,7 +76,7 @@ typedef struct {
 
 #if ENABLE_DEBUG_HASHMAP
 # define HASHMAP_DEBUG_PARAMS , const char *func, const char *file, int line
-# define HASHMAP_DEBUG_SRC_ARGS   , __func__, __FILE__, __LINE__
+# define HASHMAP_DEBUG_SRC_ARGS   , __func__, PROJECT_FILE, __LINE__
 # define HASHMAP_DEBUG_PASS_ARGS   , func, file, line
 #else
 # define HASHMAP_DEBUG_PARAMS
@@ -147,6 +147,8 @@ static inline int ordered_hashmap_put(OrderedHashmap *h, const void *key, void *
         return hashmap_put(PLAIN_HASHMAP(h), key, value);
 }
 
+int hashmap_put_strdup(Hashmap **h, const char *k, const char *v);
+
 int hashmap_update(Hashmap *h, const void *key, void *value);
 static inline int ordered_hashmap_update(OrderedHashmap *h, const void *key, void *value) {
         return hashmap_update(PLAIN_HASHMAP(h), key, value);
diff --git a/shared/systemd/src/basic/hexdecoct.c b/shared/systemd/src/basic/hexdecoct.c
index c81c09e8..36174ec3 100644
--- a/shared/systemd/src/basic/hexdecoct.c
+++ b/shared/systemd/src/basic/hexdecoct.c
@@ -110,10 +110,12 @@ static int unhex_next(const char **p, size_t *l) {
         return r;
 }
 
-int unhexmem(const char *p, size_t l, void **ret, size_t *ret_len) {
+int unhexmem_full(const char *p, size_t l, bool secure, void **ret, size_t *ret_len) {
         _cleanup_free_ uint8_t *buf = NULL;
+        size_t buf_size;
         const char *x;
         uint8_t *z;
+        int r;
 
         assert(ret);
         assert(ret_len);
@@ -123,7 +125,8 @@ int unhexmem(const char *p, size_t l, void **ret, size_t *ret_len) {
                 l = strlen(p);
 
         /* Note that the calculation of memory size is an upper boundary, as we ignore whitespace while decoding */
-        buf = malloc((l + 1) / 2 + 1);
+        buf_size = (l + 1) / 2 + 1;
+        buf = malloc(buf_size);
         if (!buf)
                 return -ENOMEM;
 
@@ -133,12 +136,16 @@ int unhexmem(const char *p, size_t l, void **ret, size_t *ret_len) {
                 a = unhex_next(&x, &l);
                 if (a == -EPIPE) /* End of string */
                         break;
-                if (a < 0)
-                        return a;
+                if (a < 0) {
+                        r = a;
+                        goto on_failure;
+                }
 
                 b = unhex_next(&x, &l);
-                if (b < 0)
-                        return b;
+                if (b < 0) {
+                        r = b;
+                        goto on_failure;
+                }
 
                 *(z++) = (uint8_t) a << 4 | (uint8_t) b;
         }
@@ -149,6 +156,12 @@ int unhexmem(const char *p, size_t l, void **ret, size_t *ret_len) {
         *ret = TAKE_PTR(buf);
 
         return 0;
+
+on_failure:
+        if (secure)
+                explicit_bzero_safe(buf, buf_size);
+
+        return r;
 }
 
 #if 0 /* NM_IGNORED */
diff --git a/shared/systemd/src/basic/hexdecoct.h b/shared/systemd/src/basic/hexdecoct.h
index fa6013ee..dfdff1e9 100644
--- a/shared/systemd/src/basic/hexdecoct.h
+++ b/shared/systemd/src/basic/hexdecoct.h
@@ -18,7 +18,10 @@ char hexchar(int x) _const_;
 int unhexchar(char c) _const_;
 
 char *hexmem(const void *p, size_t l);
-int unhexmem(const char *p, size_t l, void **mem, size_t *len);
+int unhexmem_full(const char *p, size_t l, bool secure, void **mem, size_t *len);
+static inline int unhexmem(const char *p, size_t l, void **mem, size_t *len) {
+        return unhexmem_full(p, l, false, mem, len);
+}
 
 char base32hexchar(int x) _const_;
 int unbase32hexchar(char c) _const_;
diff --git a/shared/systemd/src/basic/in-addr-util.c b/shared/systemd/src/basic/in-addr-util.c
index 5899f62f..91d687c2 100644
--- a/shared/systemd/src/basic/in-addr-util.c
+++ b/shared/systemd/src/basic/in-addr-util.c
@@ -11,6 +11,7 @@
 #include <stdlib.h>
 
 #include "alloc-util.h"
+#include "errno-util.h"
 #include "in-addr-util.h"
 #include "macro.h"
 #include "parse-util.h"
@@ -93,12 +94,19 @@ int in_addr_is_localhost(int family, const union in_addr_union *u) {
         return -EAFNOSUPPORT;
 }
 
+bool in4_addr_equal(const struct in_addr *a, const struct in_addr *b) {
+        assert(a);
+        assert(b);
+
+        return a->s_addr == b->s_addr;
+}
+
 int in_addr_equal(int family, const union in_addr_union *a, const union in_addr_union *b) {
         assert(a);
         assert(b);
 
         if (family == AF_INET)
-                return a->in.s_addr == b->in.s_addr;
+                return in4_addr_equal(&a->in, &b->in);
 
         if (family == AF_INET6)
                 return
@@ -319,7 +327,7 @@ int in_addr_to_string(int family, const union in_addr_union *u, char **ret) {
 
         errno = 0;
         if (!inet_ntop(family, u, x, l))
-                return errno > 0 ? -errno : -EINVAL;
+                return errno_or_else(EINVAL);
 
         *ret = TAKE_PTR(x);
         return 0;
@@ -350,7 +358,7 @@ int in_addr_prefix_to_string(int family, const union in_addr_union *u, unsigned
 
         errno = 0;
         if (!inet_ntop(family, u, x, l))
-                return errno > 0 ? -errno : -EINVAL;
+                return errno_or_else(EINVAL);
 
         p = x + strlen(x);
         l -= strlen(x);
@@ -390,7 +398,7 @@ int in_addr_ifindex_to_string(int family, const union in_addr_union *u, int ifin
 
         errno = 0;
         if (!inet_ntop(family, u, x, l))
-                return errno > 0 ? -errno : -EINVAL;
+                return errno_or_else(EINVAL);
 
         sprintf(strchr(x, 0), "%%%i", ifindex);
 
@@ -410,7 +418,7 @@ int in_addr_from_string(int family, const char *s, union in_addr_union *ret) {
 
         errno = 0;
         if (inet_pton(family, s, ret ?: &buffer) <= 0)
-                return errno > 0 ? -errno : -EINVAL;
+                return errno_or_else(EINVAL);
 
         return 0;
 }
@@ -747,4 +755,16 @@ static int in_addr_data_compare_func(const struct in_addr_data *x, const struct
 }
 
 DEFINE_HASH_OPS(in_addr_data_hash_ops, struct in_addr_data, in_addr_data_hash_func, in_addr_data_compare_func);
+
+static void in6_addr_hash_func(const struct in6_addr *addr, struct siphash *state) {
+        assert(addr);
+
+        siphash24_compress(addr, sizeof(*addr), state);
+}
+
+static int in6_addr_compare_func(const struct in6_addr *a, const struct in6_addr *b) {
+        return memcmp(a, b, sizeof(*a));
+}
+
+DEFINE_HASH_OPS(in6_addr_hash_ops, struct in6_addr, in6_addr_hash_func, in6_addr_compare_func);
 #endif /* NM_IGNORED */
diff --git a/shared/systemd/src/basic/in-addr-util.h b/shared/systemd/src/basic/in-addr-util.h
index a6a685b9..28afc7d8 100644
--- a/shared/systemd/src/basic/in-addr-util.h
+++ b/shared/systemd/src/basic/in-addr-util.h
@@ -32,6 +32,7 @@ int in_addr_is_localhost(int family, const union in_addr_union *u);
 
 bool in4_addr_is_non_local(const struct in_addr *a);
 
+bool in4_addr_equal(const struct in_addr *a, const struct in_addr *b);
 int in_addr_equal(int family, const union in_addr_union *a, const union in_addr_union *b);
 int in_addr_prefix_intersect(int family, const union in_addr_union *a, unsigned aprefixlen, const union in_addr_union *b, unsigned bprefixlen);
 int in_addr_prefix_next(int family, union in_addr_union *u, unsigned prefixlen);
@@ -72,3 +73,4 @@ static inline size_t FAMILY_ADDRESS_SIZE(int family) {
 #define IN_ADDR_NULL ((union in_addr_union) { .in6 = {} })
 
 extern const struct hash_ops in_addr_data_hash_ops;
+extern const struct hash_ops in6_addr_hash_ops;
diff --git a/shared/systemd/src/basic/io-util.c b/shared/systemd/src/basic/io-util.c
index 3f47eff5..9669c463 100644
--- a/shared/systemd/src/basic/io-util.c
+++ b/shared/systemd/src/basic/io-util.c
@@ -264,9 +264,91 @@ ssize_t sparse_write(int fd, const void *p, size_t sz, size_t run_length) {
 char* set_iovec_string_field(struct iovec *iovec, size_t *n_iovec, const char *field, const char *value) {
         char *x;
 
-        x = strappend(field, value);
+        x = strjoin(field, value);
         if (x)
                 iovec[(*n_iovec)++] = IOVEC_MAKE_STRING(x);
         return x;
 }
+
+char* set_iovec_string_field_free(struct iovec *iovec, size_t *n_iovec, const char *field, char *value) {
+        char *x;
+
+        x = set_iovec_string_field(iovec, n_iovec, field, value);
+        free(value);
+        return x;
+}
+
+struct iovec_wrapper *iovw_new(void) {
+        return malloc0(sizeof(struct iovec_wrapper));
+}
+
+void iovw_free_contents(struct iovec_wrapper *iovw, bool free_vectors) {
+        if (free_vectors)
+                for (size_t i = 0; i < iovw->count; i++)
+                        free(iovw->iovec[i].iov_base);
+
+        iovw->iovec = mfree(iovw->iovec);
+        iovw->count = 0;
+        iovw->size_bytes = 0;
+}
+
+struct iovec_wrapper *iovw_free_free(struct iovec_wrapper *iovw) {
+        iovw_free_contents(iovw, true);
+
+        return mfree(iovw);
+}
+
+struct iovec_wrapper *iovw_free(struct iovec_wrapper *iovw) {
+        iovw_free_contents(iovw, false);
+
+        return mfree(iovw);
+}
+
+int iovw_put(struct iovec_wrapper *iovw, void *data, size_t len) {
+        if (iovw->count >= IOV_MAX)
+                return -E2BIG;
+
+        if (!GREEDY_REALLOC(iovw->iovec, iovw->size_bytes, iovw->count + 1))
+                return log_oom();
+
+        iovw->iovec[iovw->count++] = IOVEC_MAKE(data, len);
+        return 0;
+}
+
+int iovw_put_string_field(struct iovec_wrapper *iovw, const char *field, const char *value) {
+        _cleanup_free_ char *x = NULL;
+        int r;
+
+        x = strjoin(field, value);
+        if (!x)
+                return log_oom();
+
+        r = iovw_put(iovw, x, strlen(x));
+        if (r >= 0)
+                TAKE_PTR(x);
+
+        return r;
+}
+
+int iovw_put_string_field_free(struct iovec_wrapper *iovw, const char *field, char *value) {
+        _cleanup_free_ _unused_ char *free_ptr = value;
+
+        return iovw_put_string_field(iovw, field, value);
+}
+
+void iovw_rebase(struct iovec_wrapper *iovw, char *old, char *new) {
+        size_t i;
+
+        for (i = 0; i < iovw->count; i++)
+                iovw->iovec[i].iov_base = (char *)iovw->iovec[i].iov_base - old + new;
+}
+
+size_t iovw_size(struct iovec_wrapper *iovw) {
+        size_t n = 0, i;
+
+        for (i = 0; i < iovw->count; i++)
+                n += iovw->iovec[i].iov_len;
+
+        return n;
+}
 #endif /* NM_IGNORED */
diff --git a/shared/systemd/src/basic/io-util.h b/shared/systemd/src/basic/io-util.h
index 792a64ad..719e19e8 100644
--- a/shared/systemd/src/basic/io-util.h
+++ b/shared/systemd/src/basic/io-util.h
@@ -73,3 +73,20 @@ static inline bool FILE_SIZE_VALID_OR_INFINITY(uint64_t l) {
 #define IOVEC_MAKE_STRING(string) (struct iovec) IOVEC_INIT_STRING(string)
 
 char* set_iovec_string_field(struct iovec *iovec, size_t *n_iovec, const char *field, const char *value);
+char* set_iovec_string_field_free(struct iovec *iovec, size_t *n_iovec, const char *field, char *value);
+
+struct iovec_wrapper {
+        struct iovec *iovec;
+        size_t count;
+        size_t size_bytes;
+};
+
+struct iovec_wrapper *iovw_new(void);
+struct iovec_wrapper *iovw_free(struct iovec_wrapper *iovw);
+struct iovec_wrapper *iovw_free_free(struct iovec_wrapper *iovw);
+void iovw_free_contents(struct iovec_wrapper *iovw, bool free_vectors);
+int iovw_put(struct iovec_wrapper *iovw, void *data, size_t len);
+int iovw_put_string_field(struct iovec_wrapper *iovw, const char *field, const char *value);
+int iovw_put_string_field_free(struct iovec_wrapper *iovw, const char *field, char *value);
+void iovw_rebase(struct iovec_wrapper *iovw, char *old, char *new);
+size_t iovw_size(struct iovec_wrapper *iovw);
diff --git a/shared/systemd/src/basic/log.h b/shared/systemd/src/basic/log.h
index 98c5f4d7..b81c93a5 100644
--- a/shared/systemd/src/basic/log.h
+++ b/shared/systemd/src/basic/log.h
@@ -75,6 +75,12 @@ int log_get_max_level_realm(LogRealm realm) _pure_;
  * for the application itself.
  */
 
+#if 0 /* NM_IGNORED */
+assert_cc(STRLEN(__FILE__) > STRLEN(RELATIVE_SOURCE_PATH) + 1);
+#define PROJECT_FILE (__FILE__ + STRLEN(RELATIVE_SOURCE_PATH) + 1)
+#endif /* NM_IGNORED */
+#define PROJECT_FILE __FILE__
+
 int log_open(void);
 void log_close(void);
 void log_forget_fds(void);
@@ -201,7 +207,6 @@ _noreturn_ void log_assert_failed_realm(
 #define log_assert_failed(text, ...) \
         log_assert_failed_realm(LOG_REALM, (text), __VA_ARGS__)
 
-
 _noreturn_ void log_assert_failed_unreachable_realm(
                 LogRealm realm,
                 const char *text,
@@ -221,7 +226,7 @@ void log_assert_failed_return_realm(
         log_assert_failed_return_realm(LOG_REALM, (text), __VA_ARGS__)
 
 #define log_dispatch(level, error, buffer)                              \
-        log_dispatch_internal(level, error, __FILE__, __LINE__, __func__, NULL, NULL, NULL, NULL, buffer)
+        log_dispatch_internal(level, error, PROJECT_FILE, __LINE__, __func__, NULL, NULL, NULL, NULL, buffer)
 #endif /* NM_IGNORED */
 
 /* Logging with level */
@@ -230,7 +235,7 @@ void log_assert_failed_return_realm(
                 int _level = (level), _e = (error), _realm = (realm);   \
                 (log_get_max_level_realm(_realm) >= LOG_PRI(_level))    \
                         ? log_internal_realm(LOG_REALM_PLUS_LEVEL(_realm, _level), _e, \
-                                             __FILE__, __LINE__, __func__, __VA_ARGS__) \
+                                             PROJECT_FILE, __LINE__, __func__, __VA_ARGS__) \
                         : -ERRNO_VALUE(_e);                             \
         })
 
@@ -266,20 +271,20 @@ int log_emergency_level(void);
 /* Structured logging */
 #define log_struct_errno(level, error, ...) \
         log_struct_internal(LOG_REALM_PLUS_LEVEL(LOG_REALM, level), \
-                            error, __FILE__, __LINE__, __func__, __VA_ARGS__, NULL)
+                            error, PROJECT_FILE, __LINE__, __func__, __VA_ARGS__, NULL)
 #define log_struct(level, ...) log_struct_errno(level, 0, __VA_ARGS__)
 
 #define log_struct_iovec_errno(level, error, iovec, n_iovec)            \
         log_struct_iovec_internal(LOG_REALM_PLUS_LEVEL(LOG_REALM, level), \
-                                  error, __FILE__, __LINE__, __func__, iovec, n_iovec)
+                                  error, PROJECT_FILE, __LINE__, __func__, iovec, n_iovec)
 #define log_struct_iovec(level, iovec, n_iovec) log_struct_iovec_errno(level, 0, iovec, n_iovec)
 
 /* This modifies the buffer passed! */
 #define log_dump(level, buffer) \
         log_dump_internal(LOG_REALM_PLUS_LEVEL(LOG_REALM, level), \
-                          0, __FILE__, __LINE__, __func__, buffer)
+                          0, PROJECT_FILE, __LINE__, __func__, buffer)
 
-#define log_oom() log_oom_internal(LOG_REALM, __FILE__, __LINE__, __func__)
+#define log_oom() log_oom_internal(LOG_REALM, PROJECT_FILE, __LINE__, __func__)
 
 bool log_on_console(void) _pure_;
 
@@ -307,6 +312,7 @@ void log_set_prohibit_ipc(bool b);
 
 int log_dup_console(void);
 
+#if 0 /* NM_IGNORED */
 int log_syntax_internal(
                 const char *unit,
                 int level,
@@ -317,6 +323,9 @@ int log_syntax_internal(
                 int line,
                 const char *func,
                 const char *format, ...) _printf_(9, 10);
+#endif /* NM_IGNORED */
+#define log_syntax_internal(unit, level, config_file, config_line, error, file, line, func, format, ...) \
+    log_internal_realm((level), (error), file, (line), (func), "syntax[%s]: "format, (config_file), __VA_ARGS__) \
 
 int log_syntax_invalid_utf8_internal(
                 const char *unit,
@@ -332,7 +341,7 @@ int log_syntax_invalid_utf8_internal(
         ({                                                              \
                 int _level = (level), _e = (error);                     \
                 (log_get_max_level() >= LOG_PRI(_level))                \
-                        ? log_internal_realm(_level, _e, __FILE__, __LINE__, __func__, __VA_ARGS__) \
+                        ? log_syntax_internal(unit, _level, config_file, config_line, _e, PROJECT_FILE, __LINE__, __func__, __VA_ARGS__) \
                         : -ERRNO_VALUE(_e);                             \
         })
 
@@ -340,7 +349,7 @@ int log_syntax_invalid_utf8_internal(
         ({                                                              \
                 int _level = (level);                                   \
                 (log_get_max_level() >= LOG_PRI(_level))                \
-                        ? log_syntax_invalid_utf8_internal(unit, _level, config_file, config_line, __FILE__, __LINE__, __func__, rvalue) \
+                        ? log_syntax_invalid_utf8_internal(unit, _level, config_file, config_line, PROJECT_FILE, __LINE__, __func__, rvalue) \
                         : -EINVAL;                                      \
         })
 
diff --git a/shared/systemd/src/basic/macro.h b/shared/systemd/src/basic/macro.h
index 6a1fdab5..43c51326 100644
--- a/shared/systemd/src/basic/macro.h
+++ b/shared/systemd/src/basic/macro.h
@@ -106,6 +106,15 @@
         _Pragma("GCC diagnostic push");                                 \
         _Pragma("GCC diagnostic ignored \"-Wincompatible-pointer-types\"")
 
+#if HAVE_WSTRINGOP_TRUNCATION
+#  define DISABLE_WARNING_STRINGOP_TRUNCATION                           \
+        _Pragma("GCC diagnostic push");                                 \
+        _Pragma("GCC diagnostic ignored \"-Wstringop-truncation\"")
+#else
+#  define DISABLE_WARNING_STRINGOP_TRUNCATION                           \
+        _Pragma("GCC diagnostic push")
+#endif
+
 #define REENABLE_WARNING                                                \
         _Pragma("GCC diagnostic pop")
 #else
@@ -324,12 +333,12 @@ static inline int __coverity_check__(int condition) {
 #define assert_message_se(expr, message)                                \
         do {                                                            \
                 if (_unlikely_(!(expr)))                                \
-                        log_assert_failed(message, __FILE__, __LINE__, __PRETTY_FUNCTION__); \
+                        log_assert_failed(message, PROJECT_FILE, __LINE__, __PRETTY_FUNCTION__); \
         } while (false)
 
 #define assert_log(expr, message) ((_likely_(expr))                     \
         ? (true)                                                        \
-        : (log_assert_failed_return(message, __FILE__, __LINE__, __PRETTY_FUNCTION__), false))
+        : (log_assert_failed_return(message, PROJECT_FILE, __LINE__, __PRETTY_FUNCTION__), false))
 
 #endif  /* __COVERITY__ */
 
@@ -344,18 +353,16 @@ static inline int __coverity_check__(int condition) {
 #endif
 
 #define assert_not_reached(t)                                           \
-        do {                                                            \
-                log_assert_failed_unreachable(t, __FILE__, __LINE__, __PRETTY_FUNCTION__); \
-        } while (false)
+        log_assert_failed_unreachable(t, PROJECT_FILE, __LINE__, __PRETTY_FUNCTION__)
 
 #if defined(static_assert)
 #define assert_cc(expr)                                                 \
-        static_assert(expr, #expr);
+        static_assert(expr, #expr)
 #else
 #define assert_cc(expr)                                                 \
         struct CONCATENATE(_assert_struct_, __COUNTER__) {              \
                 char x[(expr) ? 0 : -1];                                \
-        };
+        }
 #endif
 
 #define assert_return(expr, r)                                          \
@@ -464,7 +471,8 @@ static inline int __coverity_check__(int condition) {
                  * type for the array, in the hope that checkers such as ubsan don't complain that the initializers for \
                  * the array are not representable by the base type. Ideally we'd use typeof(x) as base type, but that  \
                  * doesn't work, as we want to use this on bitfields and gcc refuses typeof() on bitfields.) */         \
-                assert_cc((sizeof((long double[]){__VA_ARGS__})/sizeof(long double)) <= 20); \
+                static const long double __assert_in_set[] _unused_ = { __VA_ARGS__ }; \
+                assert_cc(ELEMENTSOF(__assert_in_set) <= 20); \
                 switch(x) {                     \
                 FOR_EACH_MAKE_CASE(__VA_ARGS__) \
                         _found = true;          \
diff --git a/shared/systemd/src/basic/memory-util.h b/shared/systemd/src/basic/memory-util.h
index 915c24a5..9cb8ac3c 100644
--- a/shared/systemd/src/basic/memory-util.h
+++ b/shared/systemd/src/basic/memory-util.h
@@ -2,6 +2,7 @@
 #pragma once
 
 #include <inttypes.h>
+#include <malloc.h>
 #include <stdbool.h>
 #include <string.h>
 #include <sys/types.h>
@@ -37,8 +38,8 @@ static inline int memcmp_nn(const void *s1, size_t n1, const void *s2, size_t n2
 #define memzero(x,l)                                            \
         ({                                                      \
                 size_t _l_ = (l);                               \
-                void *_x_ = (x);                                \
-                _l_ == 0 ? _x_ : memset(_x_, 0, _l_);           \
+                if (_l_ > 0)                                    \
+                        memset(x, 0, _l_);                      \
         })
 
 #define zero(x) (memzero(&(x), sizeof(x)))
@@ -78,6 +79,16 @@ static inline void* explicit_bzero_safe(void *p, size_t l) {
 void *explicit_bzero_safe(void *p, size_t l);
 #endif
 
+static inline void erase_and_freep(void *p) {
+        void *ptr = *(void**) p;
+
+        if (ptr) {
+                size_t l = malloc_usable_size(ptr);
+                explicit_bzero_safe(ptr, l);
+                free(ptr);
+        }
+}
+
 /* Use with _cleanup_ to erase a single 'char' when leaving scope */
 static inline void erase_char(char *p) {
         explicit_bzero_safe(p, sizeof(char));
diff --git a/shared/systemd/src/basic/parse-util.c b/shared/systemd/src/basic/parse-util.c
index 02ef426f..76ef6e09 100644
--- a/shared/systemd/src/basic/parse-util.c
+++ b/shared/systemd/src/basic/parse-util.c
@@ -6,6 +6,7 @@
 #include <inttypes.h>
 #include <linux/oom.h>
 #include <locale.h>
+#include <net/if.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -86,6 +87,9 @@ int parse_mode(const char *s, mode_t *ret) {
 int parse_ifindex(const char *s, int *ret) {
         int ifi, r;
 
+        assert(s);
+        assert(ret);
+
         r = safe_atoi(s, &ifi);
         if (r < 0)
                 return r;
@@ -96,6 +100,24 @@ int parse_ifindex(const char *s, int *ret) {
         return 0;
 }
 
+int parse_ifindex_or_ifname(const char *s, int *ret) {
+        int r;
+
+        assert(s);
+        assert(ret);
+
+        r = parse_ifindex(s, ret);
+        if (r >= 0)
+                return r;
+
+        r = (int) if_nametoindex(s);
+        if (r <= 0)
+                return -errno;
+
+        *ret = r;
+        return 0;
+}
+
 int parse_mtu(int family, const char *s, uint32_t *ret) {
         uint64_t u;
         size_t m;
@@ -342,47 +364,6 @@ int parse_syscall_and_errno(const char *in, char **name, int *error) {
 
         return 0;
 }
-
-char *format_bytes(char *buf, size_t l, uint64_t t) {
-        unsigned i;
-
-        /* This only does IEC units so far */
-
-        static const struct {
-                const char *suffix;
-                uint64_t factor;
-        } table[] = {
-                { "E", UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024) },
-                { "P", UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024) },
-                { "T", UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024) },
-                { "G", UINT64_C(1024)*UINT64_C(1024)*UINT64_C(1024) },
-                { "M", UINT64_C(1024)*UINT64_C(1024) },
-                { "K", UINT64_C(1024) },
-        };
-
-        if (t == (uint64_t) -1)
-                return NULL;
-
-        for (i = 0; i < ELEMENTSOF(table); i++) {
-
-                if (t >= table[i].factor) {
-                        snprintf(buf, l,
-                                 "%" PRIu64 ".%" PRIu64 "%s",
-                                 t / table[i].factor,
-                                 ((t*UINT64_C(10)) / table[i].factor) % UINT64_C(10),
-                                 table[i].suffix);
-
-                        goto finish;
-                }
-        }
-
-        snprintf(buf, l, "%" PRIu64 "B", t);
-
-finish:
-        buf[l-1] = 0;
-        return buf;
-
-}
 #endif /* NM_IGNORED */
 
 int safe_atou_full(const char *s, unsigned base, unsigned *ret_u) {
diff --git a/shared/systemd/src/basic/parse-util.h b/shared/systemd/src/basic/parse-util.h
index e47641b4..3a70b792 100644
--- a/shared/systemd/src/basic/parse-util.h
+++ b/shared/systemd/src/basic/parse-util.h
@@ -9,13 +9,12 @@
 
 #include "macro.h"
 
-#define MODE_INVALID ((mode_t) -1)
-
 int parse_boolean(const char *v) _pure_;
 int parse_dev(const char *s, dev_t *ret);
 int parse_pid(const char *s, pid_t* ret_pid);
 int parse_mode(const char *s, mode_t *ret);
 int parse_ifindex(const char *s, int *ret);
+int parse_ifindex_or_ifname(const char *s, int *ret);
 int parse_mtu(int family, const char *s, uint32_t *ret);
 
 int parse_size(const char *t, uint64_t base, uint64_t *size);
@@ -23,9 +22,6 @@ int parse_range(const char *t, unsigned *lower, unsigned *upper);
 int parse_errno(const char *t);
 int parse_syscall_and_errno(const char *in, char **name, int *error);
 
-#define FORMAT_BYTES_MAX 8
-char *format_bytes(char *buf, size_t l, uint64_t t);
-
 int safe_atou_full(const char *s, unsigned base, unsigned *ret_u);
 
 static inline int safe_atou(const char *s, unsigned *ret_u) {
diff --git a/shared/systemd/src/basic/path-util.c b/shared/systemd/src/basic/path-util.c
index 29955a36..e39656bc 100644
--- a/shared/systemd/src/basic/path-util.c
+++ b/shared/systemd/src/basic/path-util.c
@@ -71,10 +71,7 @@ char *path_make_absolute(const char *p, const char *prefix) {
         if (path_is_absolute(p) || isempty(prefix))
                 return strdup(p);
 
-        if (endswith(prefix, "/"))
-                return strjoin(prefix, p);
-        else
-                return strjoin(prefix, "/", p);
+        return path_join(prefix, p);
 }
 
 int safe_getcwd(char **ret) {
@@ -213,6 +210,18 @@ int path_make_relative(const char *from_dir, const char *to_path, char **_r) {
         return 0;
 }
 
+char* path_startswith_strv(const char *p, char **set) {
+        char **s, *t;
+
+        STRV_FOREACH(s, set) {
+                t = path_startswith(p, *s);
+                if (t)
+                        return t;
+        }
+
+        return NULL;
+}
+
 int path_strv_make_absolute_cwd(char **l) {
         char **s;
         int r;
@@ -259,7 +268,7 @@ char **path_strv_resolve(char **l, const char *root) {
 
                 if (root) {
                         orig = *s;
-                        t = prefix_root(root, orig);
+                        t = path_join(root, orig);
                         if (!t) {
                                 enomem = true;
                                 continue;
@@ -389,6 +398,54 @@ char *path_simplify(char *path, bool kill_dots) {
         return path;
 }
 
+#if 0 /* NM_IGNORED */
+int path_simplify_and_warn(
+                char *path,
+                unsigned flag,
+                const char *unit,
+                const char *filename,
+                unsigned line,
+                const char *lvalue) {
+
+        bool fatal = flag & PATH_CHECK_FATAL;
+
+        assert(!FLAGS_SET(flag, PATH_CHECK_ABSOLUTE | PATH_CHECK_RELATIVE));
+
+        if (!utf8_is_valid(path))
+                return log_syntax_invalid_utf8(unit, LOG_ERR, filename, line, path);
+
+        if (flag & (PATH_CHECK_ABSOLUTE | PATH_CHECK_RELATIVE)) {
+                bool absolute;
+
+                absolute = path_is_absolute(path);
+
+                if (!absolute && (flag & PATH_CHECK_ABSOLUTE))
+                        return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
+                                          "%s= path is not absolute%s: %s",
+                                          lvalue, fatal ? "" : ", ignoring", path);
+
+                if (absolute && (flag & PATH_CHECK_RELATIVE))
+                        return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
+                                          "%s= path is absolute%s: %s",
+                                          lvalue, fatal ? "" : ", ignoring", path);
+        }
+
+        path_simplify(path, true);
+
+        if (!path_is_valid(path))
+                return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
+                                  "%s= path has invalid length (%zu bytes)%s.",
+                                  lvalue, strlen(path), fatal ? "" : ", ignoring");
+
+        if (!path_is_normalized(path))
+                return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
+                                  "%s= path is not normalized%s: %s",
+                                  lvalue, fatal ? "" : ", ignoring", path);
+
+        return 0;
+}
+#endif /* NM_IGNORED */
+
 char* path_startswith(const char *path, const char *prefix) {
         assert(path);
         assert(prefix);
@@ -586,7 +643,7 @@ int find_binary(const char *name, char **ret) {
                 if (!path_is_absolute(element))
                         continue;
 
-                j = strjoin(element, "/", name);
+                j = path_join(element, name);
                 if (!j)
                         return -ENOMEM;
 
@@ -691,40 +748,6 @@ int mkfs_exists(const char *fstype) {
         return binary_is_good(mkfs);
 }
 
-char *prefix_root(const char *root, const char *path) {
-        char *n, *p;
-        size_t l;
-
-        /* If root is passed, prefixes path with it. Otherwise returns
-         * it as is. */
-
-        assert(path);
-
-        /* First, drop duplicate prefixing slashes from the path */
-        while (path[0] == '/' && path[1] == '/')
-                path++;
-
-        if (empty_or_root(root))
-                return strdup(path);
-
-        l = strlen(root) + 1 + strlen(path) + 1;
-
-        n = new(char, l);
-        if (!n)
-                return NULL;
-
-        p = stpcpy(n, root);
-
-        while (p > n && p[-1] == '/')
-                p--;
-
-        if (path[0] != '/')
-                *(p++) = '/';
-
-        strcpy(p, path);
-        return n;
-}
-
 int parse_path_argument_and_warn(const char *path, bool suppress_root, char **arg) {
         char *p;
         int r;
@@ -1034,7 +1057,7 @@ int systemd_installation_has_version(const char *root, unsigned minimal_version)
                 _cleanup_free_ char *path = NULL;
                 char *c, **name;
 
-                path = prefix_root(root, pattern);
+                path = path_join(root, pattern);
                 if (!path)
                         return -ENOMEM;
 
@@ -1104,50 +1127,4 @@ bool empty_or_root(const char *root) {
 
         return root[strspn(root, "/")] == 0;
 }
-
-int path_simplify_and_warn(
-                char *path,
-                unsigned flag,
-                const char *unit,
-                const char *filename,
-                unsigned line,
-                const char *lvalue) {
-
-        bool fatal = flag & PATH_CHECK_FATAL;
-
-        assert(!FLAGS_SET(flag, PATH_CHECK_ABSOLUTE | PATH_CHECK_RELATIVE));
-
-        if (!utf8_is_valid(path))
-                return log_syntax_invalid_utf8(unit, LOG_ERR, filename, line, path);
-
-        if (flag & (PATH_CHECK_ABSOLUTE | PATH_CHECK_RELATIVE)) {
-                bool absolute;
-
-                absolute = path_is_absolute(path);
-
-                if (!absolute && (flag & PATH_CHECK_ABSOLUTE))
-                        return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
-                                          "%s= path is not absolute%s: %s",
-                                          lvalue, fatal ? "" : ", ignoring", path);
-
-                if (absolute && (flag & PATH_CHECK_RELATIVE))
-                        return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
-                                          "%s= path is absolute%s: %s",
-                                          lvalue, fatal ? "" : ", ignoring", path);
-        }
-
-        path_simplify(path, true);
-
-        if (!path_is_valid(path))
-                return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
-                                  "%s= path has invalid length (%zu bytes)%s.",
-                                  lvalue, strlen(path), fatal ? "" : ", ignoring");
-
-        if (!path_is_normalized(path))
-                return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL),
-                                  "%s= path is not normalized%s: %s",
-                                  lvalue, fatal ? "" : ", ignoring", path);
-
-        return 0;
-}
 #endif /* NM_IGNORED */
diff --git a/shared/systemd/src/basic/path-util.h b/shared/systemd/src/basic/path-util.h
index 5204adaa..cd6216bb 100644
--- a/shared/systemd/src/basic/path-util.h
+++ b/shared/systemd/src/basic/path-util.h
@@ -56,6 +56,14 @@ char* path_join_internal(const char *first, ...);
 
 char* path_simplify(char *path, bool kill_dots);
 
+enum {
+        PATH_CHECK_FATAL    = 1 << 0,  /* If not set, then error message is appended with 'ignoring'. */
+        PATH_CHECK_ABSOLUTE = 1 << 1,
+        PATH_CHECK_RELATIVE = 1 << 2,
+};
+
+int path_simplify_and_warn(char *path, unsigned flag, const char *unit, const char *filename, unsigned line, const char *lvalue);
+
 static inline bool path_equal_ptr(const char *a, const char *b) {
         return !!a == !!b && (!a || path_equal(a, b));
 }
@@ -73,17 +81,8 @@ static inline bool path_equal_ptr(const char *a, const char *b) {
                 _found;                                         \
         })
 
-#define PATH_STARTSWITH_SET(p, ...)                             \
-        ({                                                      \
-                const char *_p = (p);                           \
-                char *_found = NULL, **_i;                      \
-                STRV_FOREACH(_i, STRV_MAKE(__VA_ARGS__)) {      \
-                        _found = path_startswith(_p, *_i);      \
-                        if (_found)                             \
-                                break;                          \
-                }                                               \
-                _found;                                         \
-        })
+char* path_startswith_strv(const char *p, char **set);
+#define PATH_STARTSWITH_SET(p, ...) path_startswith_strv(p, STRV_MAKE(__VA_ARGS__))
 
 int path_strv_make_absolute_cwd(char **l);
 char** path_strv_resolve(char **l, const char *root);
@@ -118,10 +117,8 @@ int mkfs_exists(const char *fstype);
              _slash && ((*_slash = 0), true);                           \
              _slash = strrchr((prefix), '/'))
 
-char *prefix_root(const char *root, const char *path);
-
-/* Similar to prefix_root(), but returns an alloca() buffer, or
- * possibly a const pointer into the path parameter */
+/* Similar to path_join(), but only works for two components, and only the first one may be NULL and returns
+ * an alloca() buffer, or possibly a const pointer into the path parameter. */
 #define prefix_roota(root, path)                                        \
         ({                                                              \
                 const char* _path = (path), *_root = (root), *_ret;     \
@@ -129,7 +126,7 @@ char *prefix_root(const char *root, const char *path);
                 size_t _l;                                              \
                 while (_path[0] == '/' && _path[1] == '/')              \
                         _path ++;                                       \
-                if (empty_or_root(_root))                               \
+                if (isempty(_root))                                     \
                         _ret = _path;                                   \
                 else {                                                  \
                         _l = strlen(_root) + 1 + strlen(_path) + 1;     \
@@ -182,11 +179,3 @@ bool empty_or_root(const char *root);
 static inline const char *empty_to_root(const char *path) {
         return isempty(path) ? "/" : path;
 }
-
-enum {
-        PATH_CHECK_FATAL    = 1 << 0,  /* If not set, then error message is appended with 'ignoring'. */
-        PATH_CHECK_ABSOLUTE = 1 << 1,
-        PATH_CHECK_RELATIVE = 1 << 2,
-};
-
-int path_simplify_and_warn(char *path, unsigned flag, const char *unit, const char *filename, unsigned line, const char *lvalue);
diff --git a/shared/systemd/src/basic/process-util.c b/shared/systemd/src/basic/process-util.c
index 7431be3e..317815fe 100644
--- a/shared/systemd/src/basic/process-util.c
+++ b/shared/systemd/src/basic/process-util.c
@@ -6,11 +6,9 @@
 #include <errno.h>
 #include <limits.h>
 #include <linux/oom.h>
-#include <sched.h>
 #include <signal.h>
 #include <stdbool.h>
 #include <stdio.h>
-#include <stdio_ext.h>
 #include <stdlib.h>
 #include <string.h>
 #include <sys/mman.h>
@@ -30,10 +28,12 @@
 #include "alloc-util.h"
 #include "architecture.h"
 #include "escape.h"
+#include "env-util.h"
 #include "fd-util.h"
 #include "fileio.h"
 #include "fs-util.h"
 #include "ioprio.h"
+#include "locale-util.h"
 #include "log.h"
 #include "macro.h"
 #include "memory-util.h"
@@ -48,9 +48,16 @@
 #include "string-util.h"
 #include "terminal-util.h"
 #include "user-util.h"
+#include "utf8.h"
 
 #if 0 /* NM_IGNORED */
-int get_process_state(pid_t pid) {
+
+/* The kernel limits userspace processes to TASK_COMM_LEN (16 bytes), but allows higher values for its own
+ * workers, e.g. "kworker/u9:3-kcryptd/253:0". Let's pick a fixed smallish limit that will work for the kernel.
+ */
+#define COMM_MAX_LEN 128
+
+static int get_process_state(pid_t pid) {
         const char *p;
         char state;
         int r;
@@ -86,7 +93,7 @@ int get_process_comm(pid_t pid, char **ret) {
         assert(ret);
         assert(pid >= 0);
 
-        escaped = new(char, TASK_COMM_LEN);
+        escaped = new(char, COMM_MAX_LEN);
         if (!escaped)
                 return -ENOMEM;
 
@@ -99,167 +106,92 @@ int get_process_comm(pid_t pid, char **ret) {
                 return r;
 
         /* Escape unprintable characters, just in case, but don't grow the string beyond the underlying size */
-        cellescape(escaped, TASK_COMM_LEN, comm);
+        cellescape(escaped, COMM_MAX_LEN, comm);
 
         *ret = TAKE_PTR(escaped);
         return 0;
 }
 
-int get_process_cmdline(pid_t pid, size_t max_length, bool comm_fallback, char **line) {
+int get_process_cmdline(pid_t pid, size_t max_columns, ProcessCmdlineFlags flags, char **line) {
         _cleanup_fclose_ FILE *f = NULL;
-        bool space = false;
-        char *k;
-        _cleanup_free_ char *ans = NULL;
+        _cleanup_free_ char *t = NULL, *ans = NULL;
         const char *p;
-        int c;
+        int r;
+        size_t k;
+
+        /* This is supposed to be a safety guard against runaway command lines. */
+        size_t max_length = sc_arg_max();
 
         assert(line);
         assert(pid >= 0);
 
-        /* Retrieves a process' command line. Replaces unprintable characters while doing so by whitespace (coalescing
-         * multiple sequential ones into one). If max_length is != 0 will return a string of the specified size at most
-         * (the trailing NUL byte does count towards the length here!), abbreviated with a "..." ellipsis. If
-         * comm_fallback is true and the process has no command line set (the case for kernel threads), or has a
-         * command line that resolves to the empty string will return the "comm" name of the process instead.
+        /* Retrieves a process' command line. Replaces non-utf8 bytes by replacement character (�). If
+         * max_columns is != -1 will return a string of the specified console width at most, abbreviated with
+         * an ellipsis. If PROCESS_CMDLINE_COMM_FALLBACK is specified in flags and the process has no command
+         * line set (the case for kernel threads), or has a command line that resolves to the empty string
+         * will return the "comm" name of the process instead. This will use at most _SC_ARG_MAX bytes of
+         * input data.
          *
          * Returns -ESRCH if the process doesn't exist, and -ENOENT if the process has no command line (and
          * comm_fallback is false). Returns 0 and sets *line otherwise. */
 
         p = procfs_file_alloca(pid, "cmdline");
+        r = fopen_unlocked(p, "re", &f);
+        if (r == -ENOENT)
+                return -ESRCH;
+        if (r < 0)
+                return r;
 
-        f = fopen(p, "re");
-        if (!f) {
-                if (errno == ENOENT)
-                        return -ESRCH;
-                return -errno;
-        }
-
-        (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
-
-        if (max_length == 0) {
-                /* This is supposed to be a safety guard against runaway command lines. */
-                long l = sysconf(_SC_ARG_MAX);
-                assert(l > 0);
-                max_length = l;
-        }
-
-        if (max_length == 1) {
+        /* We assume that each four-byte character uses one or two columns. If we ever check for combining
+         * characters, this assumption will need to be adjusted. */
+        if ((size_t) 4 * max_columns + 1 < max_columns)
+                max_length = MIN(max_length, (size_t) 4 * max_columns + 1);
 
-                /* If there's only room for one byte, return the empty string */
-                ans = new0(char, 1);
-                if (!ans)
-                        return -ENOMEM;
+        t = new(char, max_length);
+        if (!t)
+                return -ENOMEM;
 
-                *line = TAKE_PTR(ans);
-                return 0;
+        k = fread(t, 1, max_length, f);
+        if (k > 0) {
+                /* Arguments are separated by NULs. Let's replace those with spaces. */
+                for (size_t i = 0; i < k - 1; i++)
+                        if (t[i] == '\0')
+                                t[i] = ' ';
 
+                t[k] = '\0'; /* Normally, t[k] is already NUL, so this is just a guard in case of short read */
         } else {
-                bool dotdotdot = false;
-                size_t left;
-
-                ans = new(char, max_length);
-                if (!ans)
-                        return -ENOMEM;
-
-                k = ans;
-                left = max_length;
-                while ((c = getc(f)) != EOF) {
-
-                        if (isprint(c)) {
-
-                                if (space) {
-                                        if (left <= 2) {
-                                                dotdotdot = true;
-                                                break;
-                                        }
-
-                                        *(k++) = ' ';
-                                        left--;
-                                        space = false;
-                                }
-
-                                if (left <= 1) {
-                                        dotdotdot = true;
-                                        break;
-                                }
-
-                                *(k++) = (char) c;
-                                left--;
-                        } else if (k > ans)
-                                space = true;
-                }
-
-                if (dotdotdot) {
-                        if (max_length <= 4) {
-                                k = ans;
-                                left = max_length;
-                        } else {
-                                k = ans + max_length - 4;
-                                left = 4;
-
-                                /* Eat up final spaces */
-                                while (k > ans && isspace(k[-1])) {
-                                        k--;
-                                        left++;
-                                }
-                        }
-
-                        strncpy(k, "...", left-1);
-                        k[left-1] = 0;
-                } else
-                        *k = 0;
-        }
-
-        /* Kernel threads have no argv[] */
-        if (isempty(ans)) {
-                _cleanup_free_ char *t = NULL;
-                int h;
-
-                ans = mfree(ans);
+                /* We only treat getting nothing as an error. We *could* also get an error after reading some
+                 * data, but we ignore that case, as such an error is rather unlikely and we prefer to get
+                 * some data rather than none. */
+                if (ferror(f))
+                        return -errno;
 
-                if (!comm_fallback)
+                if (!(flags & PROCESS_CMDLINE_COMM_FALLBACK))
                         return -ENOENT;
 
-                h = get_process_comm(pid, &t);
-                if (h < 0)
-                        return h;
+                /* Kernel threads have no argv[] */
+                _cleanup_free_ char *t2 = NULL;
 
-                size_t l = strlen(t);
-
-                if (l + 3 <= max_length) {
-                        ans = strjoin("[", t, "]");
-                        if (!ans)
-                                return -ENOMEM;
+                r = get_process_comm(pid, &t2);
+                if (r < 0)
+                        return r;
 
-                } else if (max_length <= 6) {
-                        ans = new(char, max_length);
-                        if (!ans)
-                                return -ENOMEM;
+                mfree(t);
+                t = strjoin("[", t2, "]");
+                if (!t)
+                        return -ENOMEM;
+        }
 
-                        memcpy(ans, "[...]", max_length-1);
-                        ans[max_length-1] = 0;
-                } else {
-                        t[max_length - 6] = 0;
+        delete_trailing_chars(t, WHITESPACE);
 
-                        /* Chop off final spaces */
-                        delete_trailing_chars(t, WHITESPACE);
+        bool eight_bit = (flags & PROCESS_CMDLINE_USE_LOCALE) && !is_locale_utf8();
 
-                        ans = strjoin("[", t, "...]");
-                        if (!ans)
-                                return -ENOMEM;
-                }
-
-                *line = TAKE_PTR(ans);
-                return 0;
-        }
-
-        k = realloc(ans, strlen(ans) + 1);
-        if (!k)
+        ans = escape_non_printable_full(t, max_columns, eight_bit);
+        if (!ans)
                 return -ENOMEM;
 
-        ans = NULL;
-        *line = k;
-
+        (void) str_realloc(&ans);
+        *line = TAKE_PTR(ans);
         return 0;
 }
 
@@ -291,7 +223,7 @@ int rename_process(const char name[]) {
          * can use PR_SET_NAME, which sets the thread name for the calling thread. */
         if (prctl(PR_SET_NAME, name) < 0)
                 log_debug_errno(errno, "PR_SET_NAME failed: %m");
-        if (l >= TASK_COMM_LEN) /* Linux process names can be 15 chars at max */
+        if (l >= TASK_COMM_LEN) /* Linux userspace process names can be 15 chars at max */
                 truncated = true;
 
         /* Second step, change glibc's ID of the process name. */
@@ -518,14 +450,11 @@ static int get_process_id(pid_t pid, const char *field, uid_t *uid) {
                 return -EINVAL;
 
         p = procfs_file_alloca(pid, "status");
-        f = fopen(p, "re");
-        if (!f) {
-                if (errno == ENOENT)
-                        return -ESRCH;
-                return -errno;
-        }
-
-        (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
+        r = fopen_unlocked(p, "re", &f);
+        if (r == -ENOENT)
+                return -ESRCH;
+        if (r < 0)
+                return r;
 
         for (;;) {
                 _cleanup_free_ char *line = NULL;
@@ -607,14 +536,11 @@ int get_process_environ(pid_t pid, char **env) {
 
         p = procfs_file_alloca(pid, "environ");
 
-        f = fopen(p, "re");
-        if (!f) {
-                if (errno == ENOENT)
-                        return -ESRCH;
-                return -errno;
-        }
-
-        (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
+        r = fopen_unlocked(p, "re", &f);
+        if (r == -ENOENT)
+                return -ESRCH;
+        if (r < 0)
+                return r;
 
         for (;;) {
                 char c;
@@ -900,15 +826,11 @@ int getenv_for_pid(pid_t pid, const char *field, char **ret) {
 
         path = procfs_file_alloca(pid, "environ");
 
-        f = fopen(path, "re");
-        if (!f) {
-                if (errno == ENOENT)
-                        return -ESRCH;
-
-                return -errno;
-        }
-
-        (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
+        r = fopen_unlocked(path, "re", &f);
+        if (r == -ENOENT)
+                return -ESRCH;
+        if (r < 0)
+                return r;
 
         l = strlen(field);
         for (;;) {
@@ -1558,45 +1480,11 @@ int set_oom_score_adjust(int value) {
                                  WRITE_STRING_FILE_VERIFY_ON_FAILURE|WRITE_STRING_FILE_DISABLE_BUFFER);
 }
 
-int cpus_in_affinity_mask(void) {
-        size_t n = 16;
-        int r;
-
-        for (;;) {
-                cpu_set_t *c;
-
-                c = CPU_ALLOC(n);
-                if (!c)
-                        return -ENOMEM;
-
-                if (sched_getaffinity(0, CPU_ALLOC_SIZE(n), c) >= 0) {
-                        int k;
-
-                        k = CPU_COUNT_S(CPU_ALLOC_SIZE(n), c);
-                        CPU_FREE(c);
-
-                        if (k <= 0)
-                                return -EINVAL;
-
-                        return k;
-                }
-
-                r = -errno;
-                CPU_FREE(c);
-
-                if (r != -EINVAL)
-                        return r;
-                if (n > SIZE_MAX/2)
-                        return -ENOMEM;
-                n *= 2;
-        }
-}
-
 static const char *const ioprio_class_table[] = {
         [IOPRIO_CLASS_NONE] = "none",
         [IOPRIO_CLASS_RT] = "realtime",
         [IOPRIO_CLASS_BE] = "best-effort",
-        [IOPRIO_CLASS_IDLE] = "idle"
+        [IOPRIO_CLASS_IDLE] = "idle",
 };
 
 DEFINE_STRING_TABLE_LOOKUP_WITH_FALLBACK(ioprio_class, int, IOPRIO_N_CLASSES);
@@ -1617,7 +1505,7 @@ static const char* const sched_policy_table[] = {
         [SCHED_BATCH] = "batch",
         [SCHED_IDLE] = "idle",
         [SCHED_FIFO] = "fifo",
-        [SCHED_RR] = "rr"
+        [SCHED_RR] = "rr",
 };
 
 DEFINE_STRING_TABLE_LOOKUP_WITH_FALLBACK(sched_policy, int, INT_MAX);
diff --git a/shared/systemd/src/basic/process-util.h b/shared/systemd/src/basic/process-util.h
index 3933bee6..20f663e2 100644
--- a/shared/systemd/src/basic/process-util.h
+++ b/shared/systemd/src/basic/process-util.h
@@ -31,9 +31,13 @@
                 _r_;                                                    \
         })
 
-int get_process_state(pid_t pid);
+typedef enum ProcessCmdlineFlags {
+        PROCESS_CMDLINE_COMM_FALLBACK = 1 << 0,
+        PROCESS_CMDLINE_USE_LOCALE    = 1 << 1,
+} ProcessCmdlineFlags;
+
 int get_process_comm(pid_t pid, char **name);
-int get_process_cmdline(pid_t pid, size_t max_length, bool comm_fallback, char **line);
+int get_process_cmdline(pid_t pid, size_t max_columns, ProcessCmdlineFlags flags, char **line);
 int get_process_exe(pid_t pid, char **name);
 int get_process_uid(pid_t pid, uid_t *uid);
 int get_process_gid(pid_t pid, gid_t *gid);
@@ -187,7 +191,7 @@ int set_oom_score_adjust(int value);
 #error "Unknown pid_t size"
 #endif
 
-assert_cc(TASKS_MAX <= (unsigned long) PID_T_MAX)
+assert_cc(TASKS_MAX <= (unsigned long) PID_T_MAX);
 
 /* Like TAKE_PTR() but for child PIDs, resetting them to 0 */
 #define TAKE_PID(pid)                           \
@@ -196,5 +200,3 @@ assert_cc(TASKS_MAX <= (unsigned long) PID_T_MAX)
                 (pid) = 0;                      \
                 _pid_;                          \
         })
-
-int cpus_in_affinity_mask(void);
diff --git a/shared/systemd/src/basic/random-util.c b/shared/systemd/src/basic/random-util.c
index b8b45958..c70871bf 100644
--- a/shared/systemd/src/basic/random-util.c
+++ b/shared/systemd/src/basic/random-util.c
@@ -30,13 +30,75 @@
 #include "io-util.h"
 #include "missing.h"
 #include "random-util.h"
+#include "siphash24.h"
 #include "time-util.h"
 
 int rdrand(unsigned long *ret) {
 
+        /* So, you are a "security researcher", and you wonder why we bother with using raw RDRAND here,
+         * instead of sticking to /dev/urandom or getrandom()?
+         *
+         * Here's why: early boot. On Linux, during early boot the random pool that backs /dev/urandom and
+         * getrandom() is generally not initialized yet. It is very common that initialization of the random
+         * pool takes a longer time (up to many minutes), in particular on embedded devices that have no
+         * explicit hardware random generator, as well as in virtualized environments such as major cloud
+         * installations that do not provide virtio-rng or a similar mechanism.
+         *
+         * In such an environment using getrandom() synchronously means we'd block the entire system boot-up
+         * until the pool is initialized, i.e. *very* long. Using getrandom() asynchronously (GRND_NONBLOCK)
+         * would mean acquiring randomness during early boot would simply fail. Using /dev/urandom would mean
+         * generating many kmsg log messages about our use of it before the random pool is properly
+         * initialized. Neither of these outcomes is desirable.
+         *
+         * Thus, for very specific purposes we use RDRAND instead of either of these three options. RDRAND
+         * provides us quickly and relatively reliably with random values, without having to delay boot,
+         * without triggering warning messages in kmsg.
+         *
+         * Note that we use RDRAND only under very specific circumstances, when the requirements on the
+         * quality of the returned entropy permit it. Specifically, here are some cases where we *do* use
+         * RDRAND:
+         *
+         *         • UUID generation: UUIDs are supposed to be universally unique but are not cryptographic
+         *           key material. The quality and trust level of RDRAND should hence be OK: UUIDs should be
+         *           generated in a way that is reliably unique, but they do not require ultimate trust into
+         *           the entropy generator. systemd generates a number of UUIDs during early boot, including
+         *           'invocation IDs' for every unit spawned that identify the specific invocation of the
+         *           service globally, and a number of others. Other alternatives for generating these UUIDs
+         *           have been considered, but don't really work: for example, hashing uuids from a local
+         *           system identifier combined with a counter falls flat because during early boot disk
+         *           storage is not yet available (think: initrd) and thus a system-specific ID cannot be
+         *           stored or retrieved yet.
+         *
+         *         • Hash table seed generation: systemd uses many hash tables internally. Hash tables are
+         *           generally assumed to have O(1) access complexity, but can deteriorate to prohibitive
+         *           O(n) access complexity if an attacker manages to trigger a large number of hash
+         *           collisions. Thus, systemd (as any software employing hash tables should) uses seeded
+         *           hash functions for its hash tables, with a seed generated randomly. The hash tables
+         *           systemd employs watch the fill level closely and reseed if necessary. This allows use of
+         *           a low quality RNG initially, as long as it improves should a hash table be under attack:
+         *           the attacker after all needs to to trigger many collisions to exploit it for the purpose
+         *           of DoS, but if doing so improves the seed the attack surface is reduced as the attack
+         *           takes place.
+         *
+         * Some cases where we do NOT use RDRAND are:
+         *
+         *         • Generation of cryptographic key material 🔑
+         *
+         *         • Generation of cryptographic salt values 🧂
+         *
+         * This function returns:
+         *
+         *         -EOPNOTSUPP → RDRAND is not available on this system 😔
+         *         -EAGAIN     → The operation failed this time, but is likely to work if you try again a few
+         *                       times ♻
+         *         -EUCLEAN    → We got some random value, but it looked strange, so we refused using it.
+         *                       This failure might or might not be temporary. 😕
+         */
+
 #if defined(__i386__) || defined(__x86_64__)
         static int have_rdrand = -1;
-        unsigned char err;
+        unsigned long v;
+        uint8_t success;
 
         if (have_rdrand < 0) {
                 uint32_t eax, ebx, ecx, edx;
@@ -47,7 +109,12 @@ int rdrand(unsigned long *ret) {
                         return -EOPNOTSUPP;
                 }
 
-                have_rdrand = !!(ecx & (1U << 30));
+/* Compat with old gcc where bit_RDRND didn't exist yet */
+#ifndef bit_RDRND
+#define bit_RDRND (1U << 30)
+#endif
+
+                have_rdrand = !!(ecx & bit_RDRND);
         }
 
         if (have_rdrand == 0)
@@ -55,12 +122,24 @@ int rdrand(unsigned long *ret) {
 
         asm volatile("rdrand %0;"
                      "setc %1"
-                     : "=r" (*ret),
-                       "=qm" (err));
-        msan_unpoison(&err, sizeof(err));
-        if (!err)
+                     : "=r" (v),
+                       "=qm" (success));
+        msan_unpoison(&success, sizeof(success));
+        if (!success)
                 return -EAGAIN;
 
+        /* Apparently on some AMD CPUs RDRAND will sometimes (after a suspend/resume cycle?) report success
+         * via the carry flag but nonetheless return the same fixed value -1 in all cases. This appears to be
+         * a bad bug in the CPU or firmware. Let's deal with that and work-around this by explicitly checking
+         * for this special value (and also 0, just to be sure) and filtering it out. This is a work-around
+         * only however and something AMD really should fix properly. The Linux kernel should probably work
+         * around this issue by turning off RDRAND altogether on those CPUs. See:
+         * https://github.com/systemd/systemd/issues/11810 */
+        if (v == 0 || v == ULONG_MAX)
+                return log_debug_errno(SYNTHETIC_ERRNO(EUCLEAN),
+                                       "RDRAND returned suspicious value %lx, assuming bad hardware RNG, not using value.", v);
+
+        *ret = v;
         return 0;
 #else
         return -EOPNOTSUPP;
@@ -73,21 +152,32 @@ int genuine_random_bytes(void *p, size_t n, RandomFlags flags) {
         bool got_some = false;
         int r;
 
-        /* Gathers some randomness from the kernel (or the CPU if the RANDOM_ALLOW_RDRAND flag is set). This call won't
-         * block, unless the RANDOM_BLOCK flag is set. If RANDOM_DONT_DRAIN is set, an error is returned if the random
-         * pool is not initialized. Otherwise it will always return some data from the kernel, regardless of whether
-         * the random pool is fully initialized or not. */
+        /* Gathers some high-quality randomness from the kernel (or potentially mid-quality randomness from
+         * the CPU if the RANDOM_ALLOW_RDRAND flag is set). This call won't block, unless the RANDOM_BLOCK
+         * flag is set. If RANDOM_MAY_FAIL is set, an error is returned if the random pool is not
+         * initialized. Otherwise it will always return some data from the kernel, regardless of whether the
+         * random pool is fully initialized or not. If RANDOM_EXTEND_WITH_PSEUDO is set, and some but not
+         * enough better quality randomness could be acquired, the rest is filled up with low quality
+         * randomness.
+         *
+         * Of course, when creating cryptographic key material you really shouldn't use RANDOM_ALLOW_DRDRAND
+         * or even RANDOM_EXTEND_WITH_PSEUDO.
+         *
+         * When generating UUIDs it's fine to use RANDOM_ALLOW_RDRAND but not OK to use
+         * RANDOM_EXTEND_WITH_PSEUDO. In fact RANDOM_EXTEND_WITH_PSEUDO is only really fine when invoked via
+         * an "all bets are off" wrapper, such as random_bytes(), see below. */
 
         if (n == 0)
                 return 0;
 
         if (FLAGS_SET(flags, RANDOM_ALLOW_RDRAND))
-                /* Try x86-64' RDRAND intrinsic if we have it. We only use it if high quality randomness is not
-                 * required, as we don't trust it (who does?). Note that we only do a single iteration of RDRAND here,
-                 * even though the Intel docs suggest calling this in a tight loop of 10 invocations or so. That's
-                 * because we don't really care about the quality here. We generally prefer using RDRAND if the caller
-                 * allows us too, since this way we won't drain the kernel randomness pool if we don't need it, as the
-                 * pool's entropy is scarce. */
+                /* Try x86-64' RDRAND intrinsic if we have it. We only use it if high quality randomness is
+                 * not required, as we don't trust it (who does?). Note that we only do a single iteration of
+                 * RDRAND here, even though the Intel docs suggest calling this in a tight loop of 10
+                 * invocations or so. That's because we don't really care about the quality here. We
+                 * generally prefer using RDRAND if the caller allows us to, since this way we won't upset
+                 * the kernel's random subsystem by accessing it before the pool is initialized (after all it
+                 * will kmsg log about every attempt to do so)..*/
                 for (;;) {
                         unsigned long u;
                         size_t m;
@@ -162,12 +252,13 @@ int genuine_random_bytes(void *p, size_t n, RandomFlags flags) {
                                 break;
 
                         } else if (errno == EAGAIN) {
-                                /* The kernel has no entropy whatsoever. Let's remember to use the syscall the next
-                                 * time again though.
+                                /* The kernel has no entropy whatsoever. Let's remember to use the syscall
+                                 * the next time again though.
                                  *
-                                 * If RANDOM_DONT_DRAIN is set, return an error so that random_bytes() can produce some
-                                 * pseudo-random bytes instead. Otherwise, fall back to /dev/urandom, which we know is empty,
-                                 * but the kernel will produce some bytes for us on a best-effort basis. */
+                                 * If RANDOM_MAY_FAIL is set, return an error so that random_bytes() can
+                                 * produce some pseudo-random bytes instead. Otherwise, fall back to
+                                 * /dev/urandom, which we know is empty, but the kernel will produce some
+                                 * bytes for us on a best-effort basis. */
                                 have_syscall = true;
 
                                 if (got_some && FLAGS_SET(flags, RANDOM_EXTEND_WITH_PSEUDO)) {
@@ -176,7 +267,7 @@ int genuine_random_bytes(void *p, size_t n, RandomFlags flags) {
                                         return 0;
                                 }
 
-                                if (FLAGS_SET(flags, RANDOM_DONT_DRAIN))
+                                if (FLAGS_SET(flags, RANDOM_MAY_FAIL))
                                         return -ENODATA;
 
                                 /* Use /dev/urandom instead */
@@ -205,14 +296,19 @@ void initialize_srand(void) {
                 return;
 
 #if HAVE_SYS_AUXV_H
-        /* The kernel provides us with 16 bytes of entropy in auxv, so let's
-         * try to make use of that to seed the pseudo-random generator. It's
-         * better than nothing... */
+        /* The kernel provides us with 16 bytes of entropy in auxv, so let's try to make use of that to seed
+         * the pseudo-random generator. It's better than nothing... But let's first hash it to make it harder
+         * to recover the original value by watching any pseudo-random bits we generate. After all the
+         * AT_RANDOM data might be used by other stuff too (in particular: ASLR), and we probably shouldn't
+         * leak the seed for that. */
 
-        auxv = (const void*) getauxval(AT_RANDOM);
+        auxv = ULONG_TO_PTR(getauxval(AT_RANDOM));
         if (auxv) {
-                assert_cc(sizeof(x) <= 16);
-                memcpy(&x, auxv, sizeof(x));
+                static const uint8_t auxval_hash_key[16] = {
+                        0x92, 0x6e, 0xfe, 0x1b, 0xcf, 0x00, 0x52, 0x9c, 0xcc, 0x42, 0xcf, 0xdc, 0x94, 0x1f, 0x81, 0x0f
+                };
+
+                x = (unsigned) siphash24(auxv, 16, auxval_hash_key);
         } else
 #endif
                 x = 0;
@@ -238,6 +334,11 @@ void initialize_srand(void) {
 void pseudo_random_bytes(void *p, size_t n) {
         uint8_t *q;
 
+        /* This returns pseudo-random data using libc's rand() function. You probably never want to call this
+         * directly, because why would you use this if you can get better stuff cheaply? Use random_bytes()
+         * instead, see below: it will fall back to this function if there's nothing better to get, but only
+         * then. */
+
         initialize_srand();
 
         for (q = p; q < (uint8_t*) p + n; q += RAND_STEP) {
@@ -259,7 +360,39 @@ void pseudo_random_bytes(void *p, size_t n) {
 
 void random_bytes(void *p, size_t n) {
 
-        if (genuine_random_bytes(p, n, RANDOM_EXTEND_WITH_PSEUDO|RANDOM_DONT_DRAIN|RANDOM_ALLOW_RDRAND) >= 0)
+        /* This returns high quality randomness if we can get it cheaply. If we can't because for some reason
+         * it is not available we'll try some crappy fallbacks.
+         *
+         * What this function will do:
+         *
+         *         • This function will preferably use the CPU's RDRAND operation, if it is available, in
+         *           order to return "mid-quality" random values cheaply.
+         *
+         *         • Use getrandom() with GRND_NONBLOCK, to return high-quality random values if they are
+         *           cheaply available.
+         *
+         *         • This function will return pseudo-random data, generated via libc rand() if nothing
+         *           better is available.
+         *
+         *         • This function will work fine in early boot
+         *
+         *         • This function will always succeed
+         *
+         * What this function won't do:
+         *
+         *         • This function will never fail: it will give you randomness no matter what. It might not
+         *           be high quality, but it will return some, possibly generated via libc's rand() call.
+         *
+         *         • This function will never block: if the only way to get good randomness is a blocking,
+         *           synchronous getrandom() we'll instead provide you with pseudo-random data.
+         *
+         * This function is hence great for things like seeding hash tables, generating random numeric UNIX
+         * user IDs (that are checked for collisions before use) and such.
+         *
+         * This function is hence not useful for generating UUIDs or cryptographic key material.
+         */
+
+        if (genuine_random_bytes(p, n, RANDOM_EXTEND_WITH_PSEUDO|RANDOM_MAY_FAIL|RANDOM_ALLOW_RDRAND) >= 0)
                 return;
 
         /* If for some reason some user made /dev/urandom unavailable to us, or the kernel has no entropy, use a PRNG instead. */
diff --git a/shared/systemd/src/basic/random-util.h b/shared/systemd/src/basic/random-util.h
index 3e8c288d..148b6c78 100644
--- a/shared/systemd/src/basic/random-util.h
+++ b/shared/systemd/src/basic/random-util.h
@@ -8,11 +8,11 @@
 typedef enum RandomFlags {
         RANDOM_EXTEND_WITH_PSEUDO = 1 << 0, /* If we can't get enough genuine randomness, but some, fill up the rest with pseudo-randomness */
         RANDOM_BLOCK              = 1 << 1, /* Rather block than return crap randomness (only if the kernel supports that) */
-        RANDOM_DONT_DRAIN         = 1 << 2, /* If we can't get any randomness at all, return early with -EAGAIN */
+        RANDOM_MAY_FAIL           = 1 << 2, /* If we can't get any randomness at all, return early with -ENODATA */
         RANDOM_ALLOW_RDRAND       = 1 << 3, /* Allow usage of the CPU RNG */
 } RandomFlags;
 
-int genuine_random_bytes(void *p, size_t n, RandomFlags flags); /* returns "genuine" randomness, optionally filled upwith pseudo random, if not enough is available */
+int genuine_random_bytes(void *p, size_t n, RandomFlags flags); /* returns "genuine" randomness, optionally filled up with pseudo random, if not enough is available */
 void pseudo_random_bytes(void *p, size_t n);                    /* returns only pseudo-randommess (but possibly seeded from something better) */
 void random_bytes(void *p, size_t n);                           /* returns genuine randomness if cheaply available, and pseudo randomness if not. */
 
diff --git a/shared/systemd/src/basic/set.h b/shared/systemd/src/basic/set.h
index 2a80632b..2bb26c68 100644
--- a/shared/systemd/src/basic/set.h
+++ b/shared/systemd/src/basic/set.h
@@ -28,13 +28,13 @@ int internal_set_ensure_allocated(Set **s, const struct hash_ops *hash_ops HASHM
 int set_put(Set *s, const void *key);
 /* no set_update */
 /* no set_replace */
-static inline void *set_get(Set *s, void *key) {
-        return internal_hashmap_get(HASHMAP_BASE(s), key);
+static inline void *set_get(const Set *s, void *key) {
+        return internal_hashmap_get(HASHMAP_BASE((Set *) s), key);
 }
 /* no set_get2 */
 
-static inline bool set_contains(Set *s, const void *key) {
-        return internal_hashmap_contains(HASHMAP_BASE(s), key);
+static inline bool set_contains(const Set *s, const void *key) {
+        return internal_hashmap_contains(HASHMAP_BASE((Set *) s), key);
 }
 
 static inline void *set_remove(Set *s, const void *key) {
@@ -59,19 +59,19 @@ static inline int set_move_one(Set *s, Set *other, const void *key) {
         return internal_hashmap_move_one(HASHMAP_BASE(s), HASHMAP_BASE(other), key);
 }
 
-static inline unsigned set_size(Set *s) {
-        return internal_hashmap_size(HASHMAP_BASE(s));
+static inline unsigned set_size(const Set *s) {
+        return internal_hashmap_size(HASHMAP_BASE((Set *) s));
 }
 
-static inline bool set_isempty(Set *s) {
+static inline bool set_isempty(const Set *s) {
         return set_size(s) == 0;
 }
 
-static inline unsigned set_buckets(Set *s) {
-        return internal_hashmap_buckets(HASHMAP_BASE(s));
+static inline unsigned set_buckets(const Set *s) {
+        return internal_hashmap_buckets(HASHMAP_BASE((Set *) s));
 }
 
-bool set_iterate(Set *s, Iterator *i, void **value);
+bool set_iterate(const Set *s, Iterator *i, void **value);
 
 static inline void set_clear(Set *s) {
         internal_hashmap_clear(HASHMAP_BASE(s), NULL, NULL);
diff --git a/shared/systemd/src/basic/siphash24.h b/shared/systemd/src/basic/siphash24.h
index be1d3e00..c4e919df 100644
--- a/shared/systemd/src/basic/siphash24.h
+++ b/shared/systemd/src/basic/siphash24.h
@@ -47,6 +47,7 @@ siphash24 (const void *in, size_t inlen, const uint8_t k[16])
 
 void siphash24_init(struct siphash *state, const uint8_t k[static 16]);
 void siphash24_compress(const void *in, size_t inlen, struct siphash *state);
+void siphash24_compress_boolean(bool in, struct siphash *state);
 #define siphash24_compress_byte(byte, state) siphash24_compress((const uint8_t[]) { (byte) }, 1, (state))
 
 uint64_t siphash24_finalize(struct siphash *state);
diff --git a/shared/systemd/src/basic/socket-util.c b/shared/systemd/src/basic/socket-util.c
index b98b0461..b822ed03 100644
--- a/shared/systemd/src/basic/socket-util.c
+++ b/shared/systemd/src/basic/socket-util.c
@@ -81,7 +81,7 @@ int socket_address_parse(SocketAddress *a, const char *s) {
 
                 errno = 0;
                 if (inet_pton(AF_INET6, n, &a->sockaddr.in6.sin6_addr) <= 0)
-                        return errno > 0 ? -errno : -EINVAL;
+                        return errno_or_else(EINVAL);
 
                 e++;
                 if (*e != ':')
@@ -1224,17 +1224,34 @@ fallback:
 }
 
 #if 0 /* NM_IGNORED */
+/* Put a limit on how many times will attempt to call accept4(). We loop
+ * only on "transient" errors, but let's make sure we don't loop forever. */
+#define MAX_FLUSH_ITERATIONS 1024
+
 int flush_accept(int fd) {
 
         struct pollfd pollfd = {
                 .fd = fd,
                 .events = POLLIN,
         };
-        int r;
+        int r, b;
+        socklen_t l = sizeof(b);
 
-        /* Similar to flush_fd() but flushes all incoming connection by accepting them and immediately closing them. */
+        /* Similar to flush_fd() but flushes all incoming connections by accepting and immediately closing
+         * them. */
 
-        for (;;) {
+        if (getsockopt(fd, SOL_SOCKET, SO_ACCEPTCONN, &b, &l) < 0)
+                return -errno;
+
+        assert(l == sizeof(b));
+        if (!b) /* Let's check if this socket accepts connections before calling accept(). accept4() can
+                 * return EOPNOTSUPP if the fd is not a listening socket, which we should treat as a fatal
+                 * error, or in case the incoming TCP connection triggered a network issue, which we want to
+                 * treat as a transient error. Thus, let's rule out the first reason for EOPNOTSUPP early, so
+                 * we can loop safely on transient errors below. */
+                return -ENOTTY;
+
+        for (unsigned iteration = 0;; iteration++) {
                 int cfd;
 
                 r = poll(&pollfd, 1, 0);
@@ -1247,6 +1264,10 @@ int flush_accept(int fd) {
                 if (r == 0)
                         return 0;
 
+                if (iteration >= MAX_FLUSH_ITERATIONS)
+                        return log_debug_errno(SYNTHETIC_ERRNO(EBUSY),
+                                               "Failed to flush connections within " STRINGIFY(MAX_FLUSH_ITERATIONS) " iterations.");
+
                 cfd = accept4(fd, NULL, NULL, SOCK_NONBLOCK|SOCK_CLOEXEC);
                 if (cfd < 0) {
                         if (errno == EAGAIN)
@@ -1374,7 +1395,7 @@ int socket_bind_to_ifname(int fd, const char *ifname) {
 }
 
 int socket_bind_to_ifindex(int fd, int ifindex) {
-        char ifname[IFNAMSIZ] = "";
+        char ifname[IF_NAMESIZE + 1];
 
         assert(fd >= 0);
 
@@ -1392,7 +1413,7 @@ int socket_bind_to_ifindex(int fd, int ifindex) {
                 return -errno;
 
         /* Fall back to SO_BINDTODEVICE on kernels < 5.0 which didn't have SO_BINDTOIFINDEX */
-        if (!if_indextoname(ifindex, ifname))
+        if (!format_ifname(ifindex, ifname))
                 return -errno;
 
         return socket_bind_to_ifname(fd, ifname);
diff --git a/shared/systemd/src/basic/socket-util.h b/shared/systemd/src/basic/socket-util.h
index 15443f1e..a0886e0e 100644
--- a/shared/systemd/src/basic/socket-util.h
+++ b/shared/systemd/src/basic/socket-util.h
@@ -3,12 +3,13 @@
 
 #include <inttypes.h>
 #include <linux/netlink.h>
+#include <linux/if_ether.h>
 #include <linux/if_infiniband.h>
 #include <linux/if_packet.h>
-#include <netinet/ether.h>
 #include <netinet/in.h>
 #include <stdbool.h>
 #include <stddef.h>
+#include <string.h>
 #include <sys/socket.h>
 #include <sys/types.h>
 #include <sys/un.h>
diff --git a/shared/systemd/src/basic/string-util.c b/shared/systemd/src/basic/string-util.c
index df519976..2d34603e 100644
--- a/shared/systemd/src/basic/string-util.c
+++ b/shared/systemd/src/basic/string-util.c
@@ -6,7 +6,6 @@
 #include <stdarg.h>
 #include <stdint.h>
 #include <stdio.h>
-#include <stdio_ext.h>
 #include <stdlib.h>
 #include <string.h>
 
@@ -209,10 +208,6 @@ char *strnappend(const char *s, const char *suffix, size_t b) {
         return r;
 }
 
-char *strappend(const char *s, const char *suffix) {
-        return strnappend(s, suffix, strlen_ptr(suffix));
-}
-
 #if 0 /* NM_IGNORED */
 char *strjoin_real(const char *x, ...) {
         va_list ap;
@@ -736,10 +731,18 @@ char *strreplace(const char *text, const char *old_string, const char *new_strin
         return ret;
 }
 
-static void advance_offsets(ssize_t diff, size_t offsets[static 2], size_t shift[static 2], size_t size) {
+#if 0 /* NM_IGNORED */
+static void advance_offsets(
+                ssize_t diff,
+                size_t offsets[2], /* note: we can't use [static 2] here, since this may be NULL */
+                size_t shift[static 2],
+                size_t size) {
+
         if (!offsets)
                 return;
 
+        assert(shift);
+
         if ((size_t) diff < offsets[0])
                 shift[0] += size;
         if ((size_t) diff < offsets[1])
@@ -767,23 +770,20 @@ char *strip_tab_ansi(char **ibuf, size_t *_isz, size_t highlight[2]) {
          * 2. Strips ANSI color sequences (a subset of CSI), i.e. ESC '[' … 'm' sequences
          * 3. Strips ANSI operating system sequences (CSO), i.e. ESC ']' … BEL sequences
          *
-         * Everything else will be left as it is. In particular other ANSI sequences are left as they are, as are any
-         * other special characters. Truncated ANSI sequences are left-as is too. This call is supposed to suppress the
-         * most basic formatting noise, but nothing else.
+         * Everything else will be left as it is. In particular other ANSI sequences are left as they are, as
+         * are any other special characters. Truncated ANSI sequences are left-as is too. This call is
+         * supposed to suppress the most basic formatting noise, but nothing else.
          *
          * Why care for CSO sequences? Well, to undo what terminal_urlify() and friends generate. */
 
         isz = _isz ? *_isz : strlen(*ibuf);
 
-        f = open_memstream(&obuf, &osz);
+        /* Note we turn off internal locking on f for performance reasons. It's safe to do so since we
+         * created f here and it doesn't leave our scope. */
+        f = open_memstream_unlocked(&obuf, &osz);
         if (!f)
                 return NULL;
 
-        /* Note we turn off internal locking on f for performance reasons.  It's safe to do so since we created f here
-         * and it doesn't leave our scope. */
-
-        (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
-
         for (i = *ibuf; i < *ibuf + isz + 1; i++) {
 
                 switch (state) {
@@ -858,8 +858,7 @@ char *strip_tab_ansi(char **ibuf, size_t *_isz, size_t highlight[2]) {
 
         fclose(f);
 
-        free(*ibuf);
-        *ibuf = obuf;
+        free_and_replace(*ibuf, obuf);
 
         if (_isz)
                 *_isz = osz;
@@ -869,10 +868,9 @@ char *strip_tab_ansi(char **ibuf, size_t *_isz, size_t highlight[2]) {
                 highlight[1] += shift[1];
         }
 
-        return obuf;
+        return *ibuf;
 }
 
-#if 0 /* NM_IGNORED */
 char *strextend_with_separator(char **x, const char *separator, ...) {
         bool need_separator;
         size_t f, l, l_separator;
@@ -1044,20 +1042,6 @@ int free_and_strndup(char **p, const char *s, size_t l) {
         return 1;
 }
 
-char* string_erase(char *x) {
-        if (!x)
-                return NULL;
-
-        /* A delicious drop of snake-oil! To be called on memory where
-         * we stored passphrases or so, after we used them. */
-        explicit_bzero_safe(x, strlen(x));
-        return x;
-}
-
-char *string_free_erase(char *s) {
-        return mfree(string_erase(s));
-}
-
 bool string_is_safe(const char *p) {
         const char *t;
 
diff --git a/shared/systemd/src/basic/string-util.h b/shared/systemd/src/basic/string-util.h
index b23f4c83..76767afc 100644
--- a/shared/systemd/src/basic/string-util.h
+++ b/shared/systemd/src/basic/string-util.h
@@ -108,7 +108,6 @@ const char* split(const char **state, size_t *l, const char *separator, SplitFla
 #define _FOREACH_WORD(word, length, s, separator, flags, state)         \
         for ((state) = (s), (word) = split(&(state), &(length), (separator), (flags)); (word); (word) = split(&(state), &(length), (separator), (flags)))
 
-char *strappend(const char *s, const char *suffix);
 char *strnappend(const char *s, const char *suffix, size_t length);
 
 char *strjoin_real(const char *x, ...) _sentinel_;
@@ -197,12 +196,6 @@ static inline int free_and_strdup_warn(char **p, const char *s) {
 }
 int free_and_strndup(char **p, const char *s, size_t l);
 
-char *string_erase(char *x);
-
-char *string_free_erase(char *s);
-DEFINE_TRIVIAL_CLEANUP_FUNC(char *, string_free_erase);
-#define _cleanup_string_free_erase_ _cleanup_(string_free_erasep)
-
 bool string_is_safe(const char *p) _pure_;
 
 static inline size_t strlen_ptr(const char *s) {
@@ -212,6 +205,12 @@ static inline size_t strlen_ptr(const char *s) {
         return strlen(s);
 }
 
+DISABLE_WARNING_STRINGOP_TRUNCATION;
+static inline void strncpy_exact(char *buf, const char *src, size_t buf_len) {
+        strncpy(buf, src, buf_len);
+}
+REENABLE_WARNING;
+
 /* Like startswith(), but operates on arbitrary memory blocks */
 static inline void *memory_startswith(const void *p, size_t sz, const char *token) {
         size_t n;
@@ -251,3 +250,16 @@ static inline void *memory_startswith_no_case(const void *p, size_t sz, const ch
 
         return (uint8_t*) p + n;
 }
+
+static inline char* str_realloc(char **p) {
+        /* Reallocate *p to actual size */
+
+        if (!*p)
+                return NULL;
+
+        char *t = realloc(*p, strlen(*p) + 1);
+        if (!t)
+                return NULL;
+
+        return (*p = t);
+}
diff --git a/shared/systemd/src/basic/strv.c b/shared/systemd/src/basic/strv.c
index 8de40b9d..ba23178a 100644
--- a/shared/systemd/src/basic/strv.c
+++ b/shared/systemd/src/basic/strv.c
@@ -13,6 +13,7 @@
 #include "escape.h"
 #include "extract-word.h"
 #include "fileio.h"
+#include "memory-util.h"
 #include "nulstr-util.h"
 #include "sort-util.h"
 #include "string-util.h"
@@ -80,9 +81,9 @@ char **strv_free_erase(char **l) {
         char **i;
 
         STRV_FOREACH(i, l)
-                string_erase(*i);
+                erase_and_freep(i);
 
-        return strv_free(l);
+        return mfree(l);
 }
 
 char **strv_copy(char * const *l) {
@@ -227,6 +228,7 @@ rollback:
         return -ENOMEM;
 }
 
+#if 0 /* NM_IGNORED */
 int strv_extend_strv_concat(char ***a, char **b, const char *suffix) {
         int r;
         char **s;
@@ -234,7 +236,7 @@ int strv_extend_strv_concat(char ***a, char **b, const char *suffix) {
         STRV_FOREACH(s, b) {
                 char *v;
 
-                v = strappend(*s, suffix);
+                v = strjoin(*s, suffix);
                 if (!v)
                         return -ENOMEM;
 
@@ -247,6 +249,7 @@ int strv_extend_strv_concat(char ***a, char **b, const char *suffix) {
 
         return 0;
 }
+#endif /* NM_IGNORED */
 
 char **strv_split_full(const char *s, const char *separator, SplitFlags flags) {
         const char *word, *state;
@@ -896,3 +899,63 @@ int fputstrv(FILE *f, char **l, const char *separator, bool *space) {
 
         return 0;
 }
+
+static int string_strv_hashmap_put_internal(Hashmap *h, const char *key, const char *value) {
+        char **l;
+        int r;
+
+        l = hashmap_get(h, key);
+        if (l) {
+                /* A list for this key already exists, let's append to it if it is not listed yet */
+                if (strv_contains(l, value))
+                        return 0;
+
+                r = strv_extend(&l, value);
+                if (r < 0)
+                        return r;
+
+                assert_se(hashmap_update(h, key, l) >= 0);
+        } else {
+                /* No list for this key exists yet, create one */
+                _cleanup_strv_free_ char **l2 = NULL;
+                _cleanup_free_ char *t = NULL;
+
+                t = strdup(key);
+                if (!t)
+                        return -ENOMEM;
+
+                r = strv_extend(&l2, value);
+                if (r < 0)
+                        return r;
+
+                r = hashmap_put(h, t, l2);
+                if (r < 0)
+                        return r;
+                TAKE_PTR(t);
+                TAKE_PTR(l2);
+        }
+
+        return 1;
+}
+
+int string_strv_hashmap_put(Hashmap **h, const char *key, const char *value) {
+        int r;
+
+        r = hashmap_ensure_allocated(h, &string_strv_hash_ops);
+        if (r < 0)
+                return r;
+
+        return string_strv_hashmap_put_internal(*h, key, value);
+}
+
+int string_strv_ordered_hashmap_put(OrderedHashmap **h, const char *key, const char *value) {
+        int r;
+
+        r = ordered_hashmap_ensure_allocated(h, &string_strv_hash_ops);
+        if (r < 0)
+                return r;
+
+        return string_strv_hashmap_put_internal(PLAIN_HASHMAP(*h), key, value);
+}
+
+DEFINE_HASH_OPS_FULL(string_strv_hash_ops, char, string_hash_func, string_compare_func, free, char*, strv_free);
diff --git a/shared/systemd/src/basic/strv.h b/shared/systemd/src/basic/strv.h
index aa5f95ab..e80964ac 100644
--- a/shared/systemd/src/basic/strv.h
+++ b/shared/systemd/src/basic/strv.h
@@ -9,6 +9,7 @@
 
 #include "alloc-util.h"
 #include "extract-word.h"
+#include "hashmap.h"
 #include "macro.h"
 #include "string-util.h"
 
@@ -188,3 +189,7 @@ int fputstrv(FILE *f, char **l, const char *separator, bool *space);
                 (b) = NULL;                     \
                 0;                              \
         })
+
+extern const struct hash_ops string_strv_hash_ops;
+int string_strv_hashmap_put(Hashmap **h, const char *key, const char *value);
+int string_strv_ordered_hashmap_put(OrderedHashmap **h, const char *key, const char *value);
diff --git a/shared/systemd/src/basic/strxcpyx.c b/shared/systemd/src/basic/strxcpyx.c
new file mode 100644
index 00000000..301e6899
--- /dev/null
+++ b/shared/systemd/src/basic/strxcpyx.c
@@ -0,0 +1,118 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+/*
+ * Concatenates/copies strings. In any case, terminates in all cases
+ * with '\0' and moves the @dest pointer forward to the added '\0'.
+ * Returns the remaining size, and 0 if the string was truncated.
+ *
+ * Due to the intended usage, these helpers silently noop invocations
+ * having zero size.  This is technically an exception to the above
+ * statement "terminates in all cases".  It's unexpected for such calls to
+ * occur outside of a loop where this is the preferred behavior.
+ */
+
+#include "nm-sd-adapt-shared.h"
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "strxcpyx.h"
+
+size_t strnpcpy(char **dest, size_t size, const char *src, size_t len) {
+        assert(dest);
+        assert(src);
+
+        if (size == 0)
+                return 0;
+
+        if (len >= size) {
+                if (size > 1)
+                        *dest = mempcpy(*dest, src, size-1);
+                size = 0;
+        } else if (len > 0) {
+                *dest = mempcpy(*dest, src, len);
+                size -= len;
+        }
+
+        *dest[0] = '\0';
+        return size;
+}
+
+size_t strpcpy(char **dest, size_t size, const char *src) {
+        assert(dest);
+        assert(src);
+
+        return strnpcpy(dest, size, src, strlen(src));
+}
+
+size_t strpcpyf(char **dest, size_t size, const char *src, ...) {
+        va_list va;
+        int i;
+
+        assert(dest);
+        assert(src);
+
+        if (size == 0)
+                return 0;
+
+        va_start(va, src);
+        i = vsnprintf(*dest, size, src, va);
+        if (i < (int)size) {
+                *dest += i;
+                size -= i;
+        } else
+                size = 0;
+        va_end(va);
+        return size;
+}
+
+size_t strpcpyl(char **dest, size_t size, const char *src, ...) {
+        va_list va;
+
+        assert(dest);
+        assert(src);
+
+        va_start(va, src);
+        do {
+                size = strpcpy(dest, size, src);
+                src = va_arg(va, char *);
+        } while (src);
+        va_end(va);
+        return size;
+}
+
+size_t strnscpy(char *dest, size_t size, const char *src, size_t len) {
+        char *s;
+
+        assert(dest);
+        assert(src);
+
+        s = dest;
+        return strnpcpy(&s, size, src, len);
+}
+
+size_t strscpy(char *dest, size_t size, const char *src) {
+        assert(dest);
+        assert(src);
+
+        return strnscpy(dest, size, src, strlen(src));
+}
+
+size_t strscpyl(char *dest, size_t size, const char *src, ...) {
+        va_list va;
+        char *s;
+
+        assert(dest);
+        assert(src);
+
+        va_start(va, src);
+        s = dest;
+        do {
+                size = strpcpy(&s, size, src);
+                src = va_arg(va, char *);
+        } while (src);
+        va_end(va);
+
+        return size;
+}
diff --git a/shared/systemd/src/basic/strxcpyx.h b/shared/systemd/src/basic/strxcpyx.h
new file mode 100644
index 00000000..9b668412
--- /dev/null
+++ b/shared/systemd/src/basic/strxcpyx.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include <stddef.h>
+
+#include "macro.h"
+
+size_t strnpcpy(char **dest, size_t size, const char *src, size_t len);
+size_t strpcpy(char **dest, size_t size, const char *src);
+size_t strpcpyf(char **dest, size_t size, const char *src, ...) _printf_(3, 4);
+size_t strpcpyl(char **dest, size_t size, const char *src, ...) _sentinel_;
+size_t strnscpy(char *dest, size_t size, const char *src, size_t len);
+size_t strscpy(char *dest, size_t size, const char *src);
+size_t strscpyl(char *dest, size_t size, const char *src, ...) _sentinel_;
diff --git a/shared/systemd/src/basic/time-util.c b/shared/systemd/src/basic/time-util.c
index 14b17bfc..aa790023 100644
--- a/shared/systemd/src/basic/time-util.c
+++ b/shared/systemd/src/basic/time-util.c
@@ -270,13 +270,12 @@ static char *format_timestamp_internal(
 
         assert(buf);
 
-        if (l <
-            3 +                  /* week day */
-            1 + 10 +             /* space and date */
-            1 + 8 +              /* space and time */
-            (us ? 1 + 6 : 0) +   /* "." and microsecond part */
-            1 + 1 +              /* space and shortest possible zone */
-            1)
+        if (l < (size_t) (3 +                  /* week day */
+                          1 + 10 +             /* space and date */
+                          1 + 8 +              /* space and time */
+                          (us ? 1 + 6 : 0) +   /* "." and microsecond part */
+                          1 + 1 +              /* space and shortest possible zone */
+                          1))
                 return NULL; /* Not enough space even for the shortest form. */
         if (t <= 0 || t == USEC_INFINITY)
                 return NULL; /* Timestamp is unset */
@@ -580,7 +579,6 @@ static int parse_timestamp_impl(const char *t, usec_t *usec, bool with_tz) {
          */
 
         assert(t);
-        assert(usec);
 
         if (t[0] == '@' && !with_tz)
                 return parse_sec(t + 1, usec);
@@ -808,8 +806,8 @@ finish:
         else
                 return -EINVAL;
 
-        *usec = ret;
-
+        if (usec)
+                *usec = ret;
         return 0;
 }
 
@@ -866,7 +864,7 @@ int parse_timestamp(const char *t, usec_t *usec) {
         if (munmap(shared, sizeof *shared) != 0)
                 return negative_errno();
 
-        if (tmp.return_value == 0)
+        if (tmp.return_value == 0 && usec)
                 *usec = tmp.usec;
 
         return tmp.return_value;
@@ -928,7 +926,6 @@ int parse_time(const char *t, usec_t *usec, usec_t default_unit) {
         bool something = false;
 
         assert(t);
-        assert(usec);
         assert(default_unit > 0);
 
         p = t;
@@ -940,7 +937,8 @@ int parse_time(const char *t, usec_t *usec, usec_t default_unit) {
                 if (*s != 0)
                         return -EINVAL;
 
-                *usec = USEC_INFINITY;
+                if (usec)
+                        *usec = USEC_INFINITY;
                 return 0;
         }
 
@@ -1012,8 +1010,8 @@ int parse_time(const char *t, usec_t *usec, usec_t default_unit) {
                 }
         }
 
-        *usec = r;
-
+        if (usec)
+                *usec = r;
         return 0;
 }
 
diff --git a/shared/systemd/src/basic/time-util.h b/shared/systemd/src/basic/time-util.h
index a238f691..e3a529d9 100644
--- a/shared/systemd/src/basic/time-util.h
+++ b/shared/systemd/src/basic/time-util.h
@@ -81,15 +81,19 @@ triple_timestamp* triple_timestamp_from_realtime(triple_timestamp *ts, usec_t u)
 #define TRIPLE_TIMESTAMP_HAS_CLOCK(clock)                               \
         IN_SET(clock, CLOCK_REALTIME, CLOCK_REALTIME_ALARM, CLOCK_MONOTONIC, CLOCK_BOOTTIME, CLOCK_BOOTTIME_ALARM)
 
+static inline bool timestamp_is_set(usec_t timestamp) {
+        return timestamp > 0 && timestamp != USEC_INFINITY;
+}
+
 static inline bool dual_timestamp_is_set(const dual_timestamp *ts) {
-        return ((ts->realtime > 0 && ts->realtime != USEC_INFINITY) ||
-                (ts->monotonic > 0 && ts->monotonic != USEC_INFINITY));
+        return timestamp_is_set(ts->realtime) ||
+               timestamp_is_set(ts->monotonic);
 }
 
 static inline bool triple_timestamp_is_set(const triple_timestamp *ts) {
-        return ((ts->realtime > 0 && ts->realtime != USEC_INFINITY) ||
-                (ts->monotonic > 0 && ts->monotonic != USEC_INFINITY) ||
-                (ts->boottime > 0 && ts->boottime != USEC_INFINITY));
+        return timestamp_is_set(ts->realtime) ||
+               timestamp_is_set(ts->monotonic) ||
+               timestamp_is_set(ts->boottime);
 }
 
 usec_t triple_timestamp_by_clock(triple_timestamp *ts, clockid_t clock);
diff --git a/shared/systemd/src/basic/tmpfile-util.c b/shared/systemd/src/basic/tmpfile-util.c
index 019121cb..c02ce3df 100644
--- a/shared/systemd/src/basic/tmpfile-util.c
+++ b/shared/systemd/src/basic/tmpfile-util.c
@@ -2,10 +2,12 @@
 
 #include "nm-sd-adapt-shared.h"
 
+#include <stdio.h>
 #include <sys/mman.h>
 
 #include "alloc-util.h"
 #include "fd-util.h"
+#include "fileio.h"
 #include "fs-util.h"
 #include "hexdecoct.h"
 #include "macro.h"
@@ -39,12 +41,15 @@ int fopen_temporary(const char *path, FILE **_f, char **_temp_path) {
                 return -errno;
         }
 
-        f = fdopen(fd, "w");
-        if (!f) {
-                unlink_noerrno(t);
+        /* This assumes that returned FILE object is short-lived and used within the same single-threaded
+         * context and never shared externally, hence locking is not necessary. */
+
+        r = fdopen_unlocked(fd, "w", &f);
+        if (r < 0) {
+                unlink(t);
                 free(t);
                 safe_close(fd);
-                return -errno;
+                return r;
         }
 
         *_f = f;
@@ -55,13 +60,11 @@ int fopen_temporary(const char *path, FILE **_f, char **_temp_path) {
 
 /* This is much like mkostemp() but is subject to umask(). */
 int mkostemp_safe(char *pattern) {
-        _cleanup_umask_ mode_t u = 0;
+        _unused_ _cleanup_umask_ mode_t u = umask(0077);
         int fd;
 
         assert(pattern);
 
-        u = umask(077);
-
         fd = mkostemp(pattern, O_CLOEXEC);
         if (fd < 0)
                 return -errno;
@@ -322,7 +325,7 @@ int mkdtemp_malloc(const char *template, char **ret) {
                 if (r < 0)
                         return r;
 
-                p = strjoin(tmp, "/XXXXXX");
+                p = path_join(tmp, "XXXXXX");
         }
         if (!p)
                 return -ENOMEM;
diff --git a/shared/systemd/src/basic/umask-util.h b/shared/systemd/src/basic/umask-util.h
index e964292e..cad74517 100644
--- a/shared/systemd/src/basic/umask-util.h
+++ b/shared/systemd/src/basic/umask-util.h
@@ -8,21 +8,19 @@
 #include "macro.h"
 
 static inline void umaskp(mode_t *u) {
-        umask(*u);
+        umask(*u & 0777);
 }
 
 #define _cleanup_umask_ _cleanup_(umaskp)
 
-struct _umask_struct_ {
-        mode_t mask;
-        bool quit;
-};
+/* We make use of the fact here that the umask() concept is using only the lower 9 bits of mode_t, although
+ * mode_t has space for the file type in the bits further up. We simply OR in the file type mask S_IFMT to
+ * distinguish the first and the second iteration of the RUN_WITH_UMASK() loop, so that we can run the first
+ * one, and exit on the second. */
 
-static inline void _reset_umask_(struct _umask_struct_ *s) {
-        umask(s->mask);
-};
+assert_cc((S_IFMT & 0777) == 0);
 
 #define RUN_WITH_UMASK(mask)                                            \
-        for (_cleanup_(_reset_umask_) struct _umask_struct_ _saved_umask_ = { umask(mask), false }; \
-             !_saved_umask_.quit ;                                      \
-             _saved_umask_.quit = true)
+        for (_cleanup_umask_ mode_t _saved_umask_ = umask(mask) | S_IFMT; \
+             FLAGS_SET(_saved_umask_, S_IFMT);                          \
+             _saved_umask_ &= 0777)
diff --git a/shared/systemd/src/basic/utf8.c b/shared/systemd/src/basic/utf8.c
index f1c6ac1f..3c51fa1f 100644
--- a/shared/systemd/src/basic/utf8.c
+++ b/shared/systemd/src/basic/utf8.c
@@ -34,6 +34,7 @@
 #include "gunicode.h"
 #include "hexdecoct.h"
 #include "macro.h"
+#include "string-util.h"
 #include "utf8.h"
 
 bool unichar_is_valid(char32_t ch) {
@@ -198,47 +199,94 @@ char *utf8_escape_invalid(const char *str) {
         }
 
         *s = '\0';
-
+        (void) str_realloc(&p);
         return p;
 }
 
 #if 0 /* NM_IGNORED */
-char *utf8_escape_non_printable(const char *str) {
-        char *p, *s;
+static int utf8_char_console_width(const char *str) {
+        char32_t c;
+        int r;
+
+        r = utf8_encoded_to_unichar(str, &c);
+        if (r < 0)
+                return r;
+
+        /* TODO: we should detect combining characters */
+
+        return unichar_iswide(c) ? 2 : 1;
+}
+
+char *utf8_escape_non_printable_full(const char *str, size_t console_width) {
+        char *p, *s, *prev_s;
+        size_t n = 0; /* estimated print width */
 
         assert(str);
 
-        p = s = malloc(strlen(str) * 4 + 1);
+        if (console_width == 0)
+                return strdup("");
+
+        p = s = prev_s = malloc(strlen(str) * 4 + 1);
         if (!p)
                 return NULL;
 
-        while (*str) {
+        for (;;) {
                 int len;
+                char *saved_s = s;
+
+                if (!*str) /* done! */
+                        goto finish;
 
                 len = utf8_encoded_valid_unichar(str, (size_t) -1);
                 if (len > 0) {
                         if (utf8_is_printable(str, len)) {
+                                int w;
+
+                                w = utf8_char_console_width(str);
+                                assert(w >= 0);
+                                if (n + w > console_width)
+                                        goto truncation;
+
                                 s = mempcpy(s, str, len);
                                 str += len;
+                                n += w;
+
                         } else {
-                                while (len > 0) {
+                                for (; len > 0; len--) {
+                                        if (n + 4 > console_width)
+                                                goto truncation;
+
                                         *(s++) = '\\';
                                         *(s++) = 'x';
                                         *(s++) = hexchar((int) *str >> 4);
                                         *(s++) = hexchar((int) *str);
 
                                         str += 1;
-                                        len--;
+                                        n += 4;
                                 }
                         }
                 } else {
-                        s = stpcpy(s, UTF8_REPLACEMENT_CHARACTER);
+                        if (n + 1 > console_width)
+                                goto truncation;
+
+                        s = mempcpy(s, UTF8_REPLACEMENT_CHARACTER, strlen(UTF8_REPLACEMENT_CHARACTER));
                         str += 1;
+                        n += 1;
                 }
+
+                prev_s = saved_s;
         }
 
-        *s = '\0';
+ truncation:
+        /* Try to go back one if we don't have enough space for the ellipsis */
+        if (n + 1 >= console_width)
+                s = prev_s;
+
+        s = mempcpy(s, "…", strlen("…"));
 
+ finish:
+        *s = '\0';
+        (void) str_realloc(&p);
         return p;
 }
 #endif /* NM_IGNORED */
@@ -532,15 +580,15 @@ size_t utf8_console_width(const char *str) {
         /* Returns the approximate width a string will take on screen when printed on a character cell
          * terminal/console. */
 
-        while (*str != 0) {
-                char32_t c;
+        while (*str) {
+                int w;
 
-                if (utf8_encoded_to_unichar(str, &c) < 0)
+                w = utf8_char_console_width(str);
+                if (w < 0)
                         return (size_t) -1;
 
+                n += w;
                 str = utf8_next_char(str);
-
-                n += unichar_iswide(c) ? 2 : 1;
         }
 
         return n;
diff --git a/shared/systemd/src/basic/utf8.h b/shared/systemd/src/basic/utf8.h
index 6df70921..62e99b72 100644
--- a/shared/systemd/src/basic/utf8.h
+++ b/shared/systemd/src/basic/utf8.h
@@ -22,7 +22,10 @@ bool utf8_is_printable_newline(const char* str, size_t length, bool newline) _pu
 #define utf8_is_printable(str, length) utf8_is_printable_newline(str, length, true)
 
 char *utf8_escape_invalid(const char *s);
-char *utf8_escape_non_printable(const char *str);
+char *utf8_escape_non_printable_full(const char *str, size_t console_width);
+static inline char *utf8_escape_non_printable(const char *str) {
+        return utf8_escape_non_printable_full(str, (size_t) -1);
+}
 
 size_t utf8_encode_unichar(char *out_utf8, char32_t g);
 size_t utf16_encode_unichar(char16_t *out, char32_t c);
diff --git a/shared/systemd/src/shared/dns-domain.c b/shared/systemd/src/shared/dns-domain.c
new file mode 100644
index 00000000..92543ebe
--- /dev/null
+++ b/shared/systemd/src/shared/dns-domain.c
@@ -0,0 +1,1387 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "nm-sd-adapt-shared.h"
+
+#if 0 /* NM_IGNORED */
+#if HAVE_LIBIDN2
+#  include <idn2.h>
+#elif HAVE_LIBIDN
+#  include <idna.h>
+#  include <stringprep.h>
+#endif
+#endif
+
+#include <endian.h>
+#include <netinet/in.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/socket.h>
+
+#include "alloc-util.h"
+#include "dns-domain.h"
+#include "hashmap.h"
+#include "hexdecoct.h"
+#include "hostname-util.h"
+#include "in-addr-util.h"
+#include "macro.h"
+#include "parse-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "utf8.h"
+
+int dns_label_unescape(const char **name, char *dest, size_t sz, DNSLabelFlags flags) {
+        const char *n;
+        char *d, last_char = 0;
+        int r = 0;
+
+        assert(name);
+        assert(*name);
+
+        n = *name;
+        d = dest;
+
+        for (;;) {
+                if (IN_SET(*n, 0, '.')) {
+                        if (FLAGS_SET(flags, DNS_LABEL_LDH) && last_char == '-')
+                                /* Trailing dash */
+                                return -EINVAL;
+
+                        if (*n == '.')
+                                n++;
+                        break;
+                }
+
+                if (r >= DNS_LABEL_MAX)
+                        return -EINVAL;
+
+                if (sz <= 0)
+                        return -ENOBUFS;
+
+                if (*n == '\\') {
+                        /* Escaped character */
+                        if (FLAGS_SET(flags, DNS_LABEL_NO_ESCAPES))
+                                return -EINVAL;
+
+                        n++;
+
+                        if (*n == 0)
+                                /* Ending NUL */
+                                return -EINVAL;
+
+                        else if (IN_SET(*n, '\\', '.')) {
+                                /* Escaped backslash or dot */
+
+                                if (FLAGS_SET(flags, DNS_LABEL_LDH))
+                                        return -EINVAL;
+
+                                last_char = *n;
+                                if (d)
+                                        *(d++) = *n;
+                                sz--;
+                                r++;
+                                n++;
+
+                        } else if (n[0] >= '0' && n[0] <= '9') {
+                                unsigned k;
+
+                                /* Escaped literal ASCII character */
+
+                                if (!(n[1] >= '0' && n[1] <= '9') ||
+                                    !(n[2] >= '0' && n[2] <= '9'))
+                                        return -EINVAL;
+
+                                k = ((unsigned) (n[0] - '0') * 100) +
+                                        ((unsigned) (n[1] - '0') * 10) +
+                                        ((unsigned) (n[2] - '0'));
+
+                                /* Don't allow anything that doesn't
+                                 * fit in 8bit. Note that we do allow
+                                 * control characters, as some servers
+                                 * (e.g. cloudflare) are happy to
+                                 * generate labels with them
+                                 * inside. */
+                                if (k > 255)
+                                        return -EINVAL;
+
+                                if (FLAGS_SET(flags, DNS_LABEL_LDH) &&
+                                    !valid_ldh_char((char) k))
+                                        return -EINVAL;
+
+                                last_char = (char) k;
+                                if (d)
+                                        *(d++) = (char) k;
+                                sz--;
+                                r++;
+
+                                n += 3;
+                        } else
+                                return -EINVAL;
+
+                } else if ((uint8_t) *n >= (uint8_t) ' ' && *n != 127) {
+
+                        /* Normal character */
+
+                        if (FLAGS_SET(flags, DNS_LABEL_LDH)) {
+                                if (!valid_ldh_char(*n))
+                                        return -EINVAL;
+                                if (r == 0 && *n == '-')
+                                        /* Leading dash */
+                                        return -EINVAL;
+                        }
+
+                        last_char = *n;
+                        if (d)
+                                *(d++) = *n;
+                        sz--;
+                        r++;
+                        n++;
+                } else
+                        return -EINVAL;
+        }
+
+        /* Empty label that is not at the end? */
+        if (r == 0 && *n)
+                return -EINVAL;
+
+        /* More than one trailing dot? */
+        if (*n == '.')
+                return -EINVAL;
+
+        if (sz >= 1 && d)
+                *d = 0;
+
+        *name = n;
+        return r;
+}
+
+#if 0 /* NM_IGNORED */
+/* @label_terminal: terminal character of a label, updated to point to the terminal character of
+ *                  the previous label (always skipping one dot) or to NULL if there are no more
+ *                  labels. */
+int dns_label_unescape_suffix(const char *name, const char **label_terminal, char *dest, size_t sz) {
+        const char *terminal;
+        int r;
+
+        assert(name);
+        assert(label_terminal);
+        assert(dest);
+
+        /* no more labels */
+        if (!*label_terminal) {
+                if (sz >= 1)
+                        *dest = 0;
+
+                return 0;
+        }
+
+        terminal = *label_terminal;
+        assert(IN_SET(*terminal, 0, '.'));
+
+        /* Skip current terminal character (and accept domain names ending it ".") */
+        if (*terminal == 0)
+                terminal--;
+        if (terminal >= name && *terminal == '.')
+                terminal--;
+
+        /* Point name to the last label, and terminal to the preceding terminal symbol (or make it a NULL pointer) */
+        for (;;) {
+                if (terminal < name) {
+                        /* Reached the first label, so indicate that there are no more */
+                        terminal = NULL;
+                        break;
+                }
+
+                /* Find the start of the last label */
+                if (*terminal == '.') {
+                        const char *y;
+                        unsigned slashes = 0;
+
+                        for (y = terminal - 1; y >= name && *y == '\\'; y--)
+                                slashes++;
+
+                        if (slashes % 2 == 0) {
+                                /* The '.' was not escaped */
+                                name = terminal + 1;
+                                break;
+                        } else {
+                                terminal = y;
+                                continue;
+                        }
+                }
+
+                terminal--;
+        }
+
+        r = dns_label_unescape(&name, dest, sz, 0);
+        if (r < 0)
+                return r;
+
+        *label_terminal = terminal;
+
+        return r;
+}
+#endif /* NM_IGNORED */
+
+int dns_label_escape(const char *p, size_t l, char *dest, size_t sz) {
+        char *q;
+
+        /* DNS labels must be between 1 and 63 characters long. A
+         * zero-length label does not exist. See RFC 2182, Section
+         * 11. */
+
+        if (l <= 0 || l > DNS_LABEL_MAX)
+                return -EINVAL;
+        if (sz < 1)
+                return -ENOBUFS;
+
+        assert(p);
+        assert(dest);
+
+        q = dest;
+        while (l > 0) {
+
+                if (IN_SET(*p, '.', '\\')) {
+
+                        /* Dot or backslash */
+
+                        if (sz < 3)
+                                return -ENOBUFS;
+
+                        *(q++) = '\\';
+                        *(q++) = *p;
+
+                        sz -= 2;
+
+                } else if (IN_SET(*p, '_', '-') ||
+                           (*p >= '0' && *p <= '9') ||
+                           (*p >= 'a' && *p <= 'z') ||
+                           (*p >= 'A' && *p <= 'Z')) {
+
+                        /* Proper character */
+
+                        if (sz < 2)
+                                return -ENOBUFS;
+
+                        *(q++) = *p;
+                        sz -= 1;
+
+                } else {
+
+                        /* Everything else */
+
+                        if (sz < 5)
+                                return -ENOBUFS;
+
+                        *(q++) = '\\';
+                        *(q++) = '0' + (char) ((uint8_t) *p / 100);
+                        *(q++) = '0' + (char) (((uint8_t) *p / 10) % 10);
+                        *(q++) = '0' + (char) ((uint8_t) *p % 10);
+
+                        sz -= 4;
+                }
+
+                p++;
+                l--;
+        }
+
+        *q = 0;
+        return (int) (q - dest);
+}
+
+#if 0 /* NM_IGNORED */
+int dns_label_escape_new(const char *p, size_t l, char **ret) {
+        _cleanup_free_ char *s = NULL;
+        int r;
+
+        assert(p);
+        assert(ret);
+
+        if (l <= 0 || l > DNS_LABEL_MAX)
+                return -EINVAL;
+
+        s = new(char, DNS_LABEL_ESCAPED_MAX);
+        if (!s)
+                return -ENOMEM;
+
+        r = dns_label_escape(p, l, s, DNS_LABEL_ESCAPED_MAX);
+        if (r < 0)
+                return r;
+
+        *ret = TAKE_PTR(s);
+
+        return r;
+}
+
+#if HAVE_LIBIDN
+int dns_label_apply_idna(const char *encoded, size_t encoded_size, char *decoded, size_t decoded_max) {
+        _cleanup_free_ uint32_t *input = NULL;
+        size_t input_size, l;
+        const char *p;
+        bool contains_8bit = false;
+        char buffer[DNS_LABEL_MAX+1];
+
+        assert(encoded);
+        assert(decoded);
+
+        /* Converts an U-label into an A-label */
+
+        if (encoded_size <= 0)
+                return -EINVAL;
+
+        for (p = encoded; p < encoded + encoded_size; p++)
+                if ((uint8_t) *p > 127)
+                        contains_8bit = true;
+
+        if (!contains_8bit) {
+                if (encoded_size > DNS_LABEL_MAX)
+                        return -EINVAL;
+
+                return 0;
+        }
+
+        input = stringprep_utf8_to_ucs4(encoded, encoded_size, &input_size);
+        if (!input)
+                return -ENOMEM;
+
+        if (idna_to_ascii_4i(input, input_size, buffer, 0) != 0)
+                return -EINVAL;
+
+        l = strlen(buffer);
+
+        /* Verify that the result is not longer than one DNS label. */
+        if (l <= 0 || l > DNS_LABEL_MAX)
+                return -EINVAL;
+        if (l > decoded_max)
+                return -ENOBUFS;
+
+        memcpy(decoded, buffer, l);
+
+        /* If there's room, append a trailing NUL byte, but only then */
+        if (decoded_max > l)
+                decoded[l] = 0;
+
+        return (int) l;
+}
+
+int dns_label_undo_idna(const char *encoded, size_t encoded_size, char *decoded, size_t decoded_max) {
+        size_t input_size, output_size;
+        _cleanup_free_ uint32_t *input = NULL;
+        _cleanup_free_ char *result = NULL;
+        uint32_t *output = NULL;
+        size_t w;
+
+        /* To be invoked after unescaping. Converts an A-label into an U-label. */
+
+        assert(encoded);
+        assert(decoded);
+
+        if (encoded_size <= 0 || encoded_size > DNS_LABEL_MAX)
+                return -EINVAL;
+
+        if (!memory_startswith(encoded, encoded_size, IDNA_ACE_PREFIX))
+                return 0;
+
+        input = stringprep_utf8_to_ucs4(encoded, encoded_size, &input_size);
+        if (!input)
+                return -ENOMEM;
+
+        output_size = input_size;
+        output = newa(uint32_t, output_size);
+
+        idna_to_unicode_44i(input, input_size, output, &output_size, 0);
+
+        result = stringprep_ucs4_to_utf8(output, output_size, NULL, &w);
+        if (!result)
+                return -ENOMEM;
+        if (w <= 0)
+                return -EINVAL;
+        if (w > decoded_max)
+                return -ENOBUFS;
+
+        memcpy(decoded, result, w);
+
+        /* Append trailing NUL byte if there's space, but only then. */
+        if (decoded_max > w)
+                decoded[w] = 0;
+
+        return w;
+}
+#endif
+#endif /* NM_IGNORED */
+
+int dns_name_concat(const char *a, const char *b, DNSLabelFlags flags, char **_ret) {
+        _cleanup_free_ char *ret = NULL;
+        size_t n = 0, allocated = 0;
+        const char *p;
+        bool first = true;
+        int r;
+
+        if (a)
+                p = a;
+        else if (b)
+                p = TAKE_PTR(b);
+        else
+                goto finish;
+
+        for (;;) {
+                char label[DNS_LABEL_MAX];
+
+                r = dns_label_unescape(&p, label, sizeof label, flags);
+                if (r < 0)
+                        return r;
+                if (r == 0) {
+                        if (*p != 0)
+                                return -EINVAL;
+
+                        if (b) {
+                                /* Now continue with the second string, if there is one */
+                                p = TAKE_PTR(b);
+                                continue;
+                        }
+
+                        break;
+                }
+
+                if (_ret) {
+                        if (!GREEDY_REALLOC(ret, allocated, n + !first + DNS_LABEL_ESCAPED_MAX))
+                                return -ENOMEM;
+
+                        r = dns_label_escape(label, r, ret + n + !first, DNS_LABEL_ESCAPED_MAX);
+                        if (r < 0)
+                                return r;
+
+                        if (!first)
+                                ret[n] = '.';
+                } else {
+                        char escaped[DNS_LABEL_ESCAPED_MAX];
+
+                        r = dns_label_escape(label, r, escaped, sizeof(escaped));
+                        if (r < 0)
+                                return r;
+                }
+
+                if (!first)
+                        n++;
+                else
+                        first = false;
+
+                n += r;
+        }
+
+finish:
+        if (n > DNS_HOSTNAME_MAX)
+                return -EINVAL;
+
+        if (_ret) {
+                if (n == 0) {
+                        /* Nothing appended? If so, generate at least a single dot, to indicate the DNS root domain */
+                        if (!GREEDY_REALLOC(ret, allocated, 2))
+                                return -ENOMEM;
+
+                        ret[n++] = '.';
+                } else {
+                        if (!GREEDY_REALLOC(ret, allocated, n + 1))
+                                return -ENOMEM;
+                }
+
+                ret[n] = 0;
+                *_ret = TAKE_PTR(ret);
+        }
+
+        return 0;
+}
+
+#if 0 /* NM_IGNORED */
+void dns_name_hash_func(const char *p, struct siphash *state) {
+        int r;
+
+        assert(p);
+
+        for (;;) {
+                char label[DNS_LABEL_MAX+1];
+
+                r = dns_label_unescape(&p, label, sizeof label, 0);
+                if (r < 0)
+                        break;
+                if (r == 0)
+                        break;
+
+                ascii_strlower_n(label, r);
+                siphash24_compress(label, r, state);
+                siphash24_compress_byte(0, state); /* make sure foobar and foo.bar result in different hashes */
+        }
+
+        /* enforce that all names are terminated by the empty label */
+        string_hash_func("", state);
+}
+
+int dns_name_compare_func(const char *a, const char *b) {
+        const char *x, *y;
+        int r, q;
+
+        assert(a);
+        assert(b);
+
+        x = a + strlen(a);
+        y = b + strlen(b);
+
+        for (;;) {
+                char la[DNS_LABEL_MAX], lb[DNS_LABEL_MAX];
+
+                if (x == NULL && y == NULL)
+                        return 0;
+
+                r = dns_label_unescape_suffix(a, &x, la, sizeof(la));
+                q = dns_label_unescape_suffix(b, &y, lb, sizeof(lb));
+                if (r < 0 || q < 0)
+                        return CMP(r, q);
+
+                r = ascii_strcasecmp_nn(la, r, lb, q);
+                if (r != 0)
+                        return r;
+        }
+}
+
+DEFINE_HASH_OPS(dns_name_hash_ops, char, dns_name_hash_func, dns_name_compare_func);
+
+int dns_name_equal(const char *x, const char *y) {
+        int r, q;
+
+        assert(x);
+        assert(y);
+
+        for (;;) {
+                char la[DNS_LABEL_MAX], lb[DNS_LABEL_MAX];
+
+                r = dns_label_unescape(&x, la, sizeof la, 0);
+                if (r < 0)
+                        return r;
+
+                q = dns_label_unescape(&y, lb, sizeof lb, 0);
+                if (q < 0)
+                        return q;
+
+                if (r != q)
+                        return false;
+                if (r == 0)
+                        return true;
+
+                if (ascii_strcasecmp_n(la, lb, r) != 0)
+                        return false;
+        }
+}
+
+int dns_name_endswith(const char *name, const char *suffix) {
+        const char *n, *s, *saved_n = NULL;
+        int r, q;
+
+        assert(name);
+        assert(suffix);
+
+        n = name;
+        s = suffix;
+
+        for (;;) {
+                char ln[DNS_LABEL_MAX], ls[DNS_LABEL_MAX];
+
+                r = dns_label_unescape(&n, ln, sizeof ln, 0);
+                if (r < 0)
+                        return r;
+
+                if (!saved_n)
+                        saved_n = n;
+
+                q = dns_label_unescape(&s, ls, sizeof ls, 0);
+                if (q < 0)
+                        return q;
+
+                if (r == 0 && q == 0)
+                        return true;
+                if (r == 0 && saved_n == n)
+                        return false;
+
+                if (r != q || ascii_strcasecmp_n(ln, ls, r) != 0) {
+
+                        /* Not the same, let's jump back, and try with the next label again */
+                        s = suffix;
+                        n = TAKE_PTR(saved_n);
+                }
+        }
+}
+
+int dns_name_startswith(const char *name, const char *prefix) {
+        const char *n, *p;
+        int r, q;
+
+        assert(name);
+        assert(prefix);
+
+        n = name;
+        p = prefix;
+
+        for (;;) {
+                char ln[DNS_LABEL_MAX], lp[DNS_LABEL_MAX];
+
+                r = dns_label_unescape(&p, lp, sizeof lp, 0);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        return true;
+
+                q = dns_label_unescape(&n, ln, sizeof ln, 0);
+                if (q < 0)
+                        return q;
+
+                if (r != q)
+                        return false;
+                if (ascii_strcasecmp_n(ln, lp, r) != 0)
+                        return false;
+        }
+}
+
+int dns_name_change_suffix(const char *name, const char *old_suffix, const char *new_suffix, char **ret) {
+        const char *n, *s, *saved_before = NULL, *saved_after = NULL, *prefix;
+        int r, q;
+
+        assert(name);
+        assert(old_suffix);
+        assert(new_suffix);
+        assert(ret);
+
+        n = name;
+        s = old_suffix;
+
+        for (;;) {
+                char ln[DNS_LABEL_MAX], ls[DNS_LABEL_MAX];
+
+                if (!saved_before)
+                        saved_before = n;
+
+                r = dns_label_unescape(&n, ln, sizeof ln, 0);
+                if (r < 0)
+                        return r;
+
+                if (!saved_after)
+                        saved_after = n;
+
+                q = dns_label_unescape(&s, ls, sizeof ls, 0);
+                if (q < 0)
+                        return q;
+
+                if (r == 0 && q == 0)
+                        break;
+                if (r == 0 && saved_after == n) {
+                        *ret = NULL; /* doesn't match */
+                        return 0;
+                }
+
+                if (r != q || ascii_strcasecmp_n(ln, ls, r) != 0) {
+
+                        /* Not the same, let's jump back, and try with the next label again */
+                        s = old_suffix;
+                        n = TAKE_PTR(saved_after);
+                        saved_before = NULL;
+                }
+        }
+
+        /* Found it! Now generate the new name */
+        prefix = strndupa(name, saved_before - name);
+
+        r = dns_name_concat(prefix, new_suffix, 0, ret);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+int dns_name_between(const char *a, const char *b, const char *c) {
+        /* Determine if b is strictly greater than a and strictly smaller than c.
+           We consider the order of names to be circular, so that if a is
+           strictly greater than c, we consider b to be between them if it is
+           either greater than a or smaller than c. This is how the canonical
+           DNS name order used in NSEC records work. */
+
+        if (dns_name_compare_func(a, c) < 0)
+                /*
+                   a and c are properly ordered:
+                   a<---b--->c
+                */
+                return dns_name_compare_func(a, b) < 0 &&
+                       dns_name_compare_func(b, c) < 0;
+        else
+                /*
+                   a and c are equal or 'reversed':
+                   <--b--c         a----->
+                   or:
+                   <-----c         a--b-->
+                */
+                return dns_name_compare_func(b, c) < 0 ||
+                       dns_name_compare_func(a, b) < 0;
+}
+
+int dns_name_reverse(int family, const union in_addr_union *a, char **ret) {
+        const uint8_t *p;
+        int r;
+
+        assert(a);
+        assert(ret);
+
+        p = (const uint8_t*) a;
+
+        if (family == AF_INET)
+                r = asprintf(ret, "%u.%u.%u.%u.in-addr.arpa", p[3], p[2], p[1], p[0]);
+        else if (family == AF_INET6)
+                r = asprintf(ret, "%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.%c.ip6.arpa",
+                             hexchar(p[15] & 0xF), hexchar(p[15] >> 4), hexchar(p[14] & 0xF), hexchar(p[14] >> 4),
+                             hexchar(p[13] & 0xF), hexchar(p[13] >> 4), hexchar(p[12] & 0xF), hexchar(p[12] >> 4),
+                             hexchar(p[11] & 0xF), hexchar(p[11] >> 4), hexchar(p[10] & 0xF), hexchar(p[10] >> 4),
+                             hexchar(p[ 9] & 0xF), hexchar(p[ 9] >> 4), hexchar(p[ 8] & 0xF), hexchar(p[ 8] >> 4),
+                             hexchar(p[ 7] & 0xF), hexchar(p[ 7] >> 4), hexchar(p[ 6] & 0xF), hexchar(p[ 6] >> 4),
+                             hexchar(p[ 5] & 0xF), hexchar(p[ 5] >> 4), hexchar(p[ 4] & 0xF), hexchar(p[ 4] >> 4),
+                             hexchar(p[ 3] & 0xF), hexchar(p[ 3] >> 4), hexchar(p[ 2] & 0xF), hexchar(p[ 2] >> 4),
+                             hexchar(p[ 1] & 0xF), hexchar(p[ 1] >> 4), hexchar(p[ 0] & 0xF), hexchar(p[ 0] >> 4));
+        else
+                return -EAFNOSUPPORT;
+        if (r < 0)
+                return -ENOMEM;
+
+        return 0;
+}
+
+int dns_name_address(const char *p, int *family, union in_addr_union *address) {
+        int r;
+
+        assert(p);
+        assert(family);
+        assert(address);
+
+        r = dns_name_endswith(p, "in-addr.arpa");
+        if (r < 0)
+                return r;
+        if (r > 0) {
+                uint8_t a[4];
+                unsigned i;
+
+                for (i = 0; i < ELEMENTSOF(a); i++) {
+                        char label[DNS_LABEL_MAX+1];
+
+                        r = dns_label_unescape(&p, label, sizeof label, 0);
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                return -EINVAL;
+                        if (r > 3)
+                                return -EINVAL;
+
+                        r = safe_atou8(label, &a[i]);
+                        if (r < 0)
+                                return r;
+                }
+
+                r = dns_name_equal(p, "in-addr.arpa");
+                if (r <= 0)
+                        return r;
+
+                *family = AF_INET;
+                address->in.s_addr = htobe32(((uint32_t) a[3] << 24) |
+                                             ((uint32_t) a[2] << 16) |
+                                             ((uint32_t) a[1] << 8) |
+                                              (uint32_t) a[0]);
+
+                return 1;
+        }
+
+        r = dns_name_endswith(p, "ip6.arpa");
+        if (r < 0)
+                return r;
+        if (r > 0) {
+                struct in6_addr a;
+                unsigned i;
+
+                for (i = 0; i < ELEMENTSOF(a.s6_addr); i++) {
+                        char label[DNS_LABEL_MAX+1];
+                        int x, y;
+
+                        r = dns_label_unescape(&p, label, sizeof label, 0);
+                        if (r <= 0)
+                                return r;
+                        if (r != 1)
+                                return -EINVAL;
+                        x = unhexchar(label[0]);
+                        if (x < 0)
+                                return -EINVAL;
+
+                        r = dns_label_unescape(&p, label, sizeof label, 0);
+                        if (r <= 0)
+                                return r;
+                        if (r != 1)
+                                return -EINVAL;
+                        y = unhexchar(label[0]);
+                        if (y < 0)
+                                return -EINVAL;
+
+                        a.s6_addr[ELEMENTSOF(a.s6_addr) - i - 1] = (uint8_t) y << 4 | (uint8_t) x;
+                }
+
+                r = dns_name_equal(p, "ip6.arpa");
+                if (r <= 0)
+                        return r;
+
+                *family = AF_INET6;
+                address->in6 = a;
+                return 1;
+        }
+
+        return 0;
+}
+#endif /* NM_IGNORED */
+
+bool dns_name_is_root(const char *name) {
+
+        assert(name);
+
+        /* There are exactly two ways to encode the root domain name:
+         * as empty string, or with a single dot. */
+
+        return STR_IN_SET(name, "", ".");
+}
+
+bool dns_name_is_single_label(const char *name) {
+        int r;
+
+        assert(name);
+
+        r = dns_name_parent(&name);
+        if (r <= 0)
+                return false;
+
+        return dns_name_is_root(name);
+}
+
+/* Encode a domain name according to RFC 1035 Section 3.1, without compression */
+int dns_name_to_wire_format(const char *domain, uint8_t *buffer, size_t len, bool canonical) {
+        uint8_t *label_length, *out;
+        int r;
+
+        assert(domain);
+        assert(buffer);
+
+        out = buffer;
+
+        do {
+                /* Reserve a byte for label length */
+                if (len <= 0)
+                        return -ENOBUFS;
+                len--;
+                label_length = out;
+                out++;
+
+                /* Convert and copy a single label. Note that
+                 * dns_label_unescape() returns 0 when it hits the end
+                 * of the domain name, which we rely on here to encode
+                 * the trailing NUL byte. */
+                r = dns_label_unescape(&domain, (char *) out, len, 0);
+                if (r < 0)
+                        return r;
+
+                /* Optionally, output the name in DNSSEC canonical
+                 * format, as described in RFC 4034, section 6.2. Or
+                 * in other words: in lower-case. */
+                if (canonical)
+                        ascii_strlower_n((char*) out, (size_t) r);
+
+                /* Fill label length, move forward */
+                *label_length = r;
+                out += r;
+                len -= r;
+
+        } while (r != 0);
+
+        /* Verify the maximum size of the encoded name. The trailing
+         * dot + NUL byte account are included this time, hence
+         * compare against DNS_HOSTNAME_MAX + 2 (which is 255) this
+         * time. */
+        if (out - buffer > DNS_HOSTNAME_MAX + 2)
+                return -EINVAL;
+
+        return out - buffer;
+}
+
+#if 0 /* NM_IGNORED */
+static bool srv_type_label_is_valid(const char *label, size_t n) {
+        size_t k;
+
+        assert(label);
+
+        if (n < 2) /* Label needs to be at least 2 chars long */
+                return false;
+
+        if (label[0] != '_') /* First label char needs to be underscore */
+                return false;
+
+        /* Second char must be a letter */
+        if (!(label[1] >= 'A' && label[1] <= 'Z') &&
+            !(label[1] >= 'a' && label[1] <= 'z'))
+                return false;
+
+        /* Third and further chars must be alphanumeric or a hyphen */
+        for (k = 2; k < n; k++) {
+                if (!(label[k] >= 'A' && label[k] <= 'Z') &&
+                    !(label[k] >= 'a' && label[k] <= 'z') &&
+                    !(label[k] >= '0' && label[k] <= '9') &&
+                    label[k] != '-')
+                        return false;
+        }
+
+        return true;
+}
+
+bool dns_srv_type_is_valid(const char *name) {
+        unsigned c = 0;
+        int r;
+
+        if (!name)
+                return false;
+
+        for (;;) {
+                char label[DNS_LABEL_MAX];
+
+                /* This more or less implements RFC 6335, Section 5.1 */
+
+                r = dns_label_unescape(&name, label, sizeof label, 0);
+                if (r < 0)
+                        return false;
+                if (r == 0)
+                        break;
+
+                if (c >= 2)
+                        return false;
+
+                if (!srv_type_label_is_valid(label, r))
+                        return false;
+
+                c++;
+        }
+
+        return c == 2; /* exactly two labels */
+}
+
+bool dnssd_srv_type_is_valid(const char *name) {
+        return dns_srv_type_is_valid(name) &&
+                ((dns_name_endswith(name, "_tcp") > 0) ||
+                 (dns_name_endswith(name, "_udp") > 0)); /* Specific to DNS-SD. RFC 6763, Section 7 */
+}
+
+bool dns_service_name_is_valid(const char *name) {
+        size_t l;
+
+        /* This more or less implements RFC 6763, Section 4.1.1 */
+
+        if (!name)
+                return false;
+
+        if (!utf8_is_valid(name))
+                return false;
+
+        if (string_has_cc(name, NULL))
+                return false;
+
+        l = strlen(name);
+        if (l <= 0)
+                return false;
+        if (l > 63)
+                return false;
+
+        return true;
+}
+
+int dns_service_join(const char *name, const char *type, const char *domain, char **ret) {
+        char escaped[DNS_LABEL_ESCAPED_MAX];
+        _cleanup_free_ char *n = NULL;
+        int r;
+
+        assert(type);
+        assert(domain);
+        assert(ret);
+
+        if (!dns_srv_type_is_valid(type))
+                return -EINVAL;
+
+        if (!name)
+                return dns_name_concat(type, domain, 0, ret);
+
+        if (!dns_service_name_is_valid(name))
+                return -EINVAL;
+
+        r = dns_label_escape(name, strlen(name), escaped, sizeof(escaped));
+        if (r < 0)
+                return r;
+
+        r = dns_name_concat(type, domain, 0, &n);
+        if (r < 0)
+                return r;
+
+        return dns_name_concat(escaped, n, 0, ret);
+}
+
+static bool dns_service_name_label_is_valid(const char *label, size_t n) {
+        char *s;
+
+        assert(label);
+
+        if (memchr(label, 0, n))
+                return false;
+
+        s = strndupa(label, n);
+        return dns_service_name_is_valid(s);
+}
+
+int dns_service_split(const char *joined, char **_name, char **_type, char **_domain) {
+        _cleanup_free_ char *name = NULL, *type = NULL, *domain = NULL;
+        const char *p = joined, *q = NULL, *d = NULL;
+        char a[DNS_LABEL_MAX], b[DNS_LABEL_MAX], c[DNS_LABEL_MAX];
+        int an, bn, cn, r;
+        unsigned x = 0;
+
+        assert(joined);
+
+        /* Get first label from the full name */
+        an = dns_label_unescape(&p, a, sizeof(a), 0);
+        if (an < 0)
+                return an;
+
+        if (an > 0) {
+                x++;
+
+                /* If there was a first label, try to get the second one */
+                bn = dns_label_unescape(&p, b, sizeof(b), 0);
+                if (bn < 0)
+                        return bn;
+
+                if (bn > 0) {
+                        x++;
+
+                        /* If there was a second label, try to get the third one */
+                        q = p;
+                        cn = dns_label_unescape(&p, c, sizeof(c), 0);
+                        if (cn < 0)
+                                return cn;
+
+                        if (cn > 0)
+                                x++;
+                } else
+                        cn = 0;
+        } else
+                an = 0;
+
+        if (x >= 2 && srv_type_label_is_valid(b, bn)) {
+
+                if (x >= 3 && srv_type_label_is_valid(c, cn)) {
+
+                        if (dns_service_name_label_is_valid(a, an)) {
+                                /* OK, got <name> . <type> . <type2> . <domain> */
+
+                                name = strndup(a, an);
+                                if (!name)
+                                        return -ENOMEM;
+
+                                type = strjoin(b, ".", c);
+                                if (!type)
+                                        return -ENOMEM;
+
+                                d = p;
+                                goto finish;
+                        }
+
+                } else if (srv_type_label_is_valid(a, an)) {
+
+                        /* OK, got <type> . <type2> . <domain> */
+
+                        name = NULL;
+
+                        type = strjoin(a, ".", b);
+                        if (!type)
+                                return -ENOMEM;
+
+                        d = q;
+                        goto finish;
+                }
+        }
+
+        name = NULL;
+        type = NULL;
+        d = joined;
+
+finish:
+        r = dns_name_normalize(d, 0, &domain);
+        if (r < 0)
+                return r;
+
+        if (_domain)
+                *_domain = TAKE_PTR(domain);
+
+        if (_type)
+                *_type = TAKE_PTR(type);
+
+        if (_name)
+                *_name = TAKE_PTR(name);
+
+        return 0;
+}
+
+static int dns_name_build_suffix_table(const char *name, const char *table[]) {
+        const char *p;
+        unsigned n = 0;
+        int r;
+
+        assert(name);
+        assert(table);
+
+        p = name;
+        for (;;) {
+                if (n > DNS_N_LABELS_MAX)
+                        return -EINVAL;
+
+                table[n] = p;
+                r = dns_name_parent(&p);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        break;
+
+                n++;
+        }
+
+        return (int) n;
+}
+
+int dns_name_suffix(const char *name, unsigned n_labels, const char **ret) {
+        const char* labels[DNS_N_LABELS_MAX+1];
+        int n;
+
+        assert(name);
+        assert(ret);
+
+        n = dns_name_build_suffix_table(name, labels);
+        if (n < 0)
+                return n;
+
+        if ((unsigned) n < n_labels)
+                return -EINVAL;
+
+        *ret = labels[n - n_labels];
+        return (int) (n - n_labels);
+}
+
+int dns_name_skip(const char *a, unsigned n_labels, const char **ret) {
+        int r;
+
+        assert(a);
+        assert(ret);
+
+        for (; n_labels > 0; n_labels--) {
+                r = dns_name_parent(&a);
+                if (r < 0)
+                        return r;
+                if (r == 0) {
+                        *ret = "";
+                        return 0;
+                }
+        }
+
+        *ret = a;
+        return 1;
+}
+
+int dns_name_count_labels(const char *name) {
+        unsigned n = 0;
+        const char *p;
+        int r;
+
+        assert(name);
+
+        p = name;
+        for (;;) {
+                r = dns_name_parent(&p);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        break;
+
+                if (n >= DNS_N_LABELS_MAX)
+                        return -EINVAL;
+
+                n++;
+        }
+
+        return (int) n;
+}
+
+int dns_name_equal_skip(const char *a, unsigned n_labels, const char *b) {
+        int r;
+
+        assert(a);
+        assert(b);
+
+        r = dns_name_skip(a, n_labels, &a);
+        if (r <= 0)
+                return r;
+
+        return dns_name_equal(a, b);
+}
+
+int dns_name_common_suffix(const char *a, const char *b, const char **ret) {
+        const char *a_labels[DNS_N_LABELS_MAX+1], *b_labels[DNS_N_LABELS_MAX+1];
+        int n = 0, m = 0, k = 0, r, q;
+
+        assert(a);
+        assert(b);
+        assert(ret);
+
+        /* Determines the common suffix of domain names a and b */
+
+        n = dns_name_build_suffix_table(a, a_labels);
+        if (n < 0)
+                return n;
+
+        m = dns_name_build_suffix_table(b, b_labels);
+        if (m < 0)
+                return m;
+
+        for (;;) {
+                char la[DNS_LABEL_MAX], lb[DNS_LABEL_MAX];
+                const char *x, *y;
+
+                if (k >= n || k >= m) {
+                        *ret = a_labels[n - k];
+                        return 0;
+                }
+
+                x = a_labels[n - 1 - k];
+                r = dns_label_unescape(&x, la, sizeof la, 0);
+                if (r < 0)
+                        return r;
+
+                y = b_labels[m - 1 - k];
+                q = dns_label_unescape(&y, lb, sizeof lb, 0);
+                if (q < 0)
+                        return q;
+
+                if (r != q || ascii_strcasecmp_n(la, lb, r) != 0) {
+                        *ret = a_labels[n - k];
+                        return 0;
+                }
+
+                k++;
+        }
+}
+
+int dns_name_apply_idna(const char *name, char **ret) {
+        /* Return negative on error, 0 if not implemented, positive on success. */
+
+#if HAVE_LIBIDN2
+        int r;
+        _cleanup_free_ char *t = NULL;
+
+        assert(name);
+        assert(ret);
+
+        r = idn2_lookup_u8((uint8_t*) name, (uint8_t**) &t,
+                           IDN2_NFC_INPUT | IDN2_NONTRANSITIONAL);
+        log_debug("idn2_lookup_u8: %s → %s", name, t);
+        if (r == IDN2_OK) {
+                if (!startswith(name, "xn--")) {
+                        _cleanup_free_ char *s = NULL;
+
+                        r = idn2_to_unicode_8z8z(t, &s, 0);
+                        if (r != IDN2_OK) {
+                                log_debug("idn2_to_unicode_8z8z(\"%s\") failed: %d/%s",
+                                          t, r, idn2_strerror(r));
+                                return 0;
+                        }
+
+                        if (!streq_ptr(name, s)) {
+                                log_debug("idn2 roundtrip failed: \"%s\" → \"%s\" → \"%s\", ignoring.",
+                                          name, t, s);
+                                return 0;
+                        }
+                }
+
+                *ret = TAKE_PTR(t);
+
+                return 1; /* *ret has been written */
+        }
+
+        log_debug("idn2_lookup_u8(\"%s\") failed: %d/%s", name, r, idn2_strerror(r));
+        if (r == IDN2_2HYPHEN)
+                /* The name has two hyphens — forbidden by IDNA2008 in some cases */
+                return 0;
+        if (IN_SET(r, IDN2_TOO_BIG_DOMAIN, IDN2_TOO_BIG_LABEL))
+                return -ENOSPC;
+        return -EINVAL;
+#elif HAVE_LIBIDN
+        _cleanup_free_ char *buf = NULL;
+        size_t n = 0, allocated = 0;
+        bool first = true;
+        int r, q;
+
+        assert(name);
+        assert(ret);
+
+        for (;;) {
+                char label[DNS_LABEL_MAX];
+
+                r = dns_label_unescape(&name, label, sizeof label, 0);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        break;
+
+                q = dns_label_apply_idna(label, r, label, sizeof label);
+                if (q < 0)
+                        return q;
+                if (q > 0)
+                        r = q;
+
+                if (!GREEDY_REALLOC(buf, allocated, n + !first + DNS_LABEL_ESCAPED_MAX))
+                        return -ENOMEM;
+
+                r = dns_label_escape(label, r, buf + n + !first, DNS_LABEL_ESCAPED_MAX);
+                if (r < 0)
+                        return r;
+
+                if (first)
+                        first = false;
+                else
+                        buf[n++] = '.';
+
+                n += r;
+        }
+
+        if (n > DNS_HOSTNAME_MAX)
+                return -EINVAL;
+
+        if (!GREEDY_REALLOC(buf, allocated, n + 1))
+                return -ENOMEM;
+
+        buf[n] = 0;
+        *ret = TAKE_PTR(buf);
+
+        return 1;
+#else
+        return 0;
+#endif
+}
+
+int dns_name_is_valid_or_address(const char *name) {
+        /* Returns > 0 if the specified name is either a valid IP address formatted as string or a valid DNS name */
+
+        if (isempty(name))
+                return 0;
+
+        if (in_addr_from_string_auto(name, NULL, NULL) >= 0)
+                return 1;
+
+        return dns_name_is_valid(name);
+}
+#endif /* NM_IGNORED */
diff --git a/shared/systemd/src/shared/dns-domain.h b/shared/systemd/src/shared/dns-domain.h
new file mode 100644
index 00000000..88b3eb11
--- /dev/null
+++ b/shared/systemd/src/shared/dns-domain.h
@@ -0,0 +1,114 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include <errno.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "hashmap.h"
+#include "in-addr-util.h"
+
+/* Length of a single label, with all escaping removed, excluding any trailing dot or NUL byte */
+#define DNS_LABEL_MAX 63
+
+/* Worst case length of a single label, with all escaping applied and room for a trailing NUL byte. */
+#define DNS_LABEL_ESCAPED_MAX (DNS_LABEL_MAX*4+1)
+
+/* Maximum length of a full hostname, consisting of a series of unescaped labels, and no trailing dot or NUL byte */
+#define DNS_HOSTNAME_MAX 253
+
+/* Maximum length of a full hostname, on the wire, including the final NUL byte */
+#define DNS_WIRE_FORMAT_HOSTNAME_MAX 255
+
+/* Maximum number of labels per valid hostname */
+#define DNS_N_LABELS_MAX 127
+
+typedef enum DNSLabelFlags {
+        DNS_LABEL_LDH        = 1 << 0, /* Follow the "LDH" rule — only letters, digits, and internal hyphens. */
+        DNS_LABEL_NO_ESCAPES = 1 << 1, /* Do not treat backslashes specially */
+} DNSLabelFlags;
+
+int dns_label_unescape(const char **name, char *dest, size_t sz, DNSLabelFlags flags);
+int dns_label_unescape_suffix(const char *name, const char **label_end, char *dest, size_t sz);
+int dns_label_escape(const char *p, size_t l, char *dest, size_t sz);
+int dns_label_escape_new(const char *p, size_t l, char **ret);
+
+static inline int dns_name_parent(const char **name) {
+        return dns_label_unescape(name, NULL, DNS_LABEL_MAX, 0);
+}
+
+#if 0 /* NM_IGNORED */
+#if HAVE_LIBIDN
+int dns_label_apply_idna(const char *encoded, size_t encoded_size, char *decoded, size_t decoded_max);
+int dns_label_undo_idna(const char *encoded, size_t encoded_size, char *decoded, size_t decoded_max);
+#endif
+#endif /* NM_IGNORED */
+
+int dns_name_concat(const char *a, const char *b, DNSLabelFlags flags, char **ret);
+
+static inline int dns_name_normalize(const char *s, DNSLabelFlags flags, char **ret) {
+        /* dns_name_concat() normalizes as a side-effect */
+        return dns_name_concat(s, NULL, flags, ret);
+}
+
+static inline int dns_name_is_valid(const char *s) {
+        int r;
+
+        /* dns_name_normalize() verifies as a side effect */
+        r = dns_name_normalize(s, 0, NULL);
+        if (r == -EINVAL)
+                return 0;
+        if (r < 0)
+                return r;
+        return 1;
+}
+
+static inline int dns_name_is_valid_ldh(const char *s) {
+        int r;
+
+        r = dns_name_concat(s, NULL, DNS_LABEL_LDH|DNS_LABEL_NO_ESCAPES, NULL);
+        if (r == -EINVAL)
+                return 0;
+        if (r < 0)
+                return r;
+        return 1;
+}
+
+void dns_name_hash_func(const char *s, struct siphash *state);
+int dns_name_compare_func(const char *a, const char *b);
+extern const struct hash_ops dns_name_hash_ops;
+
+int dns_name_between(const char *a, const char *b, const char *c);
+int dns_name_equal(const char *x, const char *y);
+int dns_name_endswith(const char *name, const char *suffix);
+int dns_name_startswith(const char *name, const char *prefix);
+
+int dns_name_change_suffix(const char *name, const char *old_suffix, const char *new_suffix, char **ret);
+
+int dns_name_reverse(int family, const union in_addr_union *a, char **ret);
+int dns_name_address(const char *p, int *family, union in_addr_union *a);
+
+bool dns_name_is_root(const char *name);
+bool dns_name_is_single_label(const char *name);
+
+int dns_name_to_wire_format(const char *domain, uint8_t *buffer, size_t len, bool canonical);
+
+bool dns_srv_type_is_valid(const char *name);
+bool dnssd_srv_type_is_valid(const char *name);
+bool dns_service_name_is_valid(const char *name);
+
+int dns_service_join(const char *name, const char *type, const char *domain, char **ret);
+int dns_service_split(const char *joined, char **name, char **type, char **domain);
+
+int dns_name_suffix(const char *name, unsigned n_labels, const char **ret);
+int dns_name_count_labels(const char *name);
+
+int dns_name_skip(const char *a, unsigned n_labels, const char **ret);
+int dns_name_equal_skip(const char *a, unsigned n_labels, const char *b);
+
+int dns_name_common_suffix(const char *a, const char *b, const char **ret);
+
+int dns_name_apply_idna(const char *name, char **ret);
+
+int dns_name_is_valid_or_address(const char *name);