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.
This commit is contained in:
Johnathan Corgan
2026-02-13 11:35:24 +00:00
parent 7776c0f4ca
commit 2a37e2716f
11 changed files with 830 additions and 1 deletions
+324
View File
@@ -0,0 +1,324 @@
# 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
```text
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:
```bash
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.
```bash
# 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:
```bash
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.
```bash
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:
```text
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:
```bash
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:
```text
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:
```bash
# 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:
```bash
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:
```bash
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:
>
> ```bash
> 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:
```bash
# 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:
```text
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:
```bash
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:
```bash
sudo ip netns exec fips-a ping6 -c 4 \
npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le.fips
```
### Ping Node A from Node B
```bash
sudo ip netns exec fips-b ping6 -c 4 fd69:e08d:65cc:3a6b:9c2c:2ac4:bd40:5e4b
```
Or with DNS:
```bash
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 resolution**`DNS resolved .fips name, registering identity`
2. **TUN packet**`TUN packet received` with src/dst addresses
3. **Session initiation**`Initiating 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:
```bash
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:
```bash
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:
```bash
sudo ip netns exec fips-a ping -c 1 10.0.0.2
```
If this fails, the namespace setup is incomplete.
### IPv6 disabled
```bash
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:
```bash
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.
+32
View File
@@ -0,0 +1,32 @@
# FIPS Node A configuration
#
# Identity: npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m
# FIPS address: fd69:e08d:65cc:3a6b:9c2c:2ac4:bd40:5e4b
node:
identity:
nsec: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
tun:
enabled: true
name: fips0
mtu: 1280
dns:
enabled: true
bind_addr: "127.0.0.1"
port: 5354
transports:
udp:
bind_addr: "10.0.0.1:4000"
mtu: 1280
peers:
- npub: "npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le"
alias: "node-b"
addresses:
- transport: udp
addr: "10.0.0.2:4000"
priority: 1
connect_policy: auto_connect
+32
View File
@@ -0,0 +1,32 @@
# FIPS Node B configuration
#
# Identity: npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le
# FIPS address: fd8e:302c:287e:b48d:6268:122f:da76:b77
node:
identity:
nsec: "b102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fb0"
tun:
enabled: true
name: fips0
mtu: 1280
dns:
enabled: true
bind_addr: "127.0.0.1"
port: 5354
transports:
udp:
bind_addr: "10.0.0.2:4000"
mtu: 1280
peers:
- npub: "npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m"
alias: "node-a"
addresses:
- transport: udp
addr: "10.0.0.1:4000"
priority: 1
connect_policy: auto_connect