gateway: change dns.listen default to [::1]:5353

The gateway is designed for systems already serving DHCP and DNS to
a LAN segment (canonically an OpenWrt AP). On those systems port 53
is already taken by the existing resolver, so the prior `[::]:53`
default conflicted with the gateway's intended deployment target out
of the box.

The OpenWrt ipk previously overrode this in its packaged config as a
workaround; matching the source default to what the canonical
deployment actually wants makes the override redundant and removes a
foot-gun for fresh manual Linux-host installs. The redundant
`dns.listen` line in `packaging/openwrt-ipk/files/etc/fips/fips.yaml`
is dropped along with this change.

Operators on a host without a pre-existing resolver on port 53 can
opt back into the wildcard bind by setting `dns.listen: "[::]:53"`
explicitly. The new default binds IPv6 loopback only — Linux IPv6
sockets bound to explicit `::1` do not accept v4-mapped traffic, so
forwarders that reach the gateway over IPv4 loopback need to be
pointed at an explicit IPv4 listen address instead.

Touches the gateway config struct and its default-value test, the
commented-out gateway example in the Debian common fips.yaml, the
OpenWrt ipk config (override removed), the gateway reference /
how-to / design / tutorial / troubleshoot docs, and a CHANGELOG
entry under [Unreleased] -> Changed.
This commit is contained in:
Johnathan Corgan
2026-05-08 14:02:05 +00:00
parent 9112c8f7f0
commit 0fcf0f6f8f
9 changed files with 77 additions and 47 deletions
+11
View File
@@ -302,6 +302,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
decrypt-failure thresholds, STUN parser, gateway, NAT traversal, decrypt-failure thresholds, STUN parser, gateway, NAT traversal,
packaging install paths) alongside CI-side hardening for the new packaging install paths) alongside CI-side hardening for the new
Windows and macOS platforms. Windows and macOS platforms.
- Gateway `dns.listen` source default changed from `[::]:53` to
`[::1]:5353` to match the canonical deployment model (a host
already serving DHCP/DNS to a LAN segment, where port 53 is
taken by the existing resolver and `.fips` queries are forwarded
to the gateway over loopback). The OpenWrt ipk previously
overrode this in its packaged config; the override is now
redundant and has been dropped. Operators on a host without a
pre-existing resolver on port 53 can opt back into the wildcard
bind by setting `dns.listen: "[::]:53"` explicitly. The new
default binds IPv6 loopback only — forwarders that reach the
gateway over IPv4 loopback need an explicit IPv4 listen address.
### Fixed ### Fixed
+10 -5
View File
@@ -130,7 +130,7 @@ There is no `fipsctl gateway` subcommand; clients (including
│ │ │ │
│ ┌──────────────┐ ┌───────────┐ │ │ ┌──────────────┐ ┌───────────┐ │
│ │ DNS proxy │ │ Virtual │ │ │ │ DNS proxy │ │ Virtual │ │
│ │ ([::]:53) │─▶│ IP pool │ │ │ │ ([::1]:5353) │─▶│ IP pool │ │
│ │ .fips only │ │ (state │ │ │ │ .fips only │ │ (state │ │
│ └──────┬───────┘ │ machine) │ │ │ └──────┬───────┘ │ machine) │ │
│ │ └─────┬─────┘ │ │ │ └─────┬─────┘ │
@@ -182,13 +182,18 @@ involving the DNS proxy or the pool.
### DNS Resolution Flow ### DNS Resolution Flow
1. A LAN client sends a DNS query to the gateway's listener (default 1. A LAN client sends a DNS query to the gateway's listener (default
`[::]:53`, configurable via `gateway.dns.listen`). `[::1]:5353`, configurable via `gateway.dns.listen`). The default
is loopback-only on an unprivileged port: the canonical deployment
has another resolver on the host (dnsmasq, systemd-resolved, BIND)
holding port 53 and forwarding `.fips` queries to the gateway over
loopback. Operators on a host without a pre-existing resolver on
53 can override the listen value to `"[::]:53"` to let LAN clients
query the gateway directly.
2. If the question is not for a `.fips` domain, the gateway replies 2. If the question is not for a `.fips` domain, the gateway replies
`REFUSED`. The proxy is intentionally narrow — it does not resolve `REFUSED`. The proxy is intentionally narrow — it does not resolve
public DNS, and the LAN's primary resolver should hold port 53 on 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 host (the OpenWrt init script wires dnsmasq to forward
the gateway listener to a non-conflicting port and configuring `.fips` queries to the loopback listener automatically).
dnsmasq to forward `.fips` queries there).
3. The gateway forwards the query to the daemon resolver 3. The gateway forwards the query to the daemon resolver
(`gateway.dns.upstream`, default `[::1]:5354`). The daemon must (`gateway.dns.upstream`, default `[::1]:5354`). The daemon must
match: an IPv6 socket bound to `[::1]` does not accept v4-mapped match: an IPv6 socket bound to `[::1]` does not accept v4-mapped
+17 -9
View File
@@ -142,7 +142,7 @@ pick a different `fdXX::/N`). The `/112` size yields 65 536 virtual
IPs, which is the gateway's hard cap regardless of CIDR width. IPs, which is the gateway's hard cap regardless of CIDR width.
This minimum config is enough to start the gateway. The `dns.*` block This minimum config is enough to start the gateway. The `dns.*` block
is optional and defaults to `listen: "[::]:53"` and is optional and defaults to `listen: "[::1]:5353"` and
`upstream: "[::1]:5354"`. The full block — including `dns.*`, `upstream: "[::1]:5354"`. The full block — including `dns.*`,
`pool_grace_period`, `conntrack.*`, and `port_forwards[]` — is `pool_grace_period`, `conntrack.*`, and `port_forwards[]` — is
documented in documented in
@@ -195,20 +195,28 @@ Constraints:
```yaml ```yaml
gateway: gateway:
dns: dns:
listen: "[::]:53" listen: "[::1]:5353"
upstream: "[::1]:5354" upstream: "[::1]:5354"
ttl: 60 ttl: 60
``` ```
Common cases: Common cases:
- **No other resolver on the host:** `listen: "[::]:53"` is the - **Another resolver on the host (the canonical case):** the default
default and works. `listen: "[::1]:5353"` is loopback-only on an unprivileged port,
- **systemd-resolved is on port 53:** either disable its stub so it never conflicts with dnsmasq, systemd-resolved, or BIND
listener (`DNSStubListener=no` in holding 53. Configure the existing resolver to forward `.fips`
`/etc/systemd/resolved.conf`) or move the gateway to a different queries to `[::1]:5353` and you are done — this is what the
port (e.g., `[::]:5353`) and put a forwarder on 53 that delegates OpenWrt ipk does automatically.
`.fips` to the gateway. See - **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). [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"` - **Bind on the LAN address only:** `listen: "192.168.1.1:53"`
exposes the resolver only to LAN clients, not loopback. exposes the resolver only to LAN clients, not loopback.
+11 -16
View File
@@ -82,10 +82,13 @@ for the full flag list.
### Port conflict on the DNS listen port ### Port conflict on the DNS listen port
Symptom: gateway fails to start with "address already in use" on Symptom: gateway fails to start with "address already in use" on
port 53 (or whatever `gateway.dns.listen` is set to). the configured `gateway.dns.listen` address.
Another DNS server (systemd-resolved, dnsmasq, BIND) is bound to The default `[::1]:5353` is loopback-only on an unprivileged port and
the port. Identify it: should not collide with any standard resolver. If you have overridden
`dns.listen` to bind port 53 (or a LAN-side address) and another DNS
server (systemd-resolved, dnsmasq, BIND) is already bound there,
identify it:
```sh ```sh
sudo ss -tulnp | grep ':53' sudo ss -tulnp | grep ':53'
@@ -93,18 +96,10 @@ sudo ss -tulnp | grep ':53'
Two options: Two options:
- **Use an alternate listen address.** Pick a non-conflicting port - **Stay on the loopback default.** Drop the override and let the
and update the gateway config: gateway use `[::1]:5353`. Configure the existing resolver to
forward `.fips` queries to it (the canonical OpenWrt deployment
```yaml works this way out of the box).
gateway:
dns:
listen: "192.168.1.1:5353"
```
Then either point LAN clients at the alternate port directly, or
run a forwarding stub on port 53 that delegates `.fips` queries to
the gateway.
- **Relocate the conflicting resolver.** Move it to a different port - **Relocate the conflicting resolver.** Move it to a different port
(or disable it if not needed) and let the gateway bind 53. (or disable it if not needed) and let the gateway bind 53.
@@ -189,7 +184,7 @@ not running or not enabled. Check that the daemon config has
**Step 2.** Verify the gateway is listening on its DNS port: **Step 2.** Verify the gateway is listening on its DNS port:
```sh ```sh
sudo ss -tulnp | grep -E ':53\b' sudo ss -tulnp | grep -E ':(53|5353)\b'
``` ```
If nothing is listening on the configured `dns.listen` address, the If nothing is listening on the configured `dns.listen` address, the
+2 -2
View File
@@ -699,7 +699,7 @@ Non-`.fips` queries are answered with `REFUSED`.
| Parameter | Type | Default | Description | | Parameter | Type | Default | Description |
|-----------|------|---------|-------------| |-----------|------|---------|-------------|
| `gateway.dns.listen` | string | `"[::]:53"` | LAN-facing DNS listen address. Bind on the LAN-side IP (e.g., `"192.168.1.1:53"`) or on all interfaces (`"[::]:53"`) for LAN clients to query. Bind to a non-53 port if another resolver already owns 53 on the host (see [../how-to/troubleshoot-gateway.md](../how-to/troubleshoot-gateway.md)). | | `gateway.dns.listen` | string | `"[::1]:5353"` | DNS listen address. The default binds IPv6 loopback on an unprivileged port, matching the canonical deployment where another resolver on the host (dnsmasq, systemd-resolved, BIND) holds port 53 and forwards `.fips` queries to the gateway over loopback. Bind on the LAN-side IP (e.g., `"192.168.1.1:53"`) or wildcard (`"[::]:53"`) only on hosts with no other resolver on 53 and where LAN clients query the gateway directly. See [../how-to/troubleshoot-gateway.md](../how-to/troubleshoot-gateway.md). |
| `gateway.dns.upstream` | string | `"[::1]:5354"` | Upstream FIPS daemon resolver. **Must match the daemon's `dns.bind_addr` and `dns.port`.** Defaults match the daemon defaults (`::1:5354`). A v4 upstream (`"127.0.0.1:5354"`) cannot reach a daemon bound on `[::1]:5354` — Linux IPv6 sockets bound to explicit `::1` do not accept v4-mapped traffic. If you change the daemon's `dns.bind_addr`, update this field accordingly. | | `gateway.dns.upstream` | string | `"[::1]:5354"` | Upstream FIPS daemon resolver. **Must match the daemon's `dns.bind_addr` and `dns.port`.** Defaults match the daemon defaults (`::1:5354`). A v4 upstream (`"127.0.0.1:5354"`) cannot reach a daemon bound on `[::1]:5354` — Linux IPv6 sockets bound to explicit `::1` do not accept v4-mapped traffic. If you change the daemon's `dns.bind_addr`, update this field accordingly. |
| `gateway.dns.ttl` | u32 | `60` | TTL in seconds on AAAA responses returned to LAN clients. Smaller values let the gateway recycle pool addresses faster; larger values reduce LAN-side query traffic. | | `gateway.dns.ttl` | u32 | `60` | TTL in seconds on AAAA responses returned to LAN clients. Smaller values let the gateway recycle pool addresses faster; larger values reduce LAN-side query traffic. |
@@ -746,7 +746,7 @@ gateway:
pool: "fd01::/112" pool: "fd01::/112"
lan_interface: "enp3s0" lan_interface: "enp3s0"
dns: dns:
listen: "[::]:53" listen: "[::1]:5353"
upstream: "[::1]:5354" upstream: "[::1]:5354"
ttl: 60 ttl: 60
pool_grace_period: 60 pool_grace_period: 60
+5 -6
View File
@@ -132,7 +132,6 @@ gateway:
pool: "fd01::/112" # virtual IP range (up to 65535 addresses) pool: "fd01::/112" # virtual IP range (up to 65535 addresses)
lan_interface: "br-lan" # LAN-facing interface for proxy NDP lan_interface: "br-lan" # LAN-facing interface for proxy NDP
dns: dns:
listen: "[::1]:5353" # gateway DNS listener (dnsmasq forwards here)
upstream: "[::1]:5354" # FIPS daemon DNS resolver (matches daemon default) upstream: "[::1]:5354" # FIPS daemon DNS resolver (matches daemon default)
ttl: 60 # DNS TTL and mapping lifetime (seconds) ttl: 60 # DNS TTL and mapping lifetime (seconds)
pool_grace_period: 60 # seconds after last session before reclaiming pool_grace_period: 60 # seconds after last session before reclaiming
@@ -147,10 +146,11 @@ Three things to notice:
- `lan_interface: "br-lan"` — the OpenWrt LAN bridge. The gateway - `lan_interface: "br-lan"` — the OpenWrt LAN bridge. The gateway
installs proxy-NDP entries on this interface so LAN clients can installs proxy-NDP entries on this interface so LAN clients can
ARP-equivalent for pool addresses. ARP-equivalent for pool addresses.
- `dns.listen: "[::1]:5353"` — the gateway's DNS listener is bound - No `dns.listen` line — the source default `[::1]:5353` is exactly
to IPv6 loopback only. dnsmasq, which owns LAN port 53, forwards what OpenWrt wants. The gateway listens on IPv6 loopback only;
`.fips` queries to it. The init script wires up that forwarding; dnsmasq, which owns LAN port 53, forwards `.fips` queries to it.
you don't bind to a LAN address yourself. The init script wires up that forwarding; you don't bind to a LAN
address yourself.
For the full reference, see For the full reference, see
[../reference/configuration.md § Gateway](../reference/configuration.md#gateway-gateway). [../reference/configuration.md § Gateway](../reference/configuration.md#gateway-gateway).
@@ -321,7 +321,6 @@ gateway:
pool: "fd01::/112" pool: "fd01::/112"
lan_interface: "br-lan" lan_interface: "br-lan"
dns: dns:
listen: "[::1]:5353"
upstream: "[::1]:5354" upstream: "[::1]:5354"
ttl: 60 ttl: 60
pool_grace_period: 60 pool_grace_period: 60
+1 -1
View File
@@ -94,7 +94,7 @@ transports:
# pool: "fd01::/112" # pool: "fd01::/112"
# lan_interface: "eth0" # lan_interface: "eth0"
# dns: # dns:
# listen: "[::]:53" # listen: "[::1]:5353"
# # upstream must match the daemon's dns.bind_addr above. The # # upstream must match the daemon's dns.bind_addr above. The
# # default "[::1]:5354" matches the daemon's default. If you set # # default "[::1]:5354" matches the daemon's default. If you set
# # the daemon to bind on a wildcard ("::") or specific address, # # the daemon to bind on a wildcard ("::") or specific address,
@@ -76,16 +76,15 @@ peers: []
# Allows unmodified LAN hosts to reach FIPS mesh destinations via # Allows unmodified LAN hosts to reach FIPS mesh destinations via
# DNS-allocated virtual IPs and kernel nftables NAT. # DNS-allocated virtual IPs and kernel nftables NAT.
# #
# The gateway DNS listens on port 5353 by default so it does not conflict # The gateway DNS listens on `[::1]:5353` by default (set in source) so it
# with dnsmasq on port 53. The init script configures dnsmasq to forward # does not conflict with dnsmasq on port 53. The init script configures
# .fips queries to the gateway automatically. # dnsmasq to forward .fips queries to the gateway automatically.
gateway: gateway:
enabled: true enabled: true
pool: "fd01::/112" # virtual IP range (up to 65535 addresses) pool: "fd01::/112" # virtual IP range (up to 65535 addresses)
lan_interface: "br-lan" # LAN-facing interface for proxy NDP lan_interface: "br-lan" # LAN-facing interface for proxy NDP
dns: dns:
listen: "[::1]:5353" # gateway DNS listener (dnsmasq forwards here)
upstream: "[::1]:5354" # FIPS daemon DNS resolver (matches daemon default) upstream: "[::1]:5354" # FIPS daemon DNS resolver (matches daemon default)
ttl: 60 # DNS TTL and mapping lifetime (seconds) ttl: 60 # DNS TTL and mapping lifetime (seconds)
pool_grace_period: 60 # seconds after last session before reclaiming pool_grace_period: 60 # seconds after last session before reclaiming
+17 -4
View File
@@ -8,7 +8,20 @@ use std::net::SocketAddrV6;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Default gateway DNS listen address. /// Default gateway DNS listen address.
const DEFAULT_DNS_LISTEN: &str = "[::]:53"; ///
/// Loopback-only on the unprivileged port 5353. The canonical
/// gateway deployment is a host already serving DHCP/DNS to a LAN
/// segment (e.g., an OpenWrt AP), where port 53 is taken by the
/// existing resolver and `.fips` queries are forwarded to the
/// gateway over loopback. Operators on a host without a pre-existing
/// resolver on 53 can opt back into the wildcard bind by setting
/// `dns.listen: "[::]:53"` explicitly.
///
/// `[::1]` is IPv6 loopback only; Linux IPv6 sockets bound to
/// explicit `::1` do not accept v4-mapped traffic. Forwarders that
/// reach the gateway over IPv4 loopback (`127.0.0.1`) need to be
/// pointed at an explicit IPv4 listen address instead.
const DEFAULT_DNS_LISTEN: &str = "[::1]:5353";
/// Default upstream DNS resolver (FIPS daemon). /// Default upstream DNS resolver (FIPS daemon).
/// ///
@@ -118,7 +131,7 @@ pub struct PortForward {
/// Gateway DNS resolver configuration (`gateway.dns.*`). /// Gateway DNS resolver configuration (`gateway.dns.*`).
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GatewayDnsConfig { pub struct GatewayDnsConfig {
/// Listen address and port (default: `0.0.0.0:53`). /// Listen address and port (default: `[::1]:5353`).
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub listen: Option<String>, pub listen: Option<String>,
@@ -133,7 +146,7 @@ pub struct GatewayDnsConfig {
} }
impl GatewayDnsConfig { impl GatewayDnsConfig {
/// Get the listen address (default: `0.0.0.0:53`). /// Get the listen address (default: `[::1]:5353`).
pub fn listen(&self) -> &str { pub fn listen(&self) -> &str {
self.listen.as_deref().unwrap_or(DEFAULT_DNS_LISTEN) self.listen.as_deref().unwrap_or(DEFAULT_DNS_LISTEN)
} }
@@ -205,7 +218,7 @@ lan_interface: "eth0"
assert!(!config.enabled); assert!(!config.enabled);
assert_eq!(config.pool, "fd01::/112"); assert_eq!(config.pool, "fd01::/112");
assert_eq!(config.lan_interface, "eth0"); assert_eq!(config.lan_interface, "eth0");
assert_eq!(config.dns.listen(), "[::]:53"); assert_eq!(config.dns.listen(), "[::1]:5353");
assert_eq!(config.dns.upstream(), "[::1]:5354"); assert_eq!(config.dns.upstream(), "[::1]:5354");
assert_eq!(config.dns.ttl(), 60); assert_eq!(config.dns.ttl(), 60);
assert_eq!(config.grace_period(), 60); assert_eq!(config.grace_period(), 60);