Files
fips/docs/design/fips-gateway.md
T
Johnathan Corgan 5abf9a9325 docs: four-section /docs/ restructure with new-user content, accuracy pass, and gateway feature-set rewrite
Restructures /docs/ by reader purpose (tutorials, how-to,
reference, design), adds the new-user-progression and
operator-recipe content the prior layout lacked, runs an
accuracy pass against current source across the pre-existing
design docs, and rewrites the gateway feature-set documentation
end-to-end around its actual operational profile (a niche
feature designed for systems already serving DHCP/DNS to a
LAN, with two independent halves — outbound LAN→mesh, inbound
mesh→LAN — sharing one nftables table, one binary, and one
control socket). Top-level README and getting-started rewritten
around two equally-weighted deployment modes (overlay on
existing IP networks; ground-up over non-IP transports).

## Additions

- 11 new tutorials in docs/tutorials/: an 8-step new-user
  progression from single-daemon test-mesh peering through
  to a ground-up two-device mesh, an IPv6-adapter side-trip
  walkthrough, an Advanced Tutorials index, and a hand-held
  OpenWrt walk-through for fips-gateway deployment that
  exercises both halves of the feature.
- 12 new how-tos in docs/how-to/: firewall activation,
  Nostr discovery (resolve / advertise / open across five
  scenarios), Tor onion (directory + control_port modes),
  UDP buffer tuning, unprivileged-user setup, persistent
  identity, host aliases, Bluetooth LE peering, MTU
  diagnostics, manual Linux-host gateway deployment (covers
  both halves), gateway troubleshooting (organised by half),
  and a section index.
- 9 new reference docs in docs/reference/: configuration,
  wire formats, control-socket protocol, four CLI references
  (fips, fipsctl, fipstop, fips-gateway), security posture
  matrix, and Nostr events catalog. Configuration and
  wire-formats are renamed-and-extended from prior design/
  versions; the other seven are net-new.
- 6 new design docs: fips-concepts, fips-architecture, and
  fips-prior-work split out of the deleted fips-intro.md;
  consolidated fips-mmp and fips-mtu aggregations; and a
  new generic port-advertisement-and-nat-traversal doc
  (Nostr-signaled port advertisement plus UDP NAT-traversal
  protocol, FIPS as an example implementation, suitable for
  eventual NIP submission).
- Top-level docs/getting-started.md walking through the
  binary-installer-only Install story.
- packaging/common/hosts pre-populated with the eight public
  test-mesh nodes so shortnames resolve out of the box on
  every fresh install.

## Changes

- 23 wire-format diagrams relocated to reference/diagrams/
  alongside the wire-formats move.
- 4 design diagrams corrected against source code
  (fips-protocol-stack, fips-identity-derivation,
  fips-coordinate-discovery, fips-routing-decision).
