Files
fips/examples/two-node-udp
Johnathan Corgan 2a37e2716f DNS responder for .fips domain, two-node UDP example
Add DNS responder that resolves <npub>.fips queries to FipsAddress IPv6
addresses. Resolution is pure computation (npub → NodeAddr → IPv6) with
identity cache priming as a side effect, enabling subsequent TUN packet
routing to non-peer destinations.

- New dns.rs module: resolve_fips_query(), handle_dns_packet() using
  simple-dns crate, run_dns_responder() async UDP server loop
- DnsConfig in config.rs: enabled, bind_addr (127.0.0.1), port (5354)
- Fourth select! arm in RX event loop for DNS identity channel
- DNS task spawn/abort in node lifecycle
- 10 new tests (420 total)

Add examples/two-node-udp/ with standalone walkthrough for testing two
FIPS nodes in Linux network namespaces over a veth pair. Includes
network diagram, config files, DNS routing setup, and troubleshooting.
2026-02-13 11:35:24 +00:00
..

Two-Node UDP Test

This example demonstrates two FIPS nodes communicating over UDP using Linux network namespaces. Both nodes establish an encrypted peer link, build a spanning tree, and create end-to-end sessions. With TUN and DNS enabled, you can ping6 between nodes using .fips domain names or raw IPv6 addresses.

Network Diagram

            Namespace: fips-a                    Namespace: fips-b
        ┌─────────────────────┐              ┌─────────────────────┐
        │                     │              │                     │
        │  ┌───────────────┐  │              │  ┌───────────────┐  │
        │  │  FIPS Node A  │  │              │  │  FIPS Node B  │  │
        │  │               │  │              │  │               │  │
        │  │  fd69:e08d:.. │  │              │  │  fd8e:302c:.. │  │
        │  └──┬─────────┬──┘  │              │  └──┬─────────┬──┘  │
        │     │         │     │              │     │         │     │
        │  ┌──┴──┐  ┌───┴──┐  │              │  ┌──┴──┐  ┌───┴──┐  │
        │  │fips0│  │  DNS │  │              │  │fips0│  │  DNS │  │
        │  │ TUN │  │:5354 │  │              │  │ TUN │  │:5354 │  │
        │  └─────┘  └──────┘  │              │  └─────┘  └──────┘  │
        │                     │              │                     │
        │  ┌────────────────┐ │              │ ┌────────────────┐  │
        │  │  veth-a        │ │   UDP :4000  │ │        veth-b  │  │
        │  │  10.0.0.1/24   ├─┼──────────────┼─┤  10.0.0.2/24  │  │
        │  └────────────────┘ │              │ └────────────────┘  │
        └─────────────────────┘              └─────────────────────┘

  Transport layer: UDP over IPv4 veth pair (10.0.0.0/24)
  Data plane:      IPv6 over FIPS mesh (fd::/8 via fips0 TUN)
  DNS:             <npub>.fips → FIPS IPv6 address (127.0.0.1:5354)

Prerequisites

  • Linux with network namespace support (requires root for namespace setup)
  • IPv6 enabled (sysctl net.ipv6.conf.all.disable_ipv6 should be 0)
  • iproute2 tools (ip, resolvectl)
  • Rust toolchain (to build the FIPS binary)

Node Identities

Node npub FIPS Address
A npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m fd69:e08d:65cc:3a6b:9c2c:2ac4:bd40:5e4b
B npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le fd8e:302c:287e:b48d:6268:122f:da76:b77

Step 1: Build FIPS

From the FIPS source root:

cargo build

The binary will be at target/debug/fips. Note the absolute path — you'll need it when running inside namespaces.

Step 2: Create Network Namespaces

This creates two namespaces (fips-a and fips-b) connected by a virtual ethernet pair. Each namespace has its own isolated network stack, including its own routing table, TUN devices, and DNS resolver.

# Create namespaces
sudo ip netns add fips-a
sudo ip netns add fips-b

# Create a veth pair connecting them
sudo ip link add veth-a type veth peer name veth-b

# Move each end into its namespace
sudo ip link set veth-a netns fips-a
sudo ip link set veth-b netns fips-b

# Configure IPv4 addresses (used by UDP transport)
sudo ip netns exec fips-a ip addr add 10.0.0.1/24 dev veth-a
sudo ip netns exec fips-b ip addr add 10.0.0.2/24 dev veth-b

# Bring interfaces up
sudo ip netns exec fips-a ip link set veth-a up
sudo ip netns exec fips-b ip link set veth-b up

# Enable loopback in both (needed for DNS responder on 127.0.0.1)
sudo ip netns exec fips-a ip link set lo up
sudo ip netns exec fips-b ip link set lo up

# Enable IPv6 in both namespaces
sudo ip netns exec fips-a sysctl -w net.ipv6.conf.all.disable_ipv6=0
sudo ip netns exec fips-b sysctl -w net.ipv6.conf.all.disable_ipv6=0

Verify connectivity between namespaces:

sudo ip netns exec fips-a ping -c 1 10.0.0.2

Step 3: Start Node A

Open Terminal 1. This runs the FIPS daemon for Node A inside its namespace. The daemon creates a fips0 TUN device, starts the DNS responder, connects to Node B over UDP, and begins the Noise IK handshake.

sudo ip netns exec fips-a \
  env RUST_LOG=info \
  /path/to/target/debug/fips --config /path/to/examples/two-node-udp/fips-a.yaml

Replace /path/to/ with the actual absolute paths to your build and this example directory.

You should see output like:

