Files
fips/testing/acl-allowlist
Johnathan Corgan 1eb0e8a34e Move the TUN and DNS child start and stop bodies into ipv6tun
Bringing the TUN device and the .fips DNS responder up and taking them
down is host-side work, but the bodies sat inline in the node's
supervisor arms, and their handles were eight loose fields on the
supervisor. Move the bodies to ipv6tun::lifecycle and gather the
handles into one Handles struct there, held by the supervisor. The
supervisor arms, their order and the child-exit reporting are
unchanged; each arm now calls into ipv6tun.

The TUN start is two calls so the node can refresh its MSS ceiling
between them, exactly where it did before: open_tun creates and logs
the device, then spawn_tun creates the macOS/FreeBSD shutdown pipe and
starts the writer and reader threads. A failure to create the device
still continues without a TUN, and a pipe or writer failure still fails
the node's start. stop_tun and stop_dns carry the teardown unchanged,
including the shutdown-pipe write that wakes the reader on macOS and
FreeBSD.

The TUN device name moves into Handles as well, so the teardown up-set
can ask ipv6tun whether each child is up. A TUN counts as up when it
has a device name, not when it has a sender, so an app-owned TUN still
produces no TUN teardown; DNS counts as up while its task handle
exists. Node::tun_name, tun_tx, dns_local_addr and
enable_app_owned_tun keep their behaviour and now read or write the
handles. Node::mesh_ifindex had no caller left outside a test and is
replaced by the same method on Handles. Tests install a TUN sender
through a test-only Node::install_tun.

The moved log lines now log under fips::ipv6tun::lifecycle instead of
fips::node::lifecycle. Add that target to the NAT harness and its trace
overlay, and to the harnesses that relied on fips::node=debug, and note
the rename in the changelog.
2026-09-24 14:45:51 +00:00
..

ACL Allowlist Test

Six Docker nodes use per-node ACL files mounted at the hardcoded runtime paths:

  • node-a and node-b carry the insider allowlist (node-a, node-b, node-e, node-f)
  • node-c and node-d each carry a broad allowlist containing every node alias
  • node-e and node-f do not mount any ACL files locally
  • every node gets a generated /etc/fips/hosts with aliases for node-a through node-f

This lets us test three different node behaviors at once:

  • insiders (a, b) explicitly allow a, b, e, and f
  • outsiders (c, d) allow everyone locally, but still cannot join because insiders reject them
  • allowed remotes (e, f) rely on the insider ACLs and do not need local ACL files

Test Identities

Allowed:

  • node-a
    • npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m
    • 0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20
  • node-b
    • npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le
    • b102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fb0

Denied:

  • node-c
    • npub1cld9yay0u24davpu6c35l4vldrhzvaq66pcqtg9a0j2cnjrn9rtsxx2pe6
    • c102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fc0
  • node-d
    • npub1n9lpnv0592cc2ps6nm0ca3qls642vx7yjsv35rkxqzj2vgds52sqgpverl
    • d102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fd0

Additional allowed:

  • node-e
    • npub1x5z9rwzzm26q9verutx4aajhf2zw2pyp34c6whhde2zduxqav40qgq36l6
    • nsec1egyrmekfw3u4l88v8zhrak9uht503s2kvn9v49tqgp6c5l2yuxgsv386l0
  • node-f
    • npub1ytrut7gjncn2zfnhn56c0zgftf0w6p99gf6fu8j73hzw5603zglqc9av6c
    • nsec1afh3nysthqh47awpdewcw59wvvp499f8dvlyclmnv4gvpxdk56dsa6eqsn

The generated fips.key fixtures use a mix of bare hex and nsec1... values. FIPS accepts either format in key files.

Run

Build the Linux binaries and test image:

./testing/scripts/build.sh --no-docker

Start the ACL test mesh:

./testing/acl-allowlist/generate-configs.sh
docker compose -f testing/acl-allowlist/docker-compose.yml up -d --build

Or run the full integration check:

./testing/acl-allowlist/test.sh

test.sh regenerates the ACL fixtures automatically before starting Docker. The generated ACL files use alias names, and the generated hosts file makes those aliases resolvable at runtime.

The ACL harness pins the expected test entrypoint explicitly so it does not accidentally reuse an older fips-test:latest image with a different startup script.

Docker service/container/hostname identifiers in this harness intentionally use service-*, fips-acl-container-*, and host-* names so they do not collide with the logical FIPS aliases node-a through node-f. For data-plane checks and operator examples, use the explicit FIPS names such as node-a.fips and node-d.fips.

The bridge network requests no subnet, so docker assigns one from its own address pool and two concurrent runs of this harness never contend for a fixed range. Consequently no node's IPv4 address is known before startup, and the generated peer stanzas address each other by docker hostname (host-a … host-f), resolved through the container's dnsmasq to docker's embedded DNS.

ACL paths are fixed in this branch:

  • /etc/fips/peers.allow
  • /etc/fips/peers.deny

Mounted ACL files in this harness:

  • node-a and node-b: insider allowlist plus ALL deny fallback
  • node-c and node-d: broad local allowlist used by outsider nodes trying to blend in
  • node-e and node-f: no ACL files mounted
  • all nodes: /etc/fips/hosts aliases for node-a through node-f

Generated fixture location:

  • testing/acl-allowlist/generated-configs/, or generated-configs<suffix>/ when FIPS_CI_NAME_SUFFIX is set, which is how concurrent runs keep their fixtures apart

Inspect peer state:

docker exec fips-acl-container-a fipsctl show peers
docker exec fips-acl-container-b fipsctl show peers
docker exec fips-acl-container-c fipsctl show peers
docker exec fips-acl-container-d fipsctl show peers
docker exec fips-acl-container-e fipsctl show peers
docker exec fips-acl-container-f fipsctl show peers

Inspect the loaded ACL state directly:

docker exec fips-acl-container-a fipsctl acl show

Use explicit .fips names when checking reachability through the FIPS overlay:

docker exec fips-acl-container-a ping node-d.fips
docker exec fips-acl-container-a ping npub1n9lpnv0592cc2ps6nm0ca3qls642vx7yjsv35rkxqzj2vgds52sqgpverl.fips

The output shows both the original ACL file entries and the resolved effective npub entries.

Expected:

  • node-a sees node-b, node-e, and node-f
  • node-b sees node-a
  • node-c sees no peers
  • node-d sees no peers
  • node-e sees node-a
  • node-f sees node-a

Visible rejection logs:

docker compose -f testing/acl-allowlist/docker-compose.yml logs -f service-a service-b service-c service-d service-e service-f

On startup, node-c and node-d immediately try their configured outbound static connection to node-a. Their own ACLs permit that attempt, but the insider ACL on node-a still rejects both peers. Because node-a also has static peer stanzas for node-c and node-d, you may see both outbound_connect and inbound_handshake rejection messages during startup. The outsider-initiated path emits messages like:

Rejected peer by ACL ... context=inbound_handshake decision=denylist match

Those messages are now emitted at debug level. This harness enables fips::node=debug in RUST_LOG so the ACL rejection details stay visible in test logs, and operators can temporarily raise log level the same way when diagnosing ACL issues locally.

A later ping6 from node-c.fips does not emit a new inbound_handshake message. The ping uses the data-plane session path, and since no peer session to node-a.fips was established, it just times out.

Stop and clean up:

docker compose -f testing/acl-allowlist/docker-compose.yml down