- 10 pre-existing design docs reconciled with current
  source. Numeric corrections: stale link-MMP report bounds
  (now [1s, 5s] with 200 ms cold-start floor); UDP default
  MTU (now 1280, IPv6 minimum); node_addr formula
  (SHA-256(pubkey)[..16]); Noise patterns (IK at link, XK
  at session); peer-ACL semantics (strict allowlist requires
  ALL in peers.deny); daemon DNS upstream ([::1]:5354);
  on-the-wire bloom-filter size (1,071 bytes); obsolete
  Cargo-feature references (PR #79 dropped them) removed.
- Transport framing tightened across the docs: TCP is for
  UDP-filtered networks (not NAT traversal); Tor is a
  deployment mode (not failover); WebSocket dropped (not a
  shipped FIPS transport); WiFi promoted to Implemented via
  Ethernet in infrastructure mode; classic-Bluetooth row
  removed (BLE is the only Bluetooth-mode transport).
- docs/design/fips-gateway.md rewritten end-to-end to lead
  with the niche-feature framing and the two-halves
  structure. Title moved from "FIPS Outbound LAN Gateway"
  to "FIPS Gateway"; architecture section describes the
  common machinery (the fips-gateway service, the nftables
  table, the control socket) before splitting into separate
  "Outbound Half" and "Inbound Half" sections of equal
  weight; security considerations split per-half; no Future
  Work section (speculative directions live in the project
  tracker, not in protocol design docs). Inbound port
  forwarding is a first-class half rather than a buried
  "Implemented Extensions" subsection.
- Gateway terminology unified across all gateway docs as a
  separate Linux service running alongside the fips daemon
  (its own systemd unit / OpenWrt init script). Container-
  pattern terms (sidecar) are reserved for the
  Docker/Kubernetes sidecar deployment examples — the
  testing/sidecar/ tree, examples/k8s-sidecar/,
  examples/sidecar-nostr-relay/,
  examples/wireguard-sidecar-macos/, and the related
  CHANGELOG / top-level README entries — where the term
  carries its standard container meaning.
- Net-new design body content: rekey section in
  fips-mesh-layer (Noise IK msg1/msg2 over the established
  link, K-bit cutover, drain window, smaller-NodeAddr-wins
  tie-breaker on dual-init); Mesh Size Estimation and
  Antipoison FPR Cap sections in fips-bloom-filters;
  Mesh-Interface Query Filter subsection in
  fips-ipv6-adapter; failure-suppression knobs and clock-
  skew tolerance in fips-nostr-discovery; loop-rejection
  and mid-chain ancestor swap added to spanning-tree
  propagation / stability rules; Priority Chain in
  fips-mesh-operation renumbered to match the
  routing-decision diagram.
- Top-level README: dropped the stale nostr-discovery
  cargo-feature parenthetical. docs/README.md and the four
  section READMEs (tutorials, how-to, reference, design)
  refreshed for the new structure; index rows reflect both
  halves of the gateway feature and the new fips-gateway
  CLI reference.
- Cargo.toml [package.metadata.deb] assets path updated for
  the fips-security.md move; .gitignore /reference/ rule
  anchored to repo root so docs/reference/ is trackable.
- packaging/openwrt-ipk/files/etc/fips/fips.yaml
  configuration-doc URL updated to the new
  docs/reference/configuration.md location.

## Deletions

- docs/design/fips-intro.md (split into the three new intro
  design docs).
- docs/design/document-relationships.svg (orphan, no longer
  referenced).
- docs/proposals/ tree removed; the only proposal it
  contained (the Nostr UDP hole-punch protocol) was
  rewritten as the new generic
  design/port-advertisement-and-nat-traversal.md.
2026-05-08 03:02:12 +00:00

24 KiB

FIPS Gateway

The FIPS gateway lets unmodified IPv6 hosts on a LAN exchange traffic with the mesh without running any FIPS software themselves. It is a niche feature — most operators will never enable it. The gateway runs most conveniently on a system that is already providing network services (DHCP, DNS, RA) to a LAN segment, since hosts on that segment already get IP assignment and a default route from that box. The canonical example is an OpenWrt-based WiFi access point: every client that associates with the AP already has the AP as default router and DNS server, which is exactly the placement the gateway needs. The OpenWrt ipk ships with the gateway: block of /etc/fips/fips.yaml pre-populated and the integration glue (dnsmasq forwarding, RA route for the virtual pool, global-scope IPv6 prefix on br-lan) automated by the init script — packaging/openwrt-ipk/files/etc/init.d/fips-gateway. The operator only needs to enable and start the service. Running the gateway on a non-OpenWrt LAN-edge host (a Linux router/server, for example) is technically possible but requires manual integration: distributing a route to the virtual-IP pool, wiring DNS forwarding so LAN clients send .fips queries to the gateway, configuring sysctls and capabilities. That path is supported but tedious; it is the secondary path.

The feature has two halves that share common machinery and have their own unique parts.

The outbound half carries traffic from LAN to mesh. A non-FIPS LAN workstation resolves <npub>.fips (or a .fips host alias) via the gateway's DNS proxy, which returns a virtual IPv6 address from a managed pool. The kernel routes the LAN packet to that virtual IP via a route to the pool CIDR (RA-advertised, statically distributed, or on-link via the default route). The gateway runs nftables NAT so the packet appears on the mesh as if it had originated from the gateway's own FIPS identity: prerouting DNAT rewrites the destination from the virtual IP to the real fd00::/8 mesh address, and postrouting masquerade rewrites the source from the LAN host's address to the gateway's fips0 address. Return traffic follows the conntrack reverse path back to the originating LAN host, with postrouting SNAT restoring the virtual IP as source so the client sees a response from the address it connected to.

The inbound half carries traffic from mesh to LAN. A configuration entry in gateway.port_forwards[] exposes a LAN service (host:port) on a port of the gateway's mesh-side fips0 address. Mesh peers reach it as <gateway-npub>.fips:<listen_port>. A prerouting DNAT rule keyed on (iif=fips0, l4proto, dport) rewrites the destination to the LAN target; a LAN-side masquerade in postrouting rewrites the mesh peer's source so the LAN target sees a reachable LAN address and conntrack steers replies back through the gateway. This is the inverse of port-forwarding on a conventional NAT router.

The two halves are independent and can be configured separately. Inbound port-forwards work without any outbound configuration (just a port-forward list and the table); outbound works without any inbound forwards. They share the same nftables table, the same binary, the same control socket, and the same atomic-rebuild strategy. That shared machinery is what makes them halves of one feature rather than two separate features.

Architecture

The fips-gateway Service

The gateway is a separate binary, fips-gateway, not part of the FIPS daemon. It reads the same /etc/fips/fips.yaml the daemon reads (via --config, or the standard search path), but acts on the gateway.* block. It needs CAP_NET_ADMIN to install nftables rules, manage proxy NDP entries, and add the pool route. The CLI is documented in ../reference/cli-fips-gateway.md.

The gateway connects to the daemon indirectly. The outbound half forwards .fips DNS queries to the daemon's built-in resolver (default [::1]:5354); the daemon resolves the name to a mesh address and primes its identity cache as a side effect. The inbound half does not require any daemon plumbing at all — packets that arrive on fips0 after the daemon's TUN injection path are matched by the nftables rules on fips0 ingress. There is no shared memory, no IPC channel, and no startup ordering coupling beyond "the daemon's DNS responder must be reachable before the gateway starts serving LAN queries", which the gateway enforces with a bounded reachability probe at startup.

nftables Table Layout

All gateway rules live in a single nftables table, inet fips_gateway, with two chains:

  • preroutingtype nat hook prerouting priority dstnat (-100), for both LAN→mesh DNAT (per virtual-IP mapping) and mesh→LAN DNAT (per port-forward).
  • postroutingtype nat hook postrouting priority srcnat (100), for both the always-on oifname fips0 masquerade, the per-mapping return-path SNAT, and (when any port-forward is configured) the LAN-side masquerade for inbound traffic.

The table is rebuilt atomically on every change. The rebuild sequence — delete the existing table (ignore ENOENT on first call), then create a new table with chains and the full rule set in a single netlink batch — avoids reliance on kernel rule-handle tracking, which the rustables crate does not expose. The table stays small (one always-on masquerade plus two rules per active outbound mapping plus one rule per inbound forward, with one extra masquerade when any forward is present), so rebuilds are cheap.

Control Socket

fips-gateway exposes a Unix-domain control socket at /run/fips/gateway.sock (root:fips, mode 0770) with two commands: show_gateway and show_mappings. The protocol is the same line-delimited JSON used by the daemon's control socket. The shapes are documented in the Gateway command catalog. There is no fipsctl gateway subcommand; clients (including fipstop's gateway view) talk to the socket directly.

