# Deploy `fips-gateway` (Manual Linux-Host Setup) `fips-gateway` is a separate service that runs alongside the FIPS daemon and bridges a non-FIPS LAN to the FIPS mesh in two independent directions: **outbound** (LAN clients reach mesh services through DNS proxy + virtual-IP NAT) and **inbound** (mesh peers reach LAN services through 1:1 port forwards on `fips0`). This guide covers the **manual Linux-host** deployment path — wiring DNS forwarding, route distribution, and firewall integration on a server or non-OpenWrt router by hand. > **Running OpenWrt?** Use the > [tutorial](../tutorials/deploy-fips-gateway.md) instead. The OpenWrt > ipk ships with the `gateway:` block pre-populated and the init > script automates dnsmasq forwarding, RA route distribution, and the > global IPv6 prefix on `br-lan`. The OpenWrt path is the canonical > deployment of this feature; this how-to is the secondary path for > operators with a different LAN-edge box (a Linux server already > serving DHCP/DNS, a custom router distribution, etc.). For the gateway design (NAT pipeline, virtual IP pool lifecycle, DNS resolution flow), see [../design/fips-gateway.md](../design/fips-gateway.md). For the full `gateway.*` configuration block, see the [Gateway section](../reference/configuration.md#gateway-gateway) of the configuration reference. For the `fips-gateway` binary's CLI flags, see [../reference/cli-fips-gateway.md](../reference/cli-fips-gateway.md). ## The two halves The gateway exposes two independent features that share a common control plane (the same binary, the same nftables table `inet fips_gateway`, the same control socket `/run/fips/gateway.sock`, the same `gateway.*` config block). You can configure either half on its own or both together. - **Outbound gateway** (LAN → mesh). Non-FIPS LAN workstations resolve `.fips` names against the gateway's DNS listener and receive AAAA answers from the gateway's virtual-IP pool. Outbound traffic to those addresses is DNAT'd to the real mesh address and SNAT'd (masqueraded) onto `fips0` under the gateway's mesh identity. The audience is unmodified LAN clients. - **Inbound gateway** (mesh → LAN). A static `(listen_port, proto) → [target_addr]:target_port` table — configured in `gateway.port_forwards[]` — exposes selected LAN services to the mesh as `.fips:`. Mesh peers connect to the gateway's mesh address; the gateway DNATs to the LAN target and masquerades on the LAN side so return traffic flows through conntrack. The audience is mesh peers reaching a service that happens to live on this LAN. The two halves are independent. Configure the outbound half if you want LAN clients to *reach* the mesh; configure the inbound half if you want mesh peers to *reach into* the LAN; configure both if you want both. ## Common gateway-host setup Both halves require the same host preparation. Work through this section first, then jump to whichever half (or both) you need. ### FIPS daemon prerequisites The gateway runs alongside a `fips` daemon on the same host: - The daemon must be running with the TUN adapter enabled (the `fips0` interface must exist). - The daemon's DNS resolver must be enabled (`dns.enabled: true`, default) and reachable from `fips-gateway`. By default that means `[::1]:5354` (IPv6 loopback). The gateway's default `dns.upstream` matches this; a v4 upstream like `127.0.0.1:5354` cannot reach a daemon bound on `[::1]:5354` because Linux IPv6 sockets bound to explicit `::1` do not accept v4-mapped traffic. If the daemon is not yet running with these features, set up the daemon first — see [persistent-identity.md](persistent-identity.md) and [../reference/configuration.md](../reference/configuration.md). ### Kernel sysctls ```sh sudo sysctl -w net.ipv6.conf.all.forwarding=1 sudo sysctl -w net.ipv6.conf.all.proxy_ndp=1 ``` `forwarding` lets the host route IPv6 packets between the LAN interface and `fips0`. `proxy_ndp` lets the gateway answer Neighbor Solicitation requests for virtual-pool addresses so LAN clients can resolve their link-layer addresses (only relevant for the outbound half, but harmless if you only run the inbound half). Persist via a drop-in: ```sh sudo tee /etc/sysctl.d/60-fips-gateway.conf <<'EOF' net.ipv6.conf.all.forwarding = 1 net.ipv6.conf.all.proxy_ndp = 1 EOF sudo sysctl --system ``` ### Capability `fips-gateway` requires `CAP_NET_ADMIN` to manage its nftables table (`inet fips_gateway`) and proxy-NDP entries. The packaged systemd unit (`fips-gateway.service`) runs as root, which satisfies this. For non-package installs, set the file capability: ```sh sudo setcap cap_net_admin+ep /usr/bin/fips-gateway ``` ### Pool route At startup `fips-gateway` adds `local dev lo` to the local routing table. This tells the kernel to accept packets destined for pool addresses as locally-owned, enabling the NAT processing path. The route is cleaned up on shutdown. You do not need to install it manually; if you see "destination unreachable" errors for pool addresses on the gateway host, verify the route is present: ```sh ip -6 route show table local | grep ``` ### Minimum configuration In `/etc/fips/fips.yaml`, populate the `gateway` block with at minimum `enabled: true`, `pool`, and `lan_interface`: ```yaml gateway: enabled: true pool: "fd01::/112" lan_interface: "enp3s0" ``` Pick a pool CIDR that does **not** overlap with any address space in use on the LAN or in the mesh (the FIPS mesh occupies `fd00::/8`; pick a different `fdXX::/N`). The `/112` size yields 65 536 virtual IPs, which is the gateway's hard cap regardless of CIDR width. This minimum config is enough to start the gateway. The `dns.*` block is optional and defaults to `listen: "[::1]:5353"` and `upstream: "[::1]:5354"`. The full block — including `dns.*`, `pool_grace_period`, `conntrack.*`, and `port_forwards[]` — is documented in [../reference/configuration.md#gateway-gateway](../reference/configuration.md#gateway-gateway). ### Start the service ```sh sudo systemctl enable --now fips-gateway ``` Verify the unit came up: ```sh sudo systemctl status fips-gateway sudo journalctl -u fips-gateway -e ``` The startup log will report `Gateway config loaded`, `DNS upstream is reachable`, `Created nftables table 'fips_gateway'`, and finally `fips-gateway running`. The unit's `ExecStartPre` waits up to 30 s for `fips0` to appear, which covers the cold-boot race where the daemon is still bringing up its TUN. ## Configure the outbound half The outbound half lets LAN clients resolve `.fips` names and reach mesh destinations. Three operator decisions are involved: pool CIDR, DNS listen address, and how LAN clients learn the route to the pool and the resolver address. ### Choose the pool CIDR ```yaml gateway: pool: "fd01::/112" ``` Constraints: - Must not overlap with `fd00::/8` (the FIPS mesh address space). - Must not overlap with any LAN-side IPv6 prefix already in use. - `/112` is the practical width — wider just wastes address space because the pool is hard-capped at 65 536 entries. Narrower is fine if you want a smaller pool, but you'll reject DNS lookups faster under churn. ### Choose the DNS listen address ```yaml gateway: dns: listen: "[::1]:5353" upstream: "[::1]:5354" ttl: 60 ``` Common cases: - **Another resolver on the host (the canonical case):** the default `listen: "[::1]:5353"` is loopback-only on an unprivileged port, so it never conflicts with dnsmasq, systemd-resolved, or BIND holding 53. Configure the existing resolver to forward `.fips` queries to `[::1]:5353` and you are done — this is what the OpenWrt ipk does automatically. - **No other resolver on the host:** set `listen: "[::]:53"` explicitly and LAN clients can query the gateway directly. - **systemd-resolved is on port 53:** the default already side-steps this — leave the listen address at `[::1]:5353` and configure the stub or a small forwarder to delegate `.fips` to the gateway. If you would rather have the gateway on 53 directly, disable the systemd stub listener (`DNSStubListener=no` in `/etc/systemd/resolved.conf`) and switch `listen` to `"[::]:53"`. See [troubleshoot-gateway.md](troubleshoot-gateway.md#port-conflict-on-the-dns-listen-port). - **Bind on the LAN address only:** `listen: "192.168.1.1:53"` exposes the resolver only to LAN clients, not loopback. The gateway returns `REFUSED` for any non-`.fips` query — clients that point at it directly need a fallback resolver, or you should front it with a stub forwarder. ### Distribute the route to LAN clients Each LAN client must route the gateway's pool CIDR to the gateway's LAN-side IPv6 address. Three options, in order of preference for production: - **RA Route Information Option** (RFC 4191). If the LAN's RA daemon (`radvd`, `dnsmasq --enable-ra`, OpenWrt's `odhcpd`) supports publishing route options, configure it to advertise the pool CIDR with the gateway as next-hop. Clients pick this up automatically. - **Static route on the LAN router**. If clients route through a central LAN router, add a static route entry there — the router then handles forwarding to the gateway. The exact syntax depends on the router OS. - **Per-host static route** (testing or single-client deployments): ```sh sudo ip -6 route add fd01::/112 via fe80::% # or, if the gateway has a stable global LAN address: sudo ip -6 route add fd01::/112 via ``` ### Distribute the resolver to LAN clients LAN clients also need to send `.fips` queries to the gateway. Two patterns: - **Forward `.fips` from the LAN's main resolver.** If the LAN runs Pi-hole, Unbound, dnsmasq, or systemd-resolved as the central resolver, configure a conditional forward for `fips.`. Unbound example: ```text forward-zone: name: "fips." forward-addr: @53 ``` dnsmasq example: ```text server=/fips/ ``` Clients keep their existing DNS settings; only `.fips` queries are diverted. - **Point clients directly at the gateway.** Simpler for testing, but the gateway returns `REFUSED` for non-`.fips` queries, so each client must also have a fallback resolver configured. ### Verify the outbound path From a LAN client: ```sh dig @ hostname.fips AAAA # Expect an AAAA from the pool CIDR ping6 hostname.fips # Should succeed via the gateway ``` If either step fails, see [troubleshoot-gateway.md](troubleshoot-gateway.md#outbound-half-diagnostics). ## Configure the inbound half The inbound half exposes a LAN-side service to mesh peers. Configured under `gateway.port_forwards[]`: ```yaml gateway: port_forwards: - listen_port: 8080 proto: tcp target: "[fd12:3456::10]:80" - listen_port: 2222 proto: tcp target: "[fd12:3456::20]:22" - listen_port: 5353 proto: udp target: "[fd12:3456::10]:53" ``` Field reference: - `listen_port` — port on the gateway's `fips0` mesh-side address that mesh peers connect to. Must be non-zero. Each `(listen_port, proto)` pair must be unique across the list (the same port on TCP and UDP is allowed; the same port twice on the same proto is rejected at config-load time). - `proto` — `tcp` or `udp`. - `target` — IPv6 LAN destination as `[addr]:port`. IPv4 targets are rejected at parse time by the YAML deserializer (the field is typed `SocketAddrV6`). If the LAN host is reachable only by IPv4, put a small IPv6-aware reverse proxy in front of it on the gateway itself. ### Worked example: HTTP and DNS Suppose the gateway runs on a LAN with an HTTP server at `[fd12:3456::10]:80` and a recursive resolver at `[fd12:3456::10]:53`, and you want mesh peers to reach them as `.fips:8080` (HTTP) and `.fips:5353` (DNS). Add to the gateway's `fips.yaml`: ```yaml gateway: port_forwards: - listen_port: 8080 proto: tcp target: "[fd12:3456::10]:80" - listen_port: 5353 proto: udp target: "[fd12:3456::10]:53" ``` Reload: ```sh sudo systemctl restart fips-gateway ``` From any mesh peer (the host name `gateway` is whatever the gateway's npub maps to in the local `hosts` file or via Nostr advert): ```sh curl http://gateway.fips:8080/ dig @gateway.fips -p 5353 example.com A ``` Each mesh-side request enters `fips0` on the listen port, gets DNAT'd to the LAN target, and the LAN-side masquerade rule rewrites the source to the gateway's LAN address so return traffic flows back through conntrack. ### Compose with the mesh firewall `gateway.port_forwards[]` opens *mesh-side* listeners on `fips0`. If the host's mesh firewall is enabled (see [enable-mesh-firewall.md](enable-mesh-firewall.md)), inbound TCP/UDP on `fips0` for these ports must be permitted in the baseline or via a drop-in. The default baseline allows established/related and ICMPv6 only, so without an explicit allow rule, mesh peers will see TCP RSTs or silent drops on the listen port. A typical drop-in for the worked example: ```nft # /etc/fips/fips.d/gateway-inbound.nft tcp dport 8080 accept udp dport 5353 accept ``` Reload the firewall: ```sh sudo systemctl reload-or-restart fips-firewall.service ``` If the inbound half doesn't need access control beyond the listen port itself, no source filter is needed. To restrict to specific mesh peers, follow the `ip6 saddr tcp dport accept` pattern from the firewall guide. ### Verify the inbound path From a mesh peer (any FIPS node): ```sh curl -v http://.fips:8080/ ``` A successful response confirms the full path: mesh ingress on `fips0`, DNAT to the LAN target, LAN-side masquerade, and conntrack- tracked return. If it fails, see [troubleshoot-gateway.md](troubleshoot-gateway.md#inbound-half-diagnostics). ## Operate and verify `fips-gateway` exposes its own control socket at `/run/fips/gateway.sock`, separate from the daemon's `/run/fips/control.sock`. There is no `fipsctl gateway` subcommand — talk to it directly: ```sh echo '{"command":"show_gateway"}' | sudo nc -U /run/fips/gateway.sock echo '{"command":"show_mappings"}' | sudo nc -U /run/fips/gateway.sock ``` `show_gateway` returns pool counters (`pool_total`, `pool_allocated`, `pool_active`, `pool_draining`, `pool_free`), `nat_mappings`, `dns_listen`, `uptime_secs`, and the active config snapshot. `show_mappings` returns the per-allocation list with virtual IP, mesh address, npub-derived `node_addr`, dns name, state (`Allocated`, `Active`, `Draining`), session count, and ages. For the full schema see [../reference/control-socket.md#gateway-command-catalog](../reference/control-socket.md#gateway-command-catalog). The journal is the other primary signal: ```sh sudo systemctl status fips-gateway sudo journalctl -u fips-gateway -e ``` Expect `MappingCreated`/`MappingRemoved` debug lines as DNS-driven allocations come and go (run with `--log-level debug` to see them), and `Final pool status` on shutdown. Errors in adding NAT rules or proxy-NDP entries surface here. ## See also - [../tutorials/deploy-fips-gateway.md](../tutorials/deploy-fips-gateway.md) — the canonical, package-driven OpenWrt deployment path. - [../design/fips-gateway.md](../design/fips-gateway.md) — gateway design, NAT pipeline, virtual IP pool lifecycle, security considerations. - [Gateway section](../reference/configuration.md#gateway-gateway) of the configuration reference — full `gateway.*` block. - [../reference/cli-fips-gateway.md](../reference/cli-fips-gateway.md) — `fips-gateway` binary CLI flags. - [Gateway command catalog](../reference/control-socket.md#gateway-command-catalog) in the control-socket reference — JSON schema for `show_gateway` and `show_mappings`. - [troubleshoot-gateway.md](troubleshoot-gateway.md) — diagnostic recipes grouped by half. - [enable-mesh-firewall.md](enable-mesh-firewall.md) — mesh-firewall baseline and drop-ins (needed when exposing inbound ports).