INFO fips: FIPS starting
INFO fips: Node created:
INFO fips:       npub: npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m
INFO fips:    address: fd69:e08d:65cc:3a6b:9c2c:2ac4:bd40:5e4b
INFO fips: TUN device active:
INFO fips:      name: fips0
INFO fips:   address: fd69:e08d:65cc:3a6b:9c2c:2ac4:bd40:5e4b
INFO fips: DNS responder started for .fips domain
INFO fips: Peer connection initiated (node-b)

Step 4: Start Node B

Open Terminal 2. Run Node B in its namespace:

sudo ip netns exec fips-b \
  env RUST_LOG=info \
  /path/to/target/debug/fips --config /path/to/examples/two-node-udp/fips-b.yaml

Once both nodes are running, you should see handshake completion messages in both terminals:

INFO fips::node::handlers::handshake: Peer promoted to active

The spanning tree will converge within a few seconds (TreeAnnounce exchange), followed by bloom filter exchange (FilterAnnounce).

Step 5: Configure DNS Routing

Each namespace needs its own DNS routing configuration so that .fips queries are sent to the local DNS responder. Open Terminal 3 for these commands.

For Node A's namespace:

# Tell systemd-resolved (inside namespace) to route .fips queries to the
# DNS responder listening on 127.0.0.1:5354 via the fips0 interface.
sudo ip netns exec fips-a resolvectl dns fips0 127.0.0.1:5354
sudo ip netns exec fips-a resolvectl domain fips0 '~fips'

For Node B's namespace:

sudo ip netns exec fips-b resolvectl dns fips0 127.0.0.1:5354
sudo ip netns exec fips-b resolvectl domain fips0 '~fips'

Verify the configuration:

sudo ip netns exec fips-a resolvectl status fips0
sudo ip netns exec fips-b resolvectl status fips0

Note: resolvectl inside a network namespace requires that systemd-resolved is accessible from within the namespace. If your system does not support this, you can test DNS resolution directly with dig instead of relying on the system resolver:

sudo ip netns exec fips-a dig @127.0.0.1 -p 5354 AAAA \
  npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le.fips

Step 6: Test DNS Resolution

Verify that the DNS responder correctly resolves npub names to FIPS addresses. From Terminal 3:

# From Node A, resolve Node B's name
sudo ip netns exec fips-a dig @127.0.0.1 -p 5354 AAAA \
  npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le.fips

# Expected answer: fd8e:302c:287e:b48d:6268:122f:da76:b77

Watch Terminal 1 — you should see a log line:

DEBUG fips::dns: DNS resolved .fips name, registering identity

This confirms the identity cache was populated. The subsequent ping will be able to route through the mesh.

Step 7: Ping Between Nodes

Now test end-to-end connectivity. The first ping triggers session establishment (Noise IK handshake through the mesh), so it may take a moment longer than subsequent pings.

Ping Node B from Node A

From Terminal 3:

sudo ip netns exec fips-a ping6 -c 4 fd8e:302c:287e:b48d:6268:122f:da76:b77

If DNS routing is configured (Step 5), you can use the .fips name instead:

sudo ip netns exec fips-a ping6 -c 4 \
  npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le.fips

Ping Node A from Node B

sudo ip netns exec fips-b ping6 -c 4 fd69:e08d:65cc:3a6b:9c2c:2ac4:bd40:5e4b

Or with DNS:

sudo ip netns exec fips-b ping6 -c 4 \
  npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m.fips

Note: FIPS does not yet implement ICMPv6 Echo Reply (type 129). The first ping will trigger session establishment (visible in daemon logs as SessionSetup/SessionAck messages), but ping replies require Echo Reply support which is not yet implemented. You can verify that the session was established and data was delivered by watching the daemon log output for DataPacket messages.

Step 8: Watch the Logs

While pinging, watch the daemon terminals for the protocol flow:

  1. DNS resolutionDNS resolved .fips name, registering identity
  2. TUN packetTUN packet received with src/dst addresses
  3. Session initiationInitiating session to <node_addr>
  4. SessionSetup sent — Noise IK msg1 sent through mesh
  5. SessionSetup received — Responder processes msg1
  6. SessionAck — Responder sends msg2 back
  7. Session established — Both sides transition to Established
  8. DataPacket — Encrypted IPv6 payload delivered

Set RUST_LOG=debug for the full protocol trace, or RUST_LOG=info for high-level events only.

Cleanup

Stop both FIPS daemons with Ctrl+C in Terminals 1 and 2. Then tear down the namespaces:

sudo ip netns delete fips-a
sudo ip netns delete fips-b

This also removes the veth pair and TUN devices automatically.

Troubleshooting

"Permission denied" creating TUN device

The FIPS binary needs CAP_NET_ADMIN to create TUN devices. Running via sudo ip netns exec already provides root privileges. If running outside a namespace, use:

sudo setcap cap_net_admin+ep /path/to/target/debug/fips

"Address already in use" on DNS port

Another process is using port 5354. Change the dns.port in the YAML config to a different port (e.g., 5355).

No handshake completion

Check that the veth pair is up and the namespaces can reach each other:

sudo ip netns exec fips-a ping -c 1 10.0.0.2

If this fails, the namespace setup is incomplete.

IPv6 disabled

sudo ip netns exec fips-a sysctl net.ipv6.conf.all.disable_ipv6
# Should be 0

DNS query returns no answer

Verify the DNS responder is running by querying it directly:

sudo ip netns exec fips-a dig @127.0.0.1 -p 5354 AAAA \
  npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le.fips

If this works but resolvectl query doesn't, the systemd-resolved routing may not be configured correctly in the namespace.