Diagram

                           LAN clients
                              │
        DNS query (.fips)     │     IPv6 packet
        for outbound          │     to virtual IP
                              │     or mesh peer
                              ▼
            ┌───────────────────────────────────┐
            │           fips-gateway            │
            │                                   │
            │  ┌──────────────┐  ┌───────────┐  │
            │  │   DNS proxy  │  │  Virtual  │  │
            │  │   ([::]:53)  │─▶│  IP pool  │  │
            │  │   .fips only │  │ (state    │  │
            │  └──────┬───────┘  │  machine) │  │
            │         │          └─────┬─────┘  │
            │         │                │        │
            │  forward to              │ pool   │
            │  daemon resolver         │ events │
            │  ([::1]:5354)            ▼        │
            │         │          ┌───────────┐  │
            │         │          │   NAT     │  │
            │         │          │  manager  │  │
            │         │          │ (rebuild  │  │
            │         │          │  inet     │  │
            │         │          │  fips_    │  │
            │         │          │  gateway) │  │
            │         │          └─────┬─────┘  │
            │         │                │        │
            │         │          ┌─────▼─────┐  │
            │         │          │   net     │  │
            │         │          │  setup    │  │
            │         │          │ (proxy    │  │
            │         │          │  NDP, lo  │  │
            │         │          │  route)   │  │
            │         │          └───────────┘  │
            │         │                         │
            │         │   control socket        │
            │         │   /run/fips/            │
            │         │   gateway.sock          │
            └─────────┼─────────────────────────┘
                      │
                      ▼
              FIPS daemon resolver
              ([::1]:5354)
                      │
                      ▼
                fips0 TUN interface
                      │
                      ▼
                  the mesh

