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.
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_ipv6should be0) iproute2tools (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:
resolvectlinside a network namespace requires thatsystemd-resolvedis accessible from within the namespace. If your system does not support this, you can test DNS resolution directly withdiginstead 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
DataPacketmessages.
Step 8: Watch the Logs
While pinging, watch the daemon terminals for the protocol flow:
- DNS resolution —
DNS resolved .fips name, registering identity - TUN packet —
TUN packet receivedwith src/dst addresses - Session initiation —
Initiating session to <node_addr> - SessionSetup sent — Noise IK msg1 sent through mesh
- SessionSetup received — Responder processes msg1
- SessionAck — Responder sends msg2 back
- Session established — Both sides transition to Established
- 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.