about summary refs log tree commit diff
path: root/src/n-acd
diff options
context:
space:
mode:
Diffstat (limited to 'src/n-acd')
-rw-r--r--src/n-acd/.editorconfig11
-rw-r--r--src/n-acd/.github/workflows/ci.yml122
-rw-r--r--src/n-acd/.gitmodules12
-rw-r--r--src/n-acd/AUTHORS39
-rw-r--r--src/n-acd/NEWS.md46
-rw-r--r--src/n-acd/README.md60
-rw-r--r--src/n-acd/meson.build27
-rw-r--r--src/n-acd/meson_options.txt1
-rw-r--r--src/n-acd/src/libnacd.sym28
-rw-r--r--src/n-acd/src/meson.build95
-rw-r--r--src/n-acd/src/test-api.c88
-rw-r--r--src/n-acd/src/test-bpf.c226
-rw-r--r--src/n-acd/src/test-loopback.c82
-rw-r--r--src/n-acd/src/test-twice.c97
-rw-r--r--src/n-acd/src/test-unplug.c84
-rw-r--r--src/n-acd/src/test-unused.c63
-rw-r--r--src/n-acd/src/test-veth.c240
-rw-r--r--src/n-acd/src/test.h213
-rw-r--r--src/n-acd/src/util/test-timer.c177
l---------src/n-acd/subprojects/c-list1
l---------src/n-acd/subprojects/c-rbtree1
l---------src/n-acd/subprojects/c-siphash1
l---------src/n-acd/subprojects/libcstdaux-11
23 files changed, 1715 insertions, 0 deletions
diff --git a/src/n-acd/.editorconfig b/src/n-acd/.editorconfig
new file mode 100644
index 00000000..b10bb4f3
--- /dev/null
+++ b/src/n-acd/.editorconfig
@@ -0,0 +1,11 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+charset = utf-8
+
+[*.{c,h}]
+indent_style = space
+indent_size = 8
diff --git a/src/n-acd/.github/workflows/ci.yml b/src/n-acd/.github/workflows/ci.yml
new file mode 100644
index 00000000..22fc8141
--- /dev/null
+++ b/src/n-acd/.github/workflows/ci.yml
@@ -0,0 +1,122 @@
+name: Continuous Integration
+
+on:
+  push:
+  pull_request:
+  schedule:
+  - cron:  '0 0 * * *'
+
+jobs:
+  ci:
+    name: CI with Default Configuration
+    runs-on: ubuntu-latest
+
+    steps:
+    #
+    # Prepare CI
+    #
+    # We cannot use the github-action of the `ci-c-util` project, because we
+    # need privileges in the container. Therefore, fetch the CI sources and
+    # build the container manually.
+    #
+    - name: Fetch CI
+      uses: actions/checkout@v2
+      with:
+        repository: c-util/automation
+        ref: v1
+        path: automation
+    - name: Build CI
+      working-directory: automation/src/ci-c-util
+      run: docker build --tag ci-c-util:v1 .
+
+    #
+    # Run CI
+    #
+    # Take the CI image we built and run the CI with the default project
+    # configuration. We do not use valgrind, since it falls-over with bpf(2)
+    # syscalls.
+    #
+    - name: Fetch Sources
+      uses: actions/checkout@v2
+      with:
+        path: source
+    - name: Run through C-Util CI
+      run: |
+        docker run \
+                --privileged \
+                -v "$(pwd)/source:/github/workspace" \
+                "ci-c-util:v1" \
+                "--m32=1" \
+                "--source=/github/workspace"
+
+  ci-no-ebpf:
+    name: CI without eBPF
+    runs-on: ubuntu-latest
+
+    steps:
+    # See above in 'ci' job.
+    - name: Fetch CI
+      uses: actions/checkout@v2
+      with:
+        repository: c-util/automation
+        ref: v1
+        path: automation
+    - name: Build CI
+      working-directory: automation/src/ci-c-util
+      run: docker build --tag ci-c-util:v1 .
+
+    #
+    # Run CI
+    #
+    # This again runs the CI, but this time disables eBPF. We do support the
+    # legacy BPF fallback, so lets make sure we test for it.
+    #
+    - name: Fetch Sources
+      uses: actions/checkout@v2
+      with:
+        path: source
+    - name: Run through C-Util CI
+      run: |
+        docker run \
+                --privileged \
+                -v "$(pwd)/source:/github/workspace" \
+                "ci-c-util:v1" \
+                "--m32=1" \
+                "--mesonargs=-Debpf=false" \
+                "--source=/github/workspace"
+
+  ci-valgrind:
+    name: CI through Valgrind
+    runs-on: ubuntu-latest
+
+    steps:
+    # See above in 'ci' job.
+    - name: Fetch CI
+      uses: actions/checkout@v2
+      with:
+        repository: c-util/automation
+        ref: v1
+        path: automation
+    - name: Build CI
+      working-directory: automation/src/ci-c-util
+      run: docker build --tag ci-c-util:v1 .
+
+    #
+    # Run CI
+    #
+    # This again runs the CI, but this time through valgrind. Since some
+    # syscalls are not implemented on x86-64 32bit compat (e.g., bpf(2)), we
+    # disable the m32 mode.
+    #
+    - name: Fetch Sources
+      uses: actions/checkout@v2
+      with:
+        path: source
+    - name: Run through C-Util CI
+      run: |
+        docker run \
+                --privileged \
+                -v "$(pwd)/source:/github/workspace" \
+                "ci-c-util:v1" \
+                "--source=/github/workspace" \
+                "--valgrind=1"
diff --git a/src/n-acd/.gitmodules b/src/n-acd/.gitmodules
new file mode 100644
index 00000000..04829bdb
--- /dev/null
+++ b/src/n-acd/.gitmodules
@@ -0,0 +1,12 @@
+[submodule "subprojects/c-list"]
+	path = subprojects/c-list
+	url = https://github.com/c-util/c-list.git
+[submodule "subprojects/c-siphash"]
+	path = subprojects/c-siphash
+	url = https://github.com/c-util/c-siphash.git
+[submodule "subprojects/c-rbtree"]
+	path = subprojects/c-rbtree
+	url = https://github.com/c-util/c-rbtree.git
+[submodule "subprojects/c-stdaux"]
+	path = subprojects/c-stdaux
+	url = https://github.com/c-util/c-stdaux.git
diff --git a/src/n-acd/AUTHORS b/src/n-acd/AUTHORS
new file mode 100644
index 00000000..98ff1482
--- /dev/null
+++ b/src/n-acd/AUTHORS
@@ -0,0 +1,39 @@
+LICENSE:
+        This project is dual-licensed under both the Apache License, Version
+        2.0, and the GNU Lesser General Public License, Version 2.1+.
+
+AUTHORS-ASL:
+        Licensed under the Apache License, Version 2.0 (the "License");
+        you may not use this file except in compliance with the License.
+        You may obtain a copy of the License at
+
+                http://www.apache.org/licenses/LICENSE-2.0
+
+        Unless required by applicable law or agreed to in writing, software
+        distributed under the License is distributed on an "AS IS" BASIS,
+        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+        See the License for the specific language governing permissions and
+        limitations under the License.
+
+AUTHORS-LGPL:
+        This program is free software; you can redistribute it and/or modify it
+        under the terms of the GNU Lesser General Public License as published
+        by the Free Software Foundation; either version 2.1 of the License, or
+        (at your option) any later version.
+
+        This program is distributed in the hope that it will be useful, but
+        WITHOUT ANY WARRANTY; without even the implied warranty of
+        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+        Lesser General Public License for more details.
+
+        You should have received a copy of the GNU Lesser General Public License
+        along with this program; If not, see <http://www.gnu.org/licenses/>.
+
+COPYRIGHT: (ordered alphabetically)
+        Copyright (C) 2015-2019 Red Hat, Inc.
+
+AUTHORS: (ordered alphabetically)
+        Beniamino Galvani <bgalvani@redhat.com>
+        David Rheinsberg <david.rheinsberg@gmail.com>
+        Thomas Haller <thaller@redhat.com>
+        Tom Gundersen <teg@jklm.no>
diff --git a/src/n-acd/NEWS.md b/src/n-acd/NEWS.md
new file mode 100644
index 00000000..7a9ccd67
--- /dev/null
+++ b/src/n-acd/NEWS.md
@@ -0,0 +1,46 @@
+# n-acd - IPv4 Address Conflict Detection
+
+## CHANGES WITH 2:
+
+        * All public destructors now include a variant that returns `void`.
+          This was requested for easier integration with `glib` and friends.
+          Similar to the `cleanup` variants, these variants are denoted by a
+          single-character function-name suffix. E.g., `n_acd_freev()`
+
+        * A fallback to `CLOCK_MONOTONIC` is now provided in case
+          `CLOCK_BOOTTIME` is not supported by the kernel. Note that this is in
+          no way signalled through the API, so if timers should follow the
+          `BOOTTIME` rather than monotonic clock, a kernel with this clock is
+          required.
+
+        * The `c-sundry` dependency is no longer needed.
+
+        * The `transport` configuration property is now mandatory for
+          `n_acd_new()`. It defaulted to `ETHERNET` before, by mistake.
+
+        * In-source documentation for the public API is now provided.
+
+        Contributions from: Beniamino Galvani, David Herrmann, David
+                            Rheinsberg, Thomas Haller, Tom Gundersen
+
+        - Tübingen, 2019-03-20
+
+## CHANGES WITH 1:
+
+        * Initial release of n-acd. This project implements the IPv4 Address
+          Conflict Detection standard as defined in RFC-5227. The state machine
+          is implemented in a shared library and provides a stable ISO-C11 API.
+          The implementation is linux-only and relies heavily on the API
+          behavior of recent linux kernel releases.
+
+        * Compared to the pre-releases, this release supports many parallel
+          probes on a single n-acd context. This reduces the number of
+          allocated network resources to O(1), based on the number of running
+          parallel probes.
+
+        * The n-acd project is now dual-licensed: ASL-2.0 and LGPL-2.1+
+
+        Contributions from: Beniamino Galvani, David Herrmann, Thomas Haller,
+                            Tom Gundersen
+
+        - Tübingen, 2018-08-08
diff --git a/src/n-acd/README.md b/src/n-acd/README.md
new file mode 100644
index 00000000..08954182
--- /dev/null
+++ b/src/n-acd/README.md
@@ -0,0 +1,60 @@
+n-acd
+=====
+
+IPv4 Address Conflict Detection
+
+The n-acd project implements the IPv4 Address Conflict Detection standard as
+defined in RFC-5227. The state machine is implemented in a shared library and
+provides a stable ISO-C11 API. The implementation is linux-only and relies
+heavily on the API behavior of recent linux kernel releases.
+
+### Project
+
+ * **Website**: <https://nettools.github.io/n-acd>
+ * **Bug Tracker**: <https://github.com/nettools/n-acd/issues>
+ * **Mailing-List**: <https://groups.google.com/forum/#!forum/nettools-devel>
+
+### Requirements
+
+The requirements for this project are:
+
+ * `Linux kernel >= 3.19`
+ * `libc` (e.g., `glibc >= 2.16`)
+
+At build-time, the following software is required:
+
+ * `meson >= 0.41`
+ * `pkg-config >= 0.29`
+
+### Build
+
+The meson build-system is used for this project. Contact upstream
+documentation for detailed help. In most situations the following
+commands are sufficient to build and install from source:
+
+```sh
+mkdir build
+cd build
+meson setup ..
+ninja
+meson test
+ninja install
+```
+
+The following configuration options are available:
+
+ * `ebpf`: This boolean controls whether `ebpf` features are used to improve
+           the package filtering performance. If disabled, classic bpf will be
+           used. This feature requires a rather recent kernel (>=3.19).
+           Default is: true
+
+### Repository:
+
+ - **web**:   <https://github.com/nettools/n-acd>
+ - **https**: `https://github.com/nettools/n-acd.git`
+ - **ssh**:   `git@github.com:nettools/n-acd.git`
+
+### License:
+
+ - **Apache-2.0** OR **LGPL-2.1-or-later**
+ - See AUTHORS file for details.
diff --git a/src/n-acd/meson.build b/src/n-acd/meson.build
new file mode 100644
index 00000000..6479eb1a
--- /dev/null
+++ b/src/n-acd/meson.build
@@ -0,0 +1,27 @@
+project(
+        'n-acd',
+        'c',
+        version: '2',
+        license: 'Apache',
+        default_options: [
+                'c_std=c11',
+        ],
+)
+project_description = 'IPv4 Address Conflict Detection'
+
+add_project_arguments('-D_GNU_SOURCE', language: 'c')
+mod_pkgconfig = import('pkgconfig')
+
+sub_clist = subproject('c-list')
+sub_crbtree = subproject('c-rbtree')
+sub_csiphash = subproject('c-siphash')
+sub_cstdaux = subproject('libcstdaux-1')
+
+dep_clist = sub_clist.get_variable('libclist_dep')
+dep_crbtree = sub_crbtree.get_variable('libcrbtree_dep')
+dep_csiphash = sub_csiphash.get_variable('libcsiphash_dep')
+dep_cstdaux = sub_cstdaux.get_variable('libcstdaux_dep')
+
+use_ebpf = get_option('ebpf')
+
+subdir('src')
diff --git a/src/n-acd/meson_options.txt b/src/n-acd/meson_options.txt
new file mode 100644
index 00000000..b024ee1d
--- /dev/null
+++ b/src/n-acd/meson_options.txt
@@ -0,0 +1 @@
+option('ebpf', type: 'boolean', value: true, description: 'Enable eBPF packet filtering')
diff --git a/src/n-acd/src/libnacd.sym b/src/n-acd/src/libnacd.sym
new file mode 100644
index 00000000..f85e13ac
--- /dev/null
+++ b/src/n-acd/src/libnacd.sym
@@ -0,0 +1,28 @@
+LIBNACD_2 {
+global:
+        n_acd_config_new;
+        n_acd_config_free;
+        n_acd_config_set_ifindex;
+        n_acd_config_set_transport;
+        n_acd_config_set_mac;
+
+        n_acd_probe_config_new;
+        n_acd_probe_config_free;
+        n_acd_probe_config_set_ip;
+        n_acd_probe_config_set_timeout;
+
+        n_acd_new;
+        n_acd_ref;
+        n_acd_unref;
+        n_acd_get_fd;
+        n_acd_dispatch;
+        n_acd_pop_event;
+        n_acd_probe;
+
+        n_acd_probe_free;
+        n_acd_probe_set_userdata;
+        n_acd_probe_get_userdata;
+        n_acd_probe_announce;
+local:
+       *;
+};
diff --git a/src/n-acd/src/meson.build b/src/n-acd/src/meson.build
new file mode 100644
index 00000000..3e92681f
--- /dev/null
+++ b/src/n-acd/src/meson.build
@@ -0,0 +1,95 @@
+#
+# target: libnacd.so
+#
+
+libnacd_symfile = join_paths(meson.current_source_dir(), 'libnacd.sym')
+
+libnacd_deps = [
+        dep_clist,
+        dep_crbtree,
+        dep_csiphash,
+        dep_cstdaux,
+]
+
+libnacd_sources = [
+        'n-acd.c',
+        'n-acd-probe.c',
+        'util/timer.c',
+]
+
+if use_ebpf
+        libnacd_sources += [
+                'n-acd-bpf.c',
+        ]
+else
+        libnacd_sources += [
+                'n-acd-bpf-fallback.c',
+        ]
+endif
+
+libnacd_private = static_library(
+        'nacd-private',
+        libnacd_sources,
+        c_args: [
+                '-fvisibility=hidden',
+                '-fno-common'
+        ],
+        dependencies: libnacd_deps,
+        pic: true,
+)
+
+libnacd_shared = shared_library(
+        'nacd',
+        objects: libnacd_private.extract_all_objects(),
+        dependencies: libnacd_deps,
+        install: not meson.is_subproject(),
+        soversion: 0,
+        link_depends: libnacd_symfile,
+        link_args: [
+                '-Wl,--no-undefined',
+                '-Wl,--version-script=@0@'.format(libnacd_symfile)
+        ],
+)
+
+libnacd_dep = declare_dependency(
+        include_directories: include_directories('.'),
+        link_with: libnacd_private,
+        dependencies: libnacd_deps,
+        version: meson.project_version(),
+)
+
+if not meson.is_subproject()
+        install_headers('n-acd.h')
+
+        mod_pkgconfig.generate(
+                libraries: libnacd_shared,
+                version: meson.project_version(),
+                name: 'libnacd',
+                filebase: 'libnacd',
+                description: project_description,
+        )
+endif
+
+#
+# target: test-*
+#
+
+test_api = executable('test-api', ['test-api.c'], link_with: libnacd_shared)
+test('API Symbol Visibility', test_api)
+
+if use_ebpf
+        test_bpf = executable('test-bpf', ['test-bpf.c'], dependencies: libnacd_dep)
+        test('eBPF socket filtering', test_bpf)
+endif
+
+test_loopback = executable('test-loopback', ['test-loopback.c'], dependencies: libnacd_dep)
+test('Echo Suppression via Loopback', test_loopback)
+
+test_timer = executable('test-timer', ['util/test-timer.c'], dependencies: libnacd_dep)
+test('Timer helper', test_timer)
+
+#test_unplug = executable('test-unplug', ['test-unplug.c'], dependencies: libnacd_dep)
+#test('Async Interface Hotplug', test_unplug)
+
+test_veth = executable('test-veth', ['test-veth.c'], dependencies: libnacd_dep)
+test('Parallel ACD instances', test_veth)
diff --git a/src/n-acd/src/test-api.c b/src/n-acd/src/test-api.c
new file mode 100644
index 00000000..70f75208
--- /dev/null
+++ b/src/n-acd/src/test-api.c
@@ -0,0 +1,88 @@
+/*
+ * Tests for n-acd API
+ * This verifies the visibility and availability of the public API.
+ */
+
+#undef NDEBUG
+#include <assert.h>
+#include <stdlib.h>
+#include "n-acd.h"
+
+static void test_api_constants(void) {
+        assert(1 + N_ACD_TIMEOUT_RFC5227);
+
+        assert(1 + _N_ACD_E_SUCCESS);
+        assert(1 + N_ACD_E_PREEMPTED);
+        assert(1 + N_ACD_E_INVALID_ARGUMENT);
+        assert(1 + _N_ACD_E_N);
+
+        assert(1 + N_ACD_TRANSPORT_ETHERNET);
+        assert(1 + _N_ACD_TRANSPORT_N);
+
+        assert(1 + N_ACD_EVENT_READY);
+        assert(1 + N_ACD_EVENT_USED);
+        assert(1 + N_ACD_EVENT_DEFENDED);
+        assert(1 + N_ACD_EVENT_CONFLICT);
+        assert(1 + N_ACD_EVENT_DOWN);
+        assert(1 + _N_ACD_EVENT_N);
+
+        assert(1 + N_ACD_DEFEND_NEVER);
+        assert(1 + N_ACD_DEFEND_ONCE);
+        assert(1 + N_ACD_DEFEND_ALWAYS);
+        assert(1 + _N_ACD_DEFEND_N);
+}
+
+static void test_api_types(void) {
+        assert(sizeof(NAcdEvent*));
+        assert(sizeof(NAcdConfig*));
+        assert(sizeof(NAcdProbeConfig*));
+        assert(sizeof(NAcd*));
+        assert(sizeof(NAcdProbe*));
+}
+
+static void test_api_functions(void) {
+        void *fns[] = {
+                (void *)n_acd_config_new,
+                (void *)n_acd_config_free,
+                (void *)n_acd_config_set_ifindex,
+                (void *)n_acd_config_set_transport,
+                (void *)n_acd_config_set_mac,
+                (void *)n_acd_probe_config_new,
+                (void *)n_acd_probe_config_free,
+                (void *)n_acd_probe_config_set_ip,
+                (void *)n_acd_probe_config_set_timeout,
+
+                (void *)n_acd_new,
+                (void *)n_acd_ref,
+                (void *)n_acd_unref,
+                (void *)n_acd_get_fd,
+                (void *)n_acd_dispatch,
+                (void *)n_acd_pop_event,
+                (void *)n_acd_probe,
+
+                (void *)n_acd_probe_free,
+                (void *)n_acd_probe_set_userdata,
+                (void *)n_acd_probe_get_userdata,
+                (void *)n_acd_probe_announce,
+
+                (void *)n_acd_config_freep,
+                (void *)n_acd_config_freev,
+                (void *)n_acd_probe_config_freep,
+                (void *)n_acd_probe_config_freev,
+                (void *)n_acd_unrefp,
+                (void *)n_acd_unrefv,
+                (void *)n_acd_probe_freep,
+                (void *)n_acd_probe_freev,
+        };
+        size_t i;
+
+        for (i = 0; i < sizeof(fns) / sizeof(*fns); ++i)
+                assert(!!fns[i]);
+}
+
+int main(int argc, char **argv) {
+        test_api_constants();
+        test_api_types();
+        test_api_functions();
+        return 0;
+}
diff --git a/src/n-acd/src/test-bpf.c b/src/n-acd/src/test-bpf.c
new file mode 100644
index 00000000..78f9d0f1
--- /dev/null
+++ b/src/n-acd/src/test-bpf.c
@@ -0,0 +1,226 @@
+/*
+ * eBPF socket filter tests
+ */
+
+#undef NDEBUG
+#include <assert.h>
+#include <c-stdaux.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <netinet/if_ether.h>
+#include <netinet/in.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <unistd.h>
+#include "n-acd.h"
+#include "n-acd-private.h"
+#include "test.h"
+
+#define ETHER_ARP_PACKET_INIT(_op, _mac, _sip, _tip) {                  \
+                .ea_hdr = {                                             \
+                        .ar_hrd = htobe16(ARPHRD_ETHER),                \
+                        .ar_pro = htobe16(ETHERTYPE_IP),                \
+                        .ar_hln = 6,                                    \
+                        .ar_pln = 4,                                    \
+                        .ar_op = htobe16(_op),                          \
+                },                                                      \
+                .arp_sha[0] = (_mac)->ether_addr_octet[0],              \
+                .arp_sha[1] = (_mac)->ether_addr_octet[1],              \
+                .arp_sha[2] = (_mac)->ether_addr_octet[2],              \
+                .arp_sha[3] = (_mac)->ether_addr_octet[3],              \
+                .arp_sha[4] = (_mac)->ether_addr_octet[4],              \
+                .arp_sha[5] = (_mac)->ether_addr_octet[5],              \
+                .arp_spa[0] = (be32toh((_sip)->s_addr) >> 24) & 0xff,   \
+                .arp_spa[1] = (be32toh((_sip)->s_addr) >> 16) & 0xff,   \
+                .arp_spa[2] = (be32toh((_sip)->s_addr) >> 8) & 0xff,    \
+                .arp_spa[3] =  be32toh((_sip)->s_addr) & 0xff,          \
+                .arp_tpa[0] = (be32toh((_tip)->s_addr) >> 24) & 0xff,   \
+                .arp_tpa[1] = (be32toh((_tip)->s_addr) >> 16) & 0xff,   \
+                .arp_tpa[2] = (be32toh((_tip)->s_addr) >> 8) & 0xff,    \
+                .arp_tpa[3] =  be32toh((_tip)->s_addr) & 0xff,          \
+        }
+
+static void test_map(void) {
+        int r, mapfd = -1;
+        struct in_addr addr = { 1 };
+
+        r = n_acd_bpf_map_create(&mapfd, 8);
+        c_assert(r >= 0);
+        c_assert(mapfd >= 0);
+
+        r = n_acd_bpf_map_remove(mapfd, &addr);
+        c_assert(r == -ENOENT);
+
+        r = n_acd_bpf_map_add(mapfd, &addr);
+        c_assert(r >= 0);
+
+        r = n_acd_bpf_map_add(mapfd, &addr);
+        c_assert(r == -EEXIST);
+
+        r = n_acd_bpf_map_remove(mapfd, &addr);
+        c_assert(r >= 0);
+
+        r = n_acd_bpf_map_remove(mapfd, &addr);
+        c_assert(r == -ENOENT);
+
+        close(mapfd);
+}
+
+static void verify_success(struct ether_arp *packet, int out_fd, int in_fd) {
+        uint8_t buf[sizeof(struct ether_arp)];
+        int r;
+
+        r = send(out_fd, packet, sizeof(struct ether_arp), 0);
+        c_assert(r == sizeof(struct ether_arp));
+
+        r = recv(in_fd, buf, sizeof(buf), 0);
+        c_assert(r == sizeof(struct ether_arp));
+}
+
+static void verify_failure(struct ether_arp *packet, int out_fd, int in_fd) {
+        uint8_t buf[sizeof(struct ether_arp)];
+        int r;
+
+        r = send(out_fd, packet, sizeof(struct ether_arp), 0);
+        c_assert(r == sizeof(struct ether_arp));
+
+        r = recv(in_fd, buf, sizeof(buf), 0);
+        c_assert(r < 0);
+        c_assert(errno == EAGAIN);
+}
+
+static void test_filter(void) {
+        uint8_t buf[sizeof(struct ether_arp) + 1] = {};
+        struct ether_addr mac1 = { { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 } };
+        struct ether_addr mac2 = { { 0x01, 0x02, 0x03, 0x04, 0x05, 0x07 } };
+        struct in_addr ip0 = { 0 };
+        struct in_addr ip1 = { 1 };
+        struct in_addr ip2 = { 2 };
+        struct ether_arp *packet = (struct ether_arp *)buf;
+        int r, mapfd = -1, progfd = -1, pair[2];
+
+        r = n_acd_bpf_map_create(&mapfd, 1);
+        c_assert(r >= 0);
+
+        r = n_acd_bpf_compile(&progfd, mapfd, &mac1);
+        c_assert(r >= 0);
+        c_assert(progfd >= 0);
+
+        r = socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, pair);
+        c_assert(r >= 0);
+
+        r = setsockopt(pair[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd,
+                       sizeof(progfd));
+        c_assert(r >= 0);
+
+        r = n_acd_bpf_map_add(mapfd, &ip1);
+        c_assert(r >= 0);
+
+        /* valid */
+        *packet = (struct ether_arp)ETHER_ARP_PACKET_INIT(ARPOP_REQUEST, &mac2, &ip1, &ip2);
+        verify_success(packet, pair[0], pair[1]);
+
+        /* valid: reply instead of request */
+        *packet = (struct ether_arp)ETHER_ARP_PACKET_INIT(ARPOP_REPLY, &mac2, &ip1, &ip2);
+        verify_success(packet, pair[0], pair[1]);
+
+        /* valid: to us instead of from us */
+        *packet = (struct ether_arp)ETHER_ARP_PACKET_INIT(ARPOP_REQUEST, &mac2, &ip0, &ip1);
+        verify_success(packet, pair[0], pair[1]);
+
+        /* invalid header type */
+        *packet = (struct ether_arp)ETHER_ARP_PACKET_INIT(ARPOP_REQUEST, &mac2, &ip1, &ip2);
+        packet->arp_hrd += 1;
+        verify_failure(packet, pair[0], pair[1]);
+
+        /* invalid protocol */
+        *packet = (struct ether_arp)ETHER_ARP_PACKET_INIT(ARPOP_REQUEST, &mac2, &ip1, &ip2);
+        packet->arp_pro += 1;
+        verify_failure(packet, pair[0], pair[1]);
+
+        /* invalid hw addr length */
+        *packet = (struct ether_arp)ETHER_ARP_PACKET_INIT(ARPOP_REQUEST, &mac2, &ip1, &ip2);
+        packet->arp_hln += 1;
+        verify_failure(packet, pair[0], pair[1]);
+
+        /* invalid protocol addr length */
+        *packet = (struct ether_arp)ETHER_ARP_PACKET_INIT(ARPOP_REQUEST, &mac2, &ip1, &ip2);
+        packet->arp_pln += 1;
+        verify_failure(packet, pair[0], pair[1]);
+
+        /* invalid operation */
+        *packet = (struct ether_arp)ETHER_ARP_PACKET_INIT(ARPOP_NAK, &mac2, &ip1, &ip2);
+        packet->arp_hln += 1;
+        verify_failure(packet, pair[0], pair[1]);
+
+        /* own mac */
+        *packet = (struct ether_arp)ETHER_ARP_PACKET_INIT(ARPOP_REQUEST, &mac1, &ip1, &ip2);
+        verify_failure(packet, pair[0], pair[1]);
+
+        /* not to, nor from us, with source */
+        *packet = (struct ether_arp)ETHER_ARP_PACKET_INIT(ARPOP_REQUEST, &mac2, &ip2, &ip2);
+        verify_failure(packet, pair[0], pair[1]);
+
+        /* not to, nor from us, without source */
+        *packet = (struct ether_arp)ETHER_ARP_PACKET_INIT(ARPOP_REQUEST, &mac2, &ip0, &ip2);
+        verify_failure(packet, pair[0], pair[1]);
+
+        /* to us instead of from us, but reply */
+        *packet = (struct ether_arp)ETHER_ARP_PACKET_INIT(ARPOP_REPLY, &mac2, &ip0, &ip1);
+        verify_failure(packet, pair[0], pair[1]);
+
+        /* long */
+        *packet = (struct ether_arp)ETHER_ARP_PACKET_INIT(ARPOP_REQUEST, &mac2, &ip1, &ip2);
+        r = send(pair[0], buf, sizeof(struct ether_arp) + 1, 0);
+        c_assert(r == sizeof(struct ether_arp) + 1);
+
+        r = recv(pair[1], buf, sizeof(buf), 0);
+        c_assert(r == sizeof(struct ether_arp));
+
+        /* short */
+        *packet = (struct ether_arp)ETHER_ARP_PACKET_INIT(ARPOP_REQUEST, &mac2, &ip1, &ip2);
+        r = send(pair[0], buf, sizeof(struct ether_arp) - 1, 0);
+        c_assert(r == sizeof(struct ether_arp) - 1);
+
+        r = recv(pair[1], buf, sizeof(buf), 0);
+        c_assert(r < 0);
+        c_assert(errno == EAGAIN);
+
+        /*
+         * Send one packet before and one packet after modifying the map,
+         * verify that the modification applies at the time of send(), not recv().
+         */
+        *packet = (struct ether_arp)ETHER_ARP_PACKET_INIT(ARPOP_REQUEST, &mac2, &ip1, &ip2);
+        r = send(pair[0], buf, sizeof(struct ether_arp), 0);
+        c_assert(r == sizeof(struct ether_arp));
+
+        r = n_acd_bpf_map_remove(mapfd, &ip1);
+        c_assert(r >= 0);
+
+        r = send(pair[0], buf, sizeof(struct ether_arp), 0);
+        c_assert(r == sizeof(struct ether_arp));
+
+        r = recv(pair[1], buf, sizeof(buf), 0);
+        c_assert(r == sizeof(struct ether_arp));
+
+        r = recv(pair[1], buf, sizeof(buf), 0);
+        c_assert(r < 0);
+        c_assert(errno == EAGAIN);
+
+        close(pair[0]);
+        close(pair[1]);
+        close(progfd);
+        close(mapfd);
+}
+
+int main(int argc, char **argv) {
+        test_setup();
+
+        test_map();
+        test_filter();
+
+        return 0;
+}
diff --git a/src/n-acd/src/test-loopback.c b/src/n-acd/src/test-loopback.c
new file mode 100644
index 00000000..0671cf66
--- /dev/null
+++ b/src/n-acd/src/test-loopback.c
@@ -0,0 +1,82 @@
+/*
+ * Test on loopback device
+ * This runs the ACD engine on the loopback device, effectively testing the BPF
+ * filter of ACD to discard its own packets. This might happen on
+ * non-spanning-tree networks, or on networks that echo packets.
+ */
+
+#undef NDEBUG
+#include <c-stdaux.h>
+#include <stdlib.h>
+#include "test.h"
+
+static void test_loopback(int ifindex, uint8_t *mac, size_t n_mac) {
+        NAcdConfig *config;
+        NAcd *acd;
+        struct pollfd pfds;
+        int r, fd;
+
+        r = n_acd_config_new(&config);
+        c_assert(!r);
+
+        n_acd_config_set_ifindex(config, ifindex);
+        n_acd_config_set_transport(config, N_ACD_TRANSPORT_ETHERNET);
+        n_acd_config_set_mac(config, mac, n_mac);
+
+        r = n_acd_new(&acd, config);
+        c_assert(!r);
+
+        n_acd_config_free(config);
+
+        {
+                NAcdProbeConfig *probe_config;
+                NAcdProbe *probe;
+                struct in_addr ip = { htobe32((192 << 24) | (168 << 16) | (1 << 0)) };
+
+                r = n_acd_probe_config_new(&probe_config);
+                c_assert(!r);
+
+                n_acd_probe_config_set_ip(probe_config, ip);
+                n_acd_probe_config_set_timeout(probe_config, 100);
+
+                r = n_acd_probe(acd, &probe, probe_config);
+                c_assert(!r);
+
+                n_acd_probe_config_free(probe_config);
+
+                n_acd_get_fd(acd, &fd);
+
+                for (;;) {
+                        NAcdEvent *event;
+                        pfds = (struct pollfd){ .fd = fd, .events = POLLIN };
+                        r = poll(&pfds, 1, -1);
+                        c_assert(r >= 0);
+
+                        r = n_acd_dispatch(acd);
+                        c_assert(!r);
+
+                        r = n_acd_pop_event(acd, &event);
+                        c_assert(!r);
+                        if (event) {
+                                c_assert(event->event == N_ACD_EVENT_READY);
+                                break;
+                        }
+                }
+
+                n_acd_probe_free(probe);
+        }
+
+        n_acd_unref(acd);
+}
+
+int main(int argc, char **argv) {
+        struct ether_addr mac;
+        int ifindex;
+
+        test_setup();
+
+        test_loopback_up(&ifindex, &mac);
+        test_loopback(ifindex, mac.ether_addr_octet, sizeof(mac.ether_addr_octet));
+
+        return 0;
+}
diff --git a/src/n-acd/src/test-twice.c b/src/n-acd/src/test-twice.c
new file mode 100644
index 00000000..b474502e
--- /dev/null
+++ b/src/n-acd/src/test-twice.c
@@ -0,0 +1,97 @@
+/*
+ * Test with unused address twice in parallel
+ * This runs the ACD engine with an unused address on a veth pair, but it runs
+ * it on both ends. We expect the PROBE to fail on at least one of the devices.
+ */
+
+#undef NDEBUG
+#include <c-stdaux.h>
+#include <stdlib.h>
+#include "test.h"
+
+static void test_unused(int ifindex1, uint8_t *mac1, size_t n_mac1, int ifindex2, uint8_t *mac2, size_t n_mac2) {
+        NAcdConfig config1 = {
+                .ifindex = ifindex1,
+                .transport = N_ACD_TRANSPORT_ETHERNET,
+                .mac = mac1,
+                .n_mac = n_mac1,
+                .ip = { htobe32((192 << 24) | (168 << 16) | (1 << 0)) },
+                .timeout_msec = 100,
+        };
+        NAcdConfig config2 = {
+                .ifindex = ifindex2,
+                .transport = N_ACD_TRANSPORT_ETHERNET,
+                .mac = mac2,
+                .n_mac = n_mac2,
+                .ip = { htobe32((192 << 24) | (168 << 16) | (1 << 0)) },
+                .timeout_msec = 100,
+        };
+        struct pollfd pfds[2];
+        NAcd *acd1, *acd2;
+        int r, fd1, fd2, state1, state2;
+
+        r = n_acd_new(&acd1);
+        c_assert(!r);
+        r = n_acd_new(&acd2);
+        c_assert(!r);
+
+        n_acd_get_fd(acd1, &fd1);
+        n_acd_get_fd(acd2, &fd2);
+
+        r = n_acd_start(acd1, &config1);
+        c_assert(!r);
+        r = n_acd_start(acd2, &config2);
+        c_assert(!r);
+
+        for (state1 = state2 = -1; state1 == -1 || state2 == -1; ) {
+                NAcdEvent *event;
+                pfds[0] = (struct pollfd){ .fd = fd1, .events = (state1 == -1) ? POLLIN : 0 };
+                pfds[1] = (struct pollfd){ .fd = fd2, .events = (state2 == -1) ? POLLIN : 0 };
+
+                r = poll(pfds, sizeof(pfds) / sizeof(*pfds), -1);
+                c_assert(r >= 0);
+
+                if (state1 == -1) {
+                        r = n_acd_dispatch(acd1);
+                        c_assert(!r);
+
+                        r = n_acd_pop_event(acd1, &event);
+                        if (!r) {
+                                c_assert(event->event == N_ACD_EVENT_READY || event->event == N_ACD_EVENT_USED);
+                                state1 = !!(event->event == N_ACD_EVENT_READY);
+                        } else {
+                                c_assert(r == N_ACD_E_DONE);
+                        }
+                }
+
+                if (state2 == -1) {
+                        r = n_acd_dispatch(acd2);
+                        c_assert(!r);
+
+                        r = n_acd_pop_event(acd2, &event);
+                        if (!r) {
+                                c_assert(event->event == N_ACD_EVENT_READY || event->event == N_ACD_EVENT_USED);
+                                state2 = !!(event->event == N_ACD_EVENT_READY);
+                        } else {
+                                c_assert(r == N_ACD_E_DONE);
+                        }
+                }
+        }
+
+        n_acd_free(acd1);
+        n_acd_free(acd2);
+
+        c_assert(!state1 || !state2);
+}
+
+int main(int argc, char **argv) {
+        struct ether_addr mac1, mac2;
+        int ifindex1, ifindex2;
+
+        test_setup();
+
+        test_veth_new(&ifindex1, &mac1, &ifindex2, &mac2);
+        test_unused(ifindex1, mac1.ether_addr_octet, sizeof(mac2.ether_addr_octet), ifindex2, mac2.ether_addr_octet, sizeof(mac2.ether_addr_octet));
+
+        return 0;
+}
diff --git a/src/n-acd/src/test-unplug.c b/src/n-acd/src/test-unplug.c
new file mode 100644
index 00000000..9ad88a91
--- /dev/null
+++ b/src/n-acd/src/test-unplug.c
@@ -0,0 +1,84 @@
+/*
+ * Unplug device during test run
+ * Run the ACD engine with an address that is not used by anyone else on the
+ * link, but DOWN or UNPLUG the device while running.
+ */
+
+#undef NDEBUG
+#include <c-stdaux.h>
+#include <stdlib.h>
+#include "test.h"
+
+static void test_unplug_down(int ifindex, uint8_t *mac, size_t n_mac, unsigned int run) {
+        NAcdConfig config = {
+                .ifindex = ifindex,
+                .transport = N_ACD_TRANSPORT_ETHERNET,
+                .mac = mac,
+                .n_mac = n_mac,
+                .ip = { htobe32((192 << 24) | (168 << 16) | (1 << 0)) },
+                .timeout_msec = 100,
+        };
+        struct pollfd pfds;
+        NAcd *acd;
+        int r, fd;
+
+        if (!run--)
+                test_veth_cmd(ifindex, "down");
+
+        r = n_acd_new(&acd);
+        c_assert(!r);
+
+        if (!run--)
+                test_veth_cmd(ifindex, "down");
+
+        n_acd_get_fd(acd, &fd);
+        r = n_acd_start(acd, &config);
+        c_assert(!r);
+
+        if (!run--)
+                test_veth_cmd(ifindex, "down");
+
+        for (;;) {
+                NAcdEvent *event;
+                pfds = (struct pollfd){ .fd = fd, .events = POLLIN };
+                r = poll(&pfds, 1, -1);
+                c_assert(r >= 0);
+
+                if (!run--)
+                        test_veth_cmd(ifindex, "down");
+
+                r = n_acd_dispatch(acd);
+                c_assert(!r);
+
+                r = n_acd_pop_event(acd, &event);
+                if (!r) {
+                        if (event->event == N_ACD_EVENT_DOWN) {
+                                break;
+                        } else {
+                                c_assert(event->event == N_ACD_EVENT_READY);
+                                test_veth_cmd(ifindex, "down");
+                        }
+                } else {
+                        c_assert(r == N_ACD_E_DONE);
+                }
+        }
+
+        n_acd_free(acd);
+}
+
+int main(int argc, char **argv) {
+        struct ether_addr mac;
+        unsigned int i;
+        int ifindex;
+
+        test_setup();
+
+        test_veth_new(&ifindex, &mac, NULL, NULL);
+
+        for (i = 0; i < 5; ++i) {
+                test_unplug_down(ifindex, mac.ether_addr_octet, sizeof(mac.ether_addr_octet), i);
+                test_veth_cmd(ifindex, "up");
+        }
+
+        return 0;
+}
diff --git a/src/n-acd/src/test-unused.c b/src/n-acd/src/test-unused.c
new file mode 100644
index 00000000..67ec2e4c
--- /dev/null
+++ b/src/n-acd/src/test-unused.c
@@ -0,0 +1,63 @@
+/*
+ * Test with unused address
+ * Run the ACD engine with an address that is not used by anyone else on the
+ * link. This should just pass through, with a short, random timeout.
+ */
+
+#undef NDEBUG
+#include <c-stdaux.h>
+#include <stdlib.h>
+#include "test.h"
+
+static void test_unused(int ifindex, const uint8_t *mac, size_t n_mac) {
+        NAcdConfig config = {
+                .ifindex = ifindex,
+                .transport = N_ACD_TRANSPORT_ETHERNET,
+                .mac = mac,
+                .n_mac = n_mac,
+                .ip = { htobe32((192 << 24) | (168 << 16) | (1 << 0)) },
+                .timeout_msec = 100,
+        };
+        struct pollfd pfds;
+        NAcd *acd;
+        int r, fd;
+
+        r = n_acd_new(&acd);
+        c_assert(!r);
+
+        n_acd_get_fd(acd, &fd);
+        r = n_acd_start(acd, &config);
+        c_assert(!r);
+
+        for (;;) {
+                NAcdEvent *event;
+                pfds = (struct pollfd){ .fd = fd, .events = POLLIN };
+                r = poll(&pfds, 1, -1);
+                c_assert(r >= 0);
+
+                r = n_acd_dispatch(acd);
+                c_assert(!r);
+
+                r = n_acd_pop_event(acd, &event);
+                if (!r) {
+                        c_assert(event->event == N_ACD_EVENT_READY);
+                        break;
+                } else {
+                        c_assert(r == N_ACD_E_DONE);
+                }
+        }
+
+        n_acd_free(acd);
+}
+
+int main(int argc, char **argv) {
+        struct ether_addr mac;
+        int ifindex;
+
+        test_setup();
+
+        test_veth_new(&ifindex, &mac, NULL, NULL);
+        test_unused(ifindex, mac.ether_addr_octet, sizeof(mac.ether_addr_octet));
+
+        return 0;
+}
diff --git a/src/n-acd/src/test-veth.c b/src/n-acd/src/test-veth.c
new file mode 100644
index 00000000..d1923683
--- /dev/null
+++ b/src/n-acd/src/test-veth.c
@@ -0,0 +1,240 @@
+/*
+ * Test on a veth link
+ *
+ * This essentially mimics a real network with two peers.
+ *
+ * Run one ACD context on each end of the tunnel. On one end probe for N,
+ * addresses on the other end pre-configure N/3 of the same addresses and probe
+ * for another N/3 of the addresses.
+ *
+ * Verify that in the case of simultaneous probes of the same address at most one
+ * succeed, in the case of probing for a configured address it always fails, and
+ * probing for a non-existent address always succeeds.
+ *
+ * Make sure to keep N fairly high as the protocol is probabilistic, and we also
+ * want to verify that resizing the internal maps works correctly.
+ */
+
+#undef NDEBUG
+#include <c-stdaux.h>
+#include <stdlib.h>
+#include "test.h"
+
+#define TEST_ACD_N_PROBES (9)
+
+typedef enum {
+        TEST_ACD_STATE_UNKNOWN,
+        TEST_ACD_STATE_USED,
+        TEST_ACD_STATE_READY,
+} TestAcdState;
+
+static void test_veth(int ifindex1, uint8_t *mac1, size_t n_mac1,
+                      int ifindex2, uint8_t *mac2, size_t n_mac2) {
+        NAcdConfig *config;
+        NAcd *acd1, *acd2;
+        NAcdProbe *probes1[TEST_ACD_N_PROBES];
+        NAcdProbe *probes2[TEST_ACD_N_PROBES];
+        unsigned long state1, state2;
+        size_t n_running = 0;
+        int r;
+
+        r = n_acd_config_new(&config);
+        c_assert(!r);
+
+        n_acd_config_set_transport(config, N_ACD_TRANSPORT_ETHERNET);
+
+        n_acd_config_set_ifindex(config, ifindex1);
+        n_acd_config_set_mac(config, mac1, n_mac1);
+        r = n_acd_new(&acd1, config);
+        c_assert(!r);
+
+        n_acd_config_set_ifindex(config, ifindex2);
+        n_acd_config_set_mac(config, mac2, n_mac2);
+        r = n_acd_new(&acd2, config);
+        c_assert(!r);
+
+        n_acd_config_free(config);
+
+        {
+                NAcdProbeConfig *probe_config;
+
+                r = n_acd_probe_config_new(&probe_config);
+                c_assert(!r);
+                n_acd_probe_config_set_timeout(probe_config, 1024);
+
+                c_assert(TEST_ACD_N_PROBES <= 10 << 24);
+
+                for (size_t i = 0; i < TEST_ACD_N_PROBES; ++i) {
+                        struct in_addr ip = { htobe32((10 << 24) | i) };
+
+                        n_acd_probe_config_set_ip(probe_config, ip);
+
+                        switch (i % 3) {
+                        case 0:
+                                /*
+                                 * Probe on one side, and leave the address
+                                 * unset on the other. The probe must succeed.
+                                 */
+                                break;
+                        case 1:
+                                /*
+                                 * Preconfigure the address on one side, and
+                                 * probe on the other. The probe must fail.
+                                 */
+                                test_add_child_ip(&ip);
+                                break;
+                        case 2:
+                                /*
+                                 * Probe both sides for the same address, at
+                                 * most one may succeed.
+                                 */
+
+                                r = n_acd_probe(acd2, &probes2[i], probe_config);
+                                c_assert(!r);
+
+                                ++n_running;
+                                break;
+                        default:
+                                c_assert(0);
+                                abort();
+                                break;
+                        }
+
+                        r = n_acd_probe(acd1, &probes1[i], probe_config);
+                        c_assert(!r);
+
+                        ++n_running;
+                }
+
+                n_acd_probe_config_free(probe_config);
+
+                while (n_running > 0) {
+                        NAcdEvent *event;
+                        struct pollfd pfds[2] = {
+                                { .events = POLLIN },
+                                { .events = POLLIN },
+                        };
+
+                        n_acd_get_fd(acd1, &pfds[0].fd);
+                        n_acd_get_fd(acd2, &pfds[1].fd);
+
+                        r = poll(pfds, 2, -1);
+                        c_assert(r >= 0);
+
+                        if (pfds[0].revents & POLLIN) {
+                                r = n_acd_dispatch(acd1);
+                                c_assert(!r || r == N_ACD_E_PREEMPTED);
+
+                                for (;;) {
+                                        r = n_acd_pop_event(acd1, &event);
+                                        c_assert(!r);
+                                        if (event) {
+                                                switch (event->event) {
+                                                case N_ACD_EVENT_READY:
+                                                        n_acd_probe_get_userdata(event->ready.probe, (void**)&state1);
+                                                        c_assert(state1 == TEST_ACD_STATE_UNKNOWN);
+                                                        state1 = TEST_ACD_STATE_READY;
+                                                        n_acd_probe_set_userdata(event->ready.probe, (void*)state1);
+
+                                                        break;
+                                                case N_ACD_EVENT_USED:
+                                                        n_acd_probe_get_userdata(event->used.probe, (void**)&state1);
+                                                        c_assert(state1 == TEST_ACD_STATE_UNKNOWN);
+                                                        state1 = TEST_ACD_STATE_USED;
+                                                        n_acd_probe_set_userdata(event->used.probe, (void*)state1);
+
+                                                        break;
+                                                default:
+                                                        c_assert(0);
+                                                }
+
+                                                --n_running;
+                                        } else {
+                                                break;
+                                        }
+                                }
+                        }
+
+                        if (pfds[1].revents & POLLIN) {
+                                r = n_acd_dispatch(acd2);
+                                c_assert(!r || r == N_ACD_E_PREEMPTED);
+
+                                for (;;) {
+                                        r = n_acd_pop_event(acd2, &event);
+                                        c_assert(!r);
+                                        if (event) {
+                                                switch (event->event) {
+                                                case N_ACD_EVENT_READY:
+                                                        n_acd_probe_get_userdata(event->ready.probe, (void**)&state2);
+                                                        c_assert(state2 == TEST_ACD_STATE_UNKNOWN);
+                                                        state2 = TEST_ACD_STATE_READY;
+                                                        n_acd_probe_set_userdata(event->ready.probe, (void*)state2);
+
+                                                        break;
+                                                case N_ACD_EVENT_USED:
+                                                        n_acd_probe_get_userdata(event->used.probe, (void**)&state2);
+                                                        c_assert(state2 == TEST_ACD_STATE_UNKNOWN);
+                                                        state2 = TEST_ACD_STATE_USED;
+                                                        n_acd_probe_set_userdata(event->used.probe, (void*)state2);
+
+                                                        break;
+                                                default:
+                                                        c_assert(0);
+                                                }
+
+                                                --n_running;
+                                        } else {
+                                                break;
+                                        }
+                                }
+                        }
+                }
+
+                for (size_t i = 0; i < TEST_ACD_N_PROBES; ++i) {
+                        struct in_addr ip = { htobe32((10 << 24) | i) };
+
+                        switch (i % 3) {
+                        case 0:
+                                n_acd_probe_get_userdata(probes1[i], (void **)&state1);
+                                c_assert(state1 == TEST_ACD_STATE_READY);
+
+                                break;
+                        case 1:
+                                test_del_child_ip(&ip);
+
+                                n_acd_probe_get_userdata(probes1[i], (void **)&state1);
+                                c_assert(state1 == TEST_ACD_STATE_USED);
+
+                                break;
+                        case 2:
+                                n_acd_probe_get_userdata(probes1[i], (void **)&state1);
+                                n_acd_probe_get_userdata(probes2[i], (void **)&state2);
+                                c_assert(state1 != TEST_ACD_STATE_UNKNOWN);
+                                c_assert(state2 != TEST_ACD_STATE_UNKNOWN);
+                                c_assert(state1 == TEST_ACD_STATE_USED || state2 == TEST_ACD_STATE_USED);
+                                n_acd_probe_free(probes2[i]);
+
+                                break;
+                        }
+                        n_acd_probe_free(probes1[i]);
+                }
+        }
+
+        n_acd_unref(acd2);
+        n_acd_unref(acd1);
+}
+
+int main(int argc, char **argv) {
+        struct ether_addr mac1, mac2;
+        int ifindex1, ifindex2;
+
+        test_setup();
+
+        test_veth_new(&ifindex1, &mac1, &ifindex2, &mac2);
+        for (unsigned int i = 0; i < 8; ++i) {
+                test_veth(ifindex1, mac1.ether_addr_octet, sizeof(mac1.ether_addr_octet),
+                          ifindex2, mac2.ether_addr_octet, sizeof(mac2.ether_addr_octet));
+        }
+
+        return 0;
+}
diff --git a/src/n-acd/src/test.h b/src/n-acd/src/test.h
new file mode 100644
index 00000000..69a786a0
--- /dev/null
+++ b/src/n-acd/src/test.h
@@ -0,0 +1,213 @@
+#pragma once
+
+/*
+ * Test Helpers
+ * Bunch of helpers to setup the environment for networking tests. This
+ * includes net-namespace setups, veth setups, and more.
+ */
+
+#undef NDEBUG
+#include <assert.h>
+#include <c-stdaux.h>
+#include <endian.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <net/ethernet.h>
+#include <net/if.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <poll.h>
+#include <sched.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mount.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include "n-acd.h"
+
+static inline void test_add_child_ip(const struct in_addr *addr) {
+        char *p;
+        int r;
+
+        r = asprintf(&p, "ip addr add dev veth1 %s/8", inet_ntoa(*addr));
+        c_assert(r >= 0);
+
+        r = system(p);
+        c_assert(r >= 0);
+
+        free(p);
+}
+
+static inline void test_del_child_ip(const struct in_addr *addr) {
+        char *p;
+        int r;
+
+        r = asprintf(&p, "ip addr del dev veth1 %s/8", inet_ntoa(*addr));
+        c_assert(r >= 0);
+
+        r = system(p);
+        c_assert(r >= 0);
+
+        free(p);
+}
+
+static inline void test_if_query(const char *name, int *indexp, struct ether_addr *macp) {
+        struct ifreq ifr = {};
+        size_t l;
+        int r, s;
+
+        l = strlen(name);
+        c_assert(l <= IF_NAMESIZE);
+
+        if (indexp) {
+                *indexp = if_nametoindex(name);
+                c_assert(*indexp > 0);
+        }
+
+        if (macp) {
+                s = socket(AF_INET, SOCK_DGRAM, 0);
+                c_assert(s >= 0);
+
+                strncpy(ifr.ifr_name, name, l + 1);
+                r = ioctl(s, SIOCGIFHWADDR, &ifr);
+                c_assert(r >= 0);
+
+                memcpy(macp->ether_addr_octet, ifr.ifr_hwaddr.sa_data, ETH_ALEN);
+
+                close(s);
+        }
+}
+
+static inline void test_veth_cmd(int ifindex, const char *cmd) {
+        char *p, name[IF_NAMESIZE + 1] = {};
+        int r;
+
+        p = if_indextoname(ifindex, name);
+        c_assert(p);
+
+        r = asprintf(&p, "ip link set %s %s", name, cmd);
+        c_assert(r >= 0);
+
+        /* Again: Ewwww... */
+        r = system(p);
+        c_assert(r == 0);
+
+        free(p);
+}
+
+static inline void test_veth_new(int *parent_indexp,
+                                 struct ether_addr *parent_macp,
+                                 int *child_indexp,
+                                 struct ether_addr *child_macp) {
+        int r;
+
+        /* Eww... but it works. */
+        r = system("ip link add type veth");
+        c_assert(r == 0);
+        r = system("ip link set veth0 up");
+        c_assert(r == 0);
+        r = system("ip link set veth1 up");
+        c_assert(r == 0);
+
+        test_if_query("veth0", parent_indexp, parent_macp);
+        test_if_query("veth1", child_indexp, child_macp);
+}
+
+static inline void test_loopback_up(int *indexp, struct ether_addr *macp) {
+        int r;
+
+        r = system("ip link set lo up");
+        c_assert(r == 0);
+
+        test_if_query("lo", indexp, macp);
+}
+
+static inline void test_raise_memlock(void) {
+        const size_t wanted = 64 * 1024 * 1024;
+        struct rlimit get, set;
+        int r;
+
+        r = getrlimit(RLIMIT_MEMLOCK, &get);
+        c_assert(!r);
+
+        /* try raising limit to @wanted */
+        set.rlim_cur = wanted;
+        set.rlim_max = (wanted > get.rlim_max) ? wanted : get.rlim_max;
+        r = setrlimit(RLIMIT_MEMLOCK, &set);
+        if (r) {
+                c_assert(errno == EPERM);
+
+                /* not privileged to raise limit, so maximize soft limit */
+                set.rlim_cur = get.rlim_max;
+                set.rlim_max = get.rlim_max;
+                r = setrlimit(RLIMIT_MEMLOCK, &set);
+                c_assert(!r);
+        }
+}
+
+static inline void test_unshare_user_namespace(void) {
+        uid_t euid;
+        gid_t egid;
+        int r, fd;
+
+        /*
+         * Enter a new user namespace as root:root.
+         */
+
+        euid = geteuid();
+        egid = getegid();
+
+        r = unshare(CLONE_NEWUSER);
+        c_assert(r >= 0);
+
+        fd = open("/proc/self/uid_map", O_WRONLY);
+        c_assert(fd >= 0);
+        r = dprintf(fd, "0 %d 1\n", euid);
+        c_assert(r >= 0);
+        close(fd);
+
+        fd = open("/proc/self/setgroups", O_WRONLY);
+        c_assert(fd >= 0);
+        r = dprintf(fd, "deny");
+        c_assert(r >= 0);
+        close(fd);
+
+        fd = open("/proc/self/gid_map", O_WRONLY);
+        c_assert(fd >= 0);
+        r = dprintf(fd, "0 %d 1\n", egid);
+        c_assert(r >= 0);
+        close(fd);
+}
+
+static inline void test_setup(void) {
+        int r;
+
+        /*
+         * Move into a new network and mount namespace both associated
+         * with a new user namespace where the current eUID is mapped to
+         * 0. Then create a private instance of /run/netns. This ensures
+         * that any network devices or network namespaces are private to
+         * the test process.
+         */
+
+        test_raise_memlock();
+        test_unshare_user_namespace();
+
+        r = unshare(CLONE_NEWNET | CLONE_NEWNS);
+        c_assert(r >= 0);
+
+        r = mount(NULL, "/", "", MS_PRIVATE | MS_REC, NULL);
+        c_assert(r >= 0);
+
+        r = mount(NULL, "/run", "tmpfs", 0, NULL);
+        c_assert(r >= 0);
+
+        r = mkdir("/run/netns", 0755);
+        c_assert(r >= 0);
+}
diff --git a/src/n-acd/src/util/test-timer.c b/src/n-acd/src/util/test-timer.c
new file mode 100644
index 00000000..a0c908bd
--- /dev/null
+++ b/src/n-acd/src/util/test-timer.c
@@ -0,0 +1,177 @@
+/*
+ * Tests for timer utility library
+ */
+
+#undef NDEBUG
+#include <c-stdaux.h>
+#include <errno.h>
+#include <poll.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/timerfd.h>
+#include "timer.h"
+
+#define N_TIMEOUTS (10000)
+
+static void test_api(void) {
+        Timer timer = TIMER_NULL(timer);
+        Timeout t1 = TIMEOUT_INIT(t1), t2 = TIMEOUT_INIT(t2), *t;
+        int r;
+
+        r = timer_init(&timer);
+        c_assert(!r);
+
+        timeout_schedule(&t1, &timer, 1);
+        timeout_schedule(&t2, &timer, 2);
+
+        r = timer_pop_timeout(&timer, 10, &t);
+        c_assert(!r);
+        c_assert(t == &t1);
+
+        timeout_unschedule(&t2);
+
+        r = timer_pop_timeout(&timer, 10, &t);
+        c_assert(!r);
+        c_assert(!t);
+
+        timer_deinit(&timer);
+}
+
+static void test_pop(void) {
+        Timer timer = TIMER_NULL(timer);
+        Timeout timeouts[N_TIMEOUTS] = {};
+        uint64_t times[N_TIMEOUTS] = {};
+        size_t n_timeouts = 0;
+        bool armed;
+        Timeout *t;
+        int r;
+
+        r = timer_init(&timer);
+        c_assert(!r);
+
+        for(size_t i = 0; i < N_TIMEOUTS; ++i) {
+                timeouts[i] = (Timeout)TIMEOUT_INIT(timeouts[i]);
+                times[i] = rand() % 128 + 1;
+                timeout_schedule(&timeouts[i], &timer, times[i]);
+        }
+
+        armed = true;
+
+        for(size_t i = 0; i <= 128; ++i) {
+                if (armed) {
+                        struct pollfd pfd = {
+                                .fd = timer.fd,
+                                .events = POLLIN,
+                        };
+                        uint64_t count;
+
+                        r = poll(&pfd, 1, -1);
+                        c_assert(r == 1);
+
+                        r = read(timer.fd, &count, sizeof(count));
+                        c_assert(r == sizeof(count));
+                        c_assert(count == 1);
+                        armed = false;
+                }
+
+                for (;;) {
+                        uint64_t current_time;
+
+                        r = timer_pop_timeout(&timer, i, &t);
+                        c_assert(!r);
+                        if (!t) {
+                                timer_rearm(&timer);
+                                break;
+                        }
+
+                        current_time = times[t - timeouts];
+                        c_assert(current_time == i);
+                        ++n_timeouts;
+                        armed = true;
+                }
+        }
+
+        c_assert(n_timeouts == N_TIMEOUTS);
+
+        r = timer_pop_timeout(&timer, (uint64_t)-1, &t);
+        c_assert(!r);
+        c_assert(!t);
+
+        timer_deinit(&timer);
+}
+
+void test_arm(void) {
+        struct itimerspec spec = {
+                .it_value = {
+                        .tv_sec = 1000,
+                },
+        };
+        int fd1, fd2, r;
+
+        fd1 = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
+        c_assert(fd1 >= 0);
+
+        fd2 = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
+        c_assert(fd1 >= 0);
+
+        r = timerfd_settime(fd1, 0, &spec, NULL);
+        c_assert(r >= 0);
+
+        r = timerfd_settime(fd2, 0, &spec, NULL);
+        c_assert(r >= 0);
+
+        r = timerfd_gettime(fd1, &spec);
+        c_assert(r >= 0);
+        c_assert(spec.it_value.tv_sec);
+
+        r = timerfd_gettime(fd2, &spec);
+        c_assert(r >= 0);
+        c_assert(spec.it_value.tv_sec);
+
+        spec = (struct itimerspec){};
+
+        r = timerfd_settime(fd1, 0, &spec, NULL);
+        c_assert(r >= 0);
+
+        r = timerfd_gettime(fd1, &spec);
+        c_assert(r >= 0);
+        c_assert(!spec.it_value.tv_sec);
+        c_assert(!spec.it_value.tv_nsec);
+
+        r = timerfd_gettime(fd2, &spec);
+        c_assert(r >= 0);
+        c_assert(spec.it_value.tv_sec);
+
+        spec = (struct itimerspec){ .it_value = { .tv_nsec = 1, }, };
+
+        r = timerfd_settime(fd1, 0, &spec, NULL);
+        c_assert(r >= 0);
+
+        r = poll(&(struct pollfd) { .fd = fd1, .events = POLLIN }, 1, -1);
+        c_assert(r == 1);
+
+        r = timerfd_settime(fd2, 0, &spec, NULL);
+        c_assert(r >= 0);
+
+        r = poll(&(struct pollfd) { .fd = fd2, .events = POLLIN }, 1, -1);
+        c_assert(r == 1);
+
+        spec = (struct itimerspec){};
+
+        r = timerfd_settime(fd1, 0, &spec, NULL);
+        c_assert(r >= 0);
+
+        r = poll(&(struct pollfd) { .fd = fd2, .events = POLLIN }, 1, -1);
+        c_assert(r == 1);
+
+        close(fd2);
+        close(fd1);
+}
+
+int main(int argc, char **argv) {
+        test_arm();
+        test_api();
+        test_pop();
+        return 0;
+}
diff --git a/src/n-acd/subprojects/c-list b/src/n-acd/subprojects/c-list
new file mode 120000
index 00000000..4e274698
--- /dev/null
+++ b/src/n-acd/subprojects/c-list
@@ -0,0 +1 @@
+../../c-list
\ No newline at end of file
diff --git a/src/n-acd/subprojects/c-rbtree b/src/n-acd/subprojects/c-rbtree
new file mode 120000
index 00000000..49264a87
--- /dev/null
+++ b/src/n-acd/subprojects/c-rbtree
@@ -0,0 +1 @@
+../../c-rbtree
\ No newline at end of file
diff --git a/src/n-acd/subprojects/c-siphash b/src/n-acd/subprojects/c-siphash
new file mode 120000
index 00000000..70d68818
--- /dev/null
+++ b/src/n-acd/subprojects/c-siphash
@@ -0,0 +1 @@
+../../c-siphash
\ No newline at end of file
diff --git a/src/n-acd/subprojects/libcstdaux-1 b/src/n-acd/subprojects/libcstdaux-1
new file mode 120000
index 00000000..589984f3
--- /dev/null
+++ b/src/n-acd/subprojects/libcstdaux-1
@@ -0,0 +1 @@
+../../c-stdaux
\ No newline at end of file