The DNS proxy and the virtual IP pool are exclusive to the outbound half. The NAT manager and the kernel-side machinery (nftables table, fips0 and LAN interfaces, conntrack) are shared. The inbound half contributes per-port-forward rules to the same table without involving the DNS proxy or the pool.

The Outbound Half (LAN → Mesh)

DNS Resolution Flow

  1. A LAN client sends a DNS query to the gateway's listener (default [::]:53, configurable via gateway.dns.listen).
  2. If the question is not for a .fips domain, the gateway replies REFUSED. The proxy is intentionally narrow — it does not resolve public DNS, and the LAN's primary resolver should hold port 53 on the gateway host (the OpenWrt init script wires this up by binding the gateway listener to a non-conflicting port and configuring dnsmasq to forward .fips queries there).
  3. The gateway forwards the query to the daemon resolver (gateway.dns.upstream, default [::1]:5354). The daemon must match: an IPv6 socket bound to [::1] does not accept v4-mapped traffic, so a 127.0.0.1:5354 upstream cannot reach a daemon bound on [::1]:5354.
  4. If the daemon is unreachable or times out (5 s), the gateway replies SERVFAIL. If the daemon returns NXDOMAIN or a non-AAAA answer, the gateway forwards the response unchanged.
  5. The gateway extracts the AAAA (fd00::/8) record from the daemon's response. This resolution primes the daemon's identity cache as a side effect — a prerequisite for fips0 routing, because the daemon needs the cache entry to map the mesh address back to a NodeAddr for forwarding.
  6. The gateway allocates a virtual IP from the pool for that mesh address (idempotent: an existing mapping is reused and its TTL refreshed).
  7. If a new mapping was created, the pool emits MappingCreated, which the main loop turns into add_mapping calls on the NAT manager and add_proxy_ndp on the network setup.
  8. The gateway returns an AAAA response containing the virtual IP, with the configured TTL (default 60 s).

Virtual IP Pool

The pool allocates IPv6 addresses from a configured CIDR (default fd01::/112). Each address maps to one mesh destination, keyed by NodeAddr rather than by hostname — different .fips aliases for the same node share a virtual IP. Address 0 (the network-equivalent) is reserved; the rest are allocatable. The pool is capped at 2^16 addresses regardless of prefix length, to bound memory.

The pool tracks state per address:

Allocated ──→ Active ──→ Draining ──→ Free
    │                                  ▲
    └──────────────────────────────────┘
        (TTL expired, no sessions)
State Meaning
Allocated DNS query created the mapping; no NAT sessions yet.
Active Conntrack reports at least one session for this virtual IP.
Draining TTL has expired; sessions may still be in progress, or grace period is running after sessions ended.
Free Reclaimed and available for new allocations.

Transitions:

  • Allocated → Active: conntrack sessions count goes above zero.
  • Allocated → Free: TTL expires before any session is ever observed.
  • Active → Draining: TTL expires (sessions may or may not still be present).
  • Draining → Free: session count is zero and the grace period has elapsed since draining began.

Timing:

  • TTL (gateway.dns.ttl, default 60 s) is both the DNS TTL returned to the client and the mapping's idle lifetime. Repeated DNS queries for the same destination refresh the last_referenced timestamp.
  • Grace period (gateway.pool_grace_period, default 60 s) is the dwell time after the last session ends before the address is recycled. It prevents immediate reuse from confusing hosts with cached DNS responses.
  • Tick interval: the pool re-evaluates state every 10 s.

Active session counts come from /proc/net/nf_conntrack: an entry counts as a session if its original destination is the virtual IP.

If the pool is exhausted, new DNS queries return SERVFAIL. Existing mappings are never evicted prematurely — the correctness of in-flight sessions takes precedence over fresh allocations.

NAT Pipeline (Outbound)

Three rule classes in inet fips_gateway together implement the LAN→mesh path:

Prerouting DNAT (per mapping) rewrites the destination from the virtual IP to the corresponding mesh address:

match:  nfproto ipv6 && ip6 daddr == <virtual_ip>
action: dnat to <mesh_addr>

After DNAT, the kernel routes the packet through fips0 via the standard routing table.

Postrouting masquerade (oifname fips0) rewrites the source of all traffic exiting via fips0 to the gateway's own fips0 address:

match:  oifname == "fips0"
action: masquerade

This rule is critical. Without it, LAN client source addresses (for example fd02::20 from the LAN's RA-advertised prefix, or virtual addresses from another forwarding domain) would appear as the source on the mesh. Those addresses are meaningless to mesh nodes, so return traffic would be black-holed. Masquerade ensures all mesh traffic appears to originate from the gateway's own FIPS identity.

Postrouting SNAT (per mapping) rewrites the source of return traffic from the mesh address back to the virtual IP:

match:  nfproto ipv6 && ip6 saddr == <mesh_addr>
action: snat to <virtual_ip>

Without it, the LAN client would see replies from the raw fd00::/8 mesh address rather than from the virtual IP it had originally connected to, breaking application-layer assumptions about the destination address.

Network Requirements (Outbound)

The gateway host needs IPv6 forwarding enabled (net.ipv6.conf.all.forwarding=1), proxy NDP enabled on the LAN interface, CAP_NET_ADMIN for fips-gateway, and a local <pool-cidr> dev lo route so the kernel accepts packets to the pool as locally owned and runs them through the NAT chains. LAN clients need a route to the pool via the gateway and DNS resolution that forwards .fips queries there. On OpenWrt the init script handles all of this; on other Linux hosts the operator handles it manually. Full setup is documented in ../how-to/deploy-gateway.md.

The Inbound Half (Mesh → LAN)

Configuration Shape

Inbound port-forwards live in gateway.port_forwards[]. Each entry is a triple:

Field Type Notes
listen_port u16 Port on the gateway's fips0 address. Must be non-zero.
proto tcp | udp Match protocol.
target [ipv6]:port LAN destination. IPv4 targets are rejected at parse time by SocketAddrV6.

Validation runs at startup and on every config reload: (listen_port, proto) must be unique across the list, and zero listen ports are rejected. Forwards are independent of outbound configuration: a gateway with no pool consumers can still expose inbound services (the pool route and DNS proxy still run, since they are part of the same binary, but they sit idle).

NAT Pipeline (Inbound)

For each port-forward, a single prerouting DNAT rule matches mesh-originated traffic landing on the gateway's fips0 address and rewrites it to the LAN target:

match:  iifname == "fips0" && nfproto ipv6
        && l4proto == <tcp|udp> && th dport == <listen_port>
action: dnat to <target_ip>:<target_port>

The match clause is deliberately narrow:

  • iifname == "fips0" restricts the rule to traffic that arrived from the mesh. LAN-side ingress is never subject to inbound forwarding.
  • nfproto ipv6 is enforced both here and at config-load time (SocketAddrV6 rejects IPv4 targets); FIPS is IPv6-only end to end.
  • l4proto + dport narrows the match to one (listen_port, proto) pair per rule. Unique-tuple validation ensures no two rules contend for the same packet.

When any port-forward is configured, a single LAN-side masquerade is added to postrouting:

match:  iifname == "fips0" && oifname == <lan_interface>
        && nfproto ipv6
action: masquerade

Without this rule, the LAN target would attempt to reply directly to the mesh peer's fd00::/8 source address, which is not reachable on the LAN. Masquerade rewrites the source to the gateway's LAN-side address so the target sees a reachable peer and conntrack routes the reply back through the gateway.

This LAN-side masquerade is independent of the oifname fips0 masquerade in the outbound pipeline; the two have disjoint match clauses (different iifname/oifname combinations) and coexist without interaction when both directions are active.

Independence From Outbound

The inbound half does not require:

  • A virtual-IP pool. Mesh peers connect directly to the gateway's own fips0 address, which the FIPS daemon already owns.
  • DNS resolution. Mesh peers reach the gateway as <gateway-npub>.fips:<port> using their own resolver (or a numeric mesh address); the gateway's DNS proxy is not in the path.
  • A daemon-side identity cache for the LAN target. The target is a LAN-side IPv6 address, not a mesh address; no fd00::/8 lookup happens for it.

A gateway configured with port-forwards but with no LAN clients ever issuing .fips DNS queries will have an empty pool and zero outbound mappings, but its inbound forwards work normally. The inverse is also true: a gateway that serves only outbound LAN→mesh traffic has zero entries in the port-forwards list and no LAN-side masquerade.

Atomic Table Rebuild (Common)

Both halves contribute rules to the same inet fips_gateway table, and that table is rebuilt as one unit on every state change — mapping added, mapping removed, port-forwards updated. The rebuild sequence is:

  1. Delete the existing table in its own batch (ignore ENOENT).
  2. In a fresh batch: add the table; add the prerouting and postrouting chains; add the always-on oifname fips0 masquerade; add per-mapping DNAT/SNAT rules for every active pool entry; add per-port-forward DNAT rules; add the LAN-side masquerade if any port-forwards exist.
  3. Send the batch as a single netlink transaction.

The rustables crate does not expose rule-handle tracking, so incremental update of individual rules is not available. Atomic rebuild was chosen for simplicity and correctness: it eliminates an entire class of partial-update inconsistency bugs at the cost of repeating the (cheap) rule construction on every change. The total rule count is bounded by the pool capacity (2 per mapping, capped at 2^16) and the port-forward count, both of which are small in practice.

Configuration Reference

The full gateway.* block — pool CIDR, LAN interface, DNS listen/upstream/TTL, pool grace period, conntrack timeouts, and inbound port-forwards — is documented in the Gateway section of the configuration reference. The same block governs both halves; fields specific to one half (pool, dns.* for outbound; port_forwards[] for inbound) are simply unused when the other half is not in play.

Operations and Troubleshooting

Security Considerations

Outbound

  • LAN trust boundary. The DNS listener and the virtual-IP pool are reachable by every host on the LAN. Any LAN host that can resolve .fips and route to the pool CIDR can reach mesh destinations. There is no per-client authentication; access restriction is a network-level concern, enforced with firewall rules on the LAN interface or on the gateway host itself.
  • Identity masking. All outbound LAN traffic appears on the mesh under the gateway's own FIPS identity. Mesh nodes cannot determine which LAN host originated a connection. This provides privacy for LAN hosts but means the gateway's reputation covers all of its clients — and that abusive behavior from one LAN host is attributed to the gateway, not to the host.
  • Plaintext between client and gateway. Traffic between the LAN client and the gateway is unencrypted at the IP layer. FIPS encryption (FSP) protects the segment between the gateway and the destination mesh node; application-layer encryption (TLS, SSH, Noise) is the only thing that provides true end-to-end protection through the gateway.
  • Pool addresses are ephemeral. Virtual IPs are allocated dynamically and recycled. They are not authenticated and not bound to client identity — a LAN host connecting to a virtual IP is trusting the gateway's recent DNS response.
  • DNS upstream trust. The outbound half's correctness depends on the FIPS daemon's resolver returning honest fd00::/8 answers; a compromised daemon could redirect LAN clients to arbitrary mesh nodes.

Inbound

  • Port exposure. Each entry in port_forwards[] exposes the matched (listen_port, proto) on the gateway's mesh-side address to every reachable mesh peer. Inbound port-forwards are not gated by any peer ACL beyond what FMP normally enforces; treat them with the same care as a public-internet port forward.
  • Mesh peer trust. The LAN target sees connections that have been masqueraded to the gateway's LAN address. The target cannot distinguish one mesh peer from another, and there is no authenticated peer identity available to the LAN target — any application-layer authentication or rate-limiting must run on the target itself.
  • Return-path masquerade exposes the gateway's LAN address. The LAN-side masquerade rewrites the mesh peer's source to the gateway's LAN address. A malicious or buggy LAN target can use this to send unsolicited traffic back at the gateway, or to probe other LAN hosts via the gateway's network position; LAN segmentation (VLANs, host firewalls) is the right control.

Common

  • No client identity verification. The gateway authenticates neither LAN clients nor mesh peers beyond what the underlying layers already do — fips0 ingress carries an FSP-authenticated payload, the LAN side is whoever the LAN admits.

References