mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 11:08:25 +00:00
Move the gateway's default DNS port from 5353 to 5365
5353 is the mDNS port, which the daemon's LAN rendezvous, Avahi and systemd-resolved can hold. On an OpenWrt access point with the gateway enabled, the gateway lost the port to the daemon's mDNS responder and dnsmasq sent .fips queries to a responder that does not answer them. 5365 is unassigned by IANA and not used by any common resolver. The OpenWrt init script now points dnsmasq at the port gateway.dns.listen actually sets, falling back to the new default, and when it swaps the .fips forwarding it clears every loopback .fips entry rather than four fixed ones, so a stale entry for an old or custom port does not linger. Forwards to other hosts and other domains are kept. The shipped OpenWrt config and the example config leave the listen line at the default. fips.yaml is a conffile on OpenWrt, and fips-ap-setup edits it, so routers that set up an access point would keep the old explicit listen: "[::1]:5353" across an upgrade and stay broken after the default moved. The first-boot setup script, which every install and upgrade path runs before the services start, now rewrites that exact shipped line to the line a fresh install ships and logs that it did; any other value is left as configured. The script is sourced rather than executed on the SDK-feed and sysupgrade paths, so the migration runs last, cannot end the script early and cannot change its exit status. The script also removes stale loopback .fips forwarding entries for port 5353. The gateway warns at startup when it is configured on 5353, whether or not the bind succeeds, since an mDNS responder can take the port later. The OpenWrt scenario harness checks the port the init script reads, that its default matches the gateway's, and the swap's cleanup against a uci stub. It also runs the real setup script executed by both package managers' upgrade scripts and sourced in a subshell, and checks the negative, missing-file, repeat-run and stale-entry cases. The dns-resolver and deb-install harnesses check that the gateway binds the new default.
This commit is contained in:
@@ -130,7 +130,7 @@ There is no `fipsctl gateway` subcommand; clients (including
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌───────────┐ │
|
||||
│ │ DNS proxy │ │ Virtual │ │
|
||||
│ │ ([::1]:5353) │─▶│ IP pool │ │
|
||||
│ │ ([::1]:5365) │─▶│ IP pool │ │
|
||||
│ │ .fips only │ │ (state │ │
|
||||
│ └──────┬───────┘ │ machine) │ │
|
||||
│ │ └─────┬─────┘ │
|
||||
@@ -182,8 +182,10 @@ involving the DNS proxy or the pool.
|
||||
### DNS Resolution Flow
|
||||
|
||||
1. A LAN client sends a DNS query to the gateway's listener (default
|
||||
`[::1]:5353`, configurable via `gateway.dns.listen`). The default
|
||||
is loopback-only on an unprivileged port: the canonical deployment
|
||||
`[::1]:5365`, configurable via `gateway.dns.listen`). The default
|
||||
is not 5353, the mDNS port, which the daemon's LAN rendezvous and
|
||||
other mDNS responders hold. It 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
|
||||
|
||||
@@ -143,7 +143,7 @@ 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
|
||||
is optional and defaults to `listen: "[::1]:5365"` and
|
||||
`upstream: "[::1]:5354"`. The full block — including `dns.*`,
|
||||
`pool_grace_period`, `conntrack.*`, and `port_forwards[]` — is
|
||||
documented in
|
||||
@@ -196,7 +196,7 @@ Constraints:
|
||||
```yaml
|
||||
gateway:
|
||||
dns:
|
||||
listen: "[::1]:5353"
|
||||
listen: "[::1]:5365"
|
||||
upstream: "[::1]:5354"
|
||||
ttl: 60
|
||||
```
|
||||
@@ -204,15 +204,16 @@ gateway:
|
||||
Common cases:
|
||||
|
||||
- **Another resolver on the host (the canonical case):** the default
|
||||
`listen: "[::1]:5353"` is loopback-only on an unprivileged port,
|
||||
`listen: "[::1]:5365"` 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.
|
||||
queries to `[::1]:5365` and you are done — this is what the
|
||||
OpenWrt ipk does automatically. On OpenWrt the init script reads
|
||||
`gateway.dns.listen` and points dnsmasq at whatever port it sets.
|
||||
- **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
|
||||
this — leave the listen address at `[::1]:5365` 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
|
||||
|
||||
@@ -111,6 +111,7 @@ The error names the service most likely to hold the port:
|
||||
- **5354**: the fips daemon's own DNS responder. `gateway.dns.listen`
|
||||
must not be the daemon's DNS port.
|
||||
- **5355**: LLMNR, held by systemd-resolved unless `LLMNR=no`.
|
||||
- **5365**, the default: another fips-gateway already running.
|
||||
- **Any other port**: another process.
|
||||
|
||||
Find the actual holder, replacing the port with your own:
|
||||
@@ -121,13 +122,17 @@ sudo ss -ulpn 'sport = :53'
|
||||
netstat -ulnp
|
||||
```
|
||||
|
||||
The default listen address, `[::1]:5353`, is loopback-only on an
|
||||
unprivileged port. Two options:
|
||||
The default listen address, `[::1]:5365`, is loopback-only on an
|
||||
unprivileged port. Releases before 0.5.2 defaulted to `[::1]:5353`,
|
||||
the mDNS port; a config that still sets it explicitly keeps it, and
|
||||
the gateway warns at startup. On OpenWrt, an upgrade rewrites the
|
||||
previously shipped `listen: "[::1]:5353"` line to the new default.
|
||||
Two options:
|
||||
|
||||
- **Move the gateway.** Set `gateway.dns.listen` to a free port and
|
||||
point the resolver that forwards `.fips` at the same port. With the
|
||||
loopback default, configure the existing resolver to forward `.fips`
|
||||
queries to `[::1]:5353` (the canonical OpenWrt deployment works this
|
||||
queries to `[::1]:5365` (the canonical OpenWrt deployment works this
|
||||
way out of the box).
|
||||
|
||||
- **Relocate the conflicting resolver.** Move it to a different port
|
||||
@@ -240,7 +245,7 @@ not running or not enabled. Check that the daemon config has
|
||||
**Step 2.** Verify the gateway is listening on its DNS port:
|
||||
|
||||
```sh
|
||||
sudo ss -tulnp | grep -E ':(53|5353)\b'
|
||||
sudo ss -tulnp | grep -E ':(53|5365)\b'
|
||||
```
|
||||
|
||||
If nothing is listening on the configured `dns.listen` address, the
|
||||
|
||||
@@ -889,7 +889,7 @@ Non-`.fips` queries are answered with `REFUSED`.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `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.listen` | string | `"[::1]:5365"` | DNS listen address. The default binds IPv6 loopback on an unprivileged port, not 5353, the mDNS 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.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. |
|
||||
|
||||
@@ -936,7 +936,7 @@ gateway:
|
||||
pool: "fd01::/112"
|
||||
lan_interface: "enp3s0"
|
||||
dns:
|
||||
listen: "[::1]:5353"
|
||||
listen: "[::1]:5365"
|
||||
upstream: "[::1]:5354"
|
||||
ttl: 60
|
||||
pool_grace_period: 60
|
||||
|
||||
@@ -132,7 +132,7 @@ gateway:
|
||||
pool: "fd01::/112" # virtual IP range (up to 65535 addresses)
|
||||
lan_interface: "br-lan" # LAN-facing interface for proxy NDP
|
||||
dns:
|
||||
listen: "[::1]:5353" # gateway DNS bind (IPv6 loopback only)
|
||||
# listen: "[::1]:5365" # the default; the init script points dnsmasq at this port
|
||||
upstream: "[::1]:5354" # FIPS daemon DNS resolver (matches daemon default)
|
||||
ttl: 60 # DNS TTL and mapping lifetime (seconds)
|
||||
pool_grace_period: 60 # seconds after last session before reclaiming
|
||||
@@ -147,9 +147,10 @@ Three things to notice:
|
||||
- `lan_interface: "br-lan"` — the OpenWrt LAN bridge. The gateway
|
||||
installs proxy-NDP entries on this interface so LAN clients can
|
||||
ARP-equivalent for pool addresses.
|
||||
- `dns.listen: "[::1]:5353"` — the gateway's DNS bind, pinned to
|
||||
IPv6 loopback only. dnsmasq, which owns LAN port 53, forwards
|
||||
`.fips` queries to it. The init script wires up that forwarding;
|
||||
- `dns.listen`, commented out — the gateway's DNS bind, left at its
|
||||
default `[::1]:5365`, IPv6 loopback only. dnsmasq, which owns LAN
|
||||
port 53, forwards `.fips` queries to it. The init script reads
|
||||
`gateway.dns.listen` and points dnsmasq at whatever port it sets;
|
||||
you don't bind to a LAN address yourself.
|
||||
|
||||
For the full reference, see
|
||||
@@ -172,7 +173,7 @@ Behind that single command, the init script
|
||||
`/etc/sysctl.d/fips-gateway.conf`.
|
||||
2. **Reconfigures dnsmasq via UCI** so `.fips` queries arriving at
|
||||
the LAN's port 53 are forwarded to the gateway's loopback
|
||||
listener on port 5353 instead of going straight to the daemon's
|
||||
listener on port 5365 instead of going straight to the daemon's
|
||||
resolver on port 5354. (Dnsmasq still owns 53; the gateway sits
|
||||
in front of the daemon for `.fips` only.)
|
||||
3. **Adds a global-scope IPv6 prefix** to `br-lan`. Without a
|
||||
@@ -242,7 +243,7 @@ Expectations:
|
||||
|
||||
> **What just happened end to end.** Your client asked dnsmasq for
|
||||
> `test-us01.fips`. Dnsmasq forwarded the query to the gateway's
|
||||
> loopback listener on port 5353. The gateway forwarded the query on
|
||||
> loopback listener on port 5365. The gateway forwarded the query on
|
||||
> to the daemon's resolver on port 5354. The daemon answered with
|
||||
> `test-us01`'s mesh address (`fd97:...`). The gateway allocated a
|
||||
> virtual IP from `fd01::/112`, installed nftables DNAT/SNAT/
|
||||
|
||||
@@ -133,7 +133,7 @@ transports:
|
||||
# pool: "fd01::/112"
|
||||
# lan_interface: "eth0"
|
||||
# dns:
|
||||
# listen: "[::1]:5353"
|
||||
# listen: "[::1]:5365"
|
||||
# # upstream must match the daemon's dns.bind_addr above. The
|
||||
# # default "[::1]:5354" matches the daemon's default. If you set
|
||||
# # the daemon to bind on a wildcard ("::") or specific address,
|
||||
|
||||
@@ -164,15 +164,15 @@ transports:
|
||||
|
||||
# No BLE transport: OpenWrt builds target musl, which has no BlueZ backend.
|
||||
|
||||
# Outbound LAN gateway. dnsmasq forwards .fips queries to listen=[::1]:5353
|
||||
# while it runs (configured by the fips-gateway init script). Requires IPv6
|
||||
# forwarding enabled.
|
||||
# Outbound LAN gateway. While it runs, the fips-gateway init script points
|
||||
# dnsmasq's .fips forwarding at the port dns.listen sets (default [::1]:5365).
|
||||
# Requires IPv6 forwarding enabled.
|
||||
gateway:
|
||||
enabled: true
|
||||
pool: "fd01::/112"
|
||||
lan_interface: "br-lan"
|
||||
dns:
|
||||
listen: "[::1]:5353"
|
||||
# listen: "[::1]:5365" # the default; the init script points dnsmasq at this port
|
||||
upstream: "[::1]:5354"
|
||||
ttl: 60
|
||||
pool_grace_period: 60
|
||||
|
||||
@@ -15,8 +15,10 @@ STOP=09
|
||||
PROG=/usr/bin/fips-gateway
|
||||
CONFIG=/etc/fips/fips.yaml
|
||||
|
||||
# Port the gateway DNS listens on (must match dns.listen in fips.yaml).
|
||||
GW_DNS_PORT=5353
|
||||
# Port the gateway DNS listens on when gateway.dns.listen is not set. Must
|
||||
# match DEFAULT_DNS_LISTEN in the gateway's source; the scenario harness checks
|
||||
# the two agree. The port actually used comes from gateway_dns_port.
|
||||
GW_DNS_DEFAULT=5365
|
||||
# Port the FIPS daemon DNS listens on.
|
||||
DAEMON_DNS_PORT=5354
|
||||
|
||||
@@ -40,11 +42,11 @@ start_service() {
|
||||
# Load conntrack module for /proc/net/nf_conntrack.
|
||||
modprobe nf_conntrack 2>/dev/null || true
|
||||
|
||||
# Redirect dnsmasq .fips forwarding from daemon (5354) to gateway (5353)
|
||||
# so LAN clients get virtual IPs instead of raw mesh addresses.
|
||||
# Done early and synchronously so dnsmasq is ready before the gateway
|
||||
# starts accepting DNS queries.
|
||||
dnsmasq_swap_fips_upstream "$GW_DNS_PORT"
|
||||
# Redirect dnsmasq .fips forwarding from the daemon (5354) to the port the
|
||||
# gateway listens on, so LAN clients get virtual IPs instead of raw mesh
|
||||
# addresses. Done early and synchronously so dnsmasq is ready before the
|
||||
# gateway starts accepting DNS queries.
|
||||
dnsmasq_swap_fips_upstream "$(gateway_dns_port)"
|
||||
sleep 1
|
||||
|
||||
# Add a global-scope IPv6 prefix to br-lan so Android/Chrome clients
|
||||
@@ -87,6 +89,35 @@ gateway_config_enabled() {
|
||||
awk '/^gateway:/{found=1; next} found && /^[^ ]/{found=0} found && /enabled:/{gsub(/.*enabled:[[:space:]]*/, ""); gsub(/["'"'"']/, ""); print; exit}' "$CONFIG"
|
||||
}
|
||||
|
||||
# Print the port the gateway's DNS listener will bind: the digits after the
|
||||
# last ":" of the "listen:" value inside the top-level "gateway:" block of
|
||||
# fips.yaml, or $GW_DNS_DEFAULT when there is no such line or its value does
|
||||
# not end in a port. Commented lines are skipped, and "listen_port:" (a port
|
||||
# forward key) does not match.
|
||||
#
|
||||
# Block-style YAML only: a flow-style "dns: {listen: ...}" reads as the
|
||||
# default. The dnsmasq entry this port feeds is always ::1#<port>, so a
|
||||
# gateway listening only on 127.0.0.1 is still not reachable through it.
|
||||
gateway_dns_port() {
|
||||
local port
|
||||
port="$(awk '
|
||||
/^[A-Za-z_]/ { top = $1 }
|
||||
top != "gateway:" { next }
|
||||
/^[[:space:]]*#/ { next }
|
||||
/^[[:space:]]+listen:/ {
|
||||
v = $0
|
||||
sub(/^[[:space:]]+listen:[[:space:]]*/, "", v)
|
||||
sub(/[[:space:]]+#.*$/, "", v)
|
||||
gsub(/["'"'"']/, "", v)
|
||||
sub(/[[:space:]]+$/, "", v)
|
||||
n = split(v, part, ":")
|
||||
if (n > 1 && part[n] ~ /^[0-9]+$/) print part[n]
|
||||
exit
|
||||
}
|
||||
' "$CONFIG" 2>/dev/null)"
|
||||
echo "${port:-$GW_DNS_DEFAULT}"
|
||||
}
|
||||
|
||||
# Extract the gateway pool CIDR from fips.yaml.
|
||||
# Looks for "pool:" indented under the top-level "gateway:" block.
|
||||
gateway_pool_cidr() {
|
||||
@@ -178,13 +209,20 @@ gateway_remove_global_prefix() {
|
||||
# $1 = target port number
|
||||
dnsmasq_swap_fips_upstream() {
|
||||
local port="$1"
|
||||
local server
|
||||
|
||||
# Remove both possible entries, then add the correct one.
|
||||
uci -q del_list dhcp.@dnsmasq[0].server="/fips/127.0.0.1#${DAEMON_DNS_PORT}" 2>/dev/null
|
||||
uci -q del_list dhcp.@dnsmasq[0].server="/fips/127.0.0.1#${GW_DNS_PORT}" 2>/dev/null
|
||||
# Also handle IPv6 loopback variants.
|
||||
uci -q del_list dhcp.@dnsmasq[0].server="/fips/::1#${DAEMON_DNS_PORT}" 2>/dev/null
|
||||
uci -q del_list dhcp.@dnsmasq[0].server="/fips/::1#${GW_DNS_PORT}" 2>/dev/null
|
||||
# Remove every loopback .fips forward, then add the one for $port. That
|
||||
# covers the daemon's port, this gateway's, and a stale entry for any other
|
||||
# local port, such as the old default 5353 or a changed gateway.dns.listen.
|
||||
# A .fips forward to another host and servers for other domains are kept.
|
||||
# uci prints a list on one line separated by spaces.
|
||||
for server in $(uci -q get 'dhcp.@dnsmasq[0].server' 2>/dev/null); do
|
||||
case "$server" in
|
||||
"/fips/::1#"* | "/fips/127.0.0.1#"*)
|
||||
uci -q del_list dhcp.@dnsmasq[0].server="$server" 2>/dev/null
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
uci add_list dhcp.@dnsmasq[0].server="/fips/::1#${port}"
|
||||
uci commit dhcp
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
#!/bin/sh
|
||||
# FIPS first-boot setup — runs once after package installation.
|
||||
# Configures the firewall and kernel modules for FIPS operation.
|
||||
# This script is executed by /etc/rc.d/S19sysctl on first boot and
|
||||
# then deleted by the UCI defaults mechanism.
|
||||
# The UCI defaults mechanism runs it once and deletes it when it ends with
|
||||
# status 0.
|
||||
#
|
||||
# It is executed by the package's postinst, but sourced, not executed, by
|
||||
# OpenWrt's default_postinst for a package built from the SDK feed and by the
|
||||
# first-boot uci-defaults run after a sysupgrade. Nothing here may exit early,
|
||||
# change directory or set shell options, and it must end with status 0: a
|
||||
# non-zero status leaves the script in place to run again on every boot.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Kernel modules
|
||||
@@ -63,9 +69,13 @@ uci commit firewall
|
||||
# dnsmasq init script builds its config from UCI and loads no directory under
|
||||
# /etc. The daemon's DNS responder binds ::1. The 127.0.0.1 del_list removes
|
||||
# the entry older packages added. While fips-gateway runs, its init script
|
||||
# points this entry at the gateway's DNS port instead.
|
||||
# points this entry at the gateway's DNS port instead. The two del_lists after
|
||||
# the first remove the gateway's entry for its old default port, left behind
|
||||
# by a gateway that stopped without its init script's stop running.
|
||||
|
||||
uci -q del_list dhcp.@dnsmasq[0].server="/fips/127.0.0.1#5354" 2>/dev/null || true
|
||||
uci -q del_list dhcp.@dnsmasq[0].server="/fips/::1#5353" 2>/dev/null || true
|
||||
uci -q del_list dhcp.@dnsmasq[0].server="/fips/127.0.0.1#5353" 2>/dev/null || true
|
||||
uci -q del_list dhcp.@dnsmasq[0].server="/fips/::1#5354" 2>/dev/null || true
|
||||
uci add_list dhcp.@dnsmasq[0].server="/fips/::1#5354"
|
||||
uci -q del_list dhcp.@dnsmasq[0].rebind_domain="fips" 2>/dev/null || true
|
||||
@@ -86,4 +96,40 @@ grep -qxF 'nf_conntrack' /etc/modules.d/nf-conntrack 2>/dev/null || \
|
||||
# proxy NDP entries are actually added.
|
||||
sysctl -p /etc/sysctl.d/fips-gateway.conf 2>/dev/null || true
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. Gateway DNS listen port
|
||||
# ---------------------------------------------------------------------------
|
||||
# Every release up to 0.5.1 shipped the gateway's DNS listener on 5353, the
|
||||
# mDNS port, which the daemon's LAN rendezvous can hold. fips.yaml is a
|
||||
# conffile and fips-ap-setup edits it, so an upgrade keeps the old line.
|
||||
# Rewrite exactly that shipped line, four-space indent and nothing after the
|
||||
# closing quote, inside the top-level gateway block, to the line a fresh
|
||||
# install ships. Any other value is left as configured; the gateway warns at
|
||||
# startup when it is still on the mDNS port.
|
||||
#
|
||||
# Runs last, and cannot fail the script: see the note at the top.
|
||||
fips_migrate_gateway_dns_listen() {
|
||||
local cfg=/etc/fips/fips.yaml
|
||||
local old=' listen: "[::1]:5353"'
|
||||
local new=' # listen: "[::1]:5365" # the default; the init script points dnsmasq at this port'
|
||||
local msg='fips: moved gateway.dns.listen off the mDNS port 5353 to the default [::1]:5365'
|
||||
|
||||
if [ -f "$cfg" ] && grep -qxF "$old" "$cfg" 2>/dev/null; then
|
||||
if awk -v old="$old" -v new="$new" '
|
||||
/^[A-Za-z_]/ { top = $1 }
|
||||
top == "gateway:" && $0 == old { print new; changed = 1; next }
|
||||
{ print }
|
||||
END { exit changed ? 0 : 1 }
|
||||
' "$cfg" > "$cfg.tmp" 2>/dev/null &&
|
||||
chmod 600 "$cfg.tmp" 2>/dev/null &&
|
||||
mv -f "$cfg.tmp" "$cfg" 2>/dev/null; then
|
||||
logger -t fips "$msg" 2>/dev/null || true
|
||||
echo "$msg"
|
||||
else
|
||||
rm -f "$cfg.tmp" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
}
|
||||
fips_migrate_gateway_dns_listen
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -372,6 +372,11 @@ async fn main() {
|
||||
// Before the pool, NAT table and routes exist, so a port that is already
|
||||
// taken ends the gateway with nothing to tear down, and a service manager
|
||||
// restarting it does not churn nftables.
|
||||
if gw_config.dns.is_mdns() {
|
||||
warn!(
|
||||
"gateway.dns.listen uses port 5353, the mDNS port; an mDNS responder (the fips daemon's LAN rendezvous, avahi) will conflict with it; the default is now [::1]:5365"
|
||||
);
|
||||
}
|
||||
let dns_socket = match dns::bind_listener(gw_config.dns.listen()).await {
|
||||
Ok(socket) => socket,
|
||||
Err(e) => {
|
||||
|
||||
+51
-5
@@ -9,7 +9,10 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Default gateway DNS listen address.
|
||||
///
|
||||
/// Loopback-only on the unprivileged port 5353. The canonical
|
||||
/// Loopback-only on the unprivileged port 5365, which IANA leaves
|
||||
/// unassigned and no common resolver uses. It is not 5353, the mDNS
|
||||
/// port, which the daemon's LAN rendezvous, avahi-daemon and
|
||||
/// systemd-resolved can hold. 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
|
||||
@@ -21,7 +24,7 @@ use serde::{Deserialize, Serialize};
|
||||
/// 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";
|
||||
const DEFAULT_DNS_LISTEN: &str = "[::1]:5365";
|
||||
|
||||
/// Default upstream DNS resolver (FIPS daemon).
|
||||
///
|
||||
@@ -131,7 +134,7 @@ pub struct PortForward {
|
||||
/// Gateway DNS resolver configuration (`gateway.dns.*`).
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct GatewayDnsConfig {
|
||||
/// Listen address and port (default: `[::1]:5353`).
|
||||
/// Listen address and port (default: `[::1]:5365`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub listen: Option<String>,
|
||||
|
||||
@@ -146,7 +149,7 @@ pub struct GatewayDnsConfig {
|
||||
}
|
||||
|
||||
impl GatewayDnsConfig {
|
||||
/// Get the listen address (default: `[::1]:5353`).
|
||||
/// Get the listen address (default: `[::1]:5365`).
|
||||
pub fn listen(&self) -> &str {
|
||||
self.listen.as_deref().unwrap_or(DEFAULT_DNS_LISTEN)
|
||||
}
|
||||
@@ -166,6 +169,12 @@ impl GatewayDnsConfig {
|
||||
pub(crate) fn port_of(listen: &str) -> Option<u16> {
|
||||
listen.rsplit_once(':')?.1.parse().ok()
|
||||
}
|
||||
|
||||
/// Whether the listen address is on the mDNS port, which an mDNS
|
||||
/// responder can take from the gateway at any time.
|
||||
pub fn is_mdns(&self) -> bool {
|
||||
Self::port_of(self.listen()) == Some(5353)
|
||||
}
|
||||
}
|
||||
|
||||
/// Conntrack timeout overrides (`gateway.conntrack.*`).
|
||||
@@ -224,7 +233,7 @@ lan_interface: "eth0"
|
||||
assert!(!config.enabled);
|
||||
assert_eq!(config.pool, "fd01::/112");
|
||||
assert_eq!(config.lan_interface, "eth0");
|
||||
assert_eq!(config.dns.listen(), "[::1]:5353");
|
||||
assert_eq!(config.dns.listen(), "[::1]:5365");
|
||||
assert_eq!(config.dns.upstream(), "[::1]:5354");
|
||||
assert_eq!(config.dns.ttl(), 60);
|
||||
assert_eq!(config.grace_period(), 60);
|
||||
@@ -232,6 +241,43 @@ lan_interface: "eth0"
|
||||
assert_eq!(config.conntrack.udp_timeout(), 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_listen_on_5353_is_flagged_as_mdns() {
|
||||
for listen in [
|
||||
"[::1]:5353",
|
||||
"[::]:5353",
|
||||
"127.0.0.1:5353",
|
||||
"localhost:5353",
|
||||
] {
|
||||
let dns = GatewayDnsConfig {
|
||||
listen: Some(listen.to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(dns.is_mdns(), "{listen} must be flagged as the mDNS port");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_default_and_other_ports_are_not_flagged_as_mdns() {
|
||||
assert!(!GatewayDnsConfig::default().is_mdns());
|
||||
for listen in [
|
||||
"[::1]:5365",
|
||||
"[::]:53",
|
||||
"192.168.1.1:53",
|
||||
"[::]:5355",
|
||||
"localhost",
|
||||
] {
|
||||
let dns = GatewayDnsConfig {
|
||||
listen: Some(listen.to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
!dns.is_mdns(),
|
||||
"{listen} must not be flagged as the mDNS port"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gateway_config_custom() {
|
||||
let yaml = r#"
|
||||
|
||||
@@ -304,6 +304,42 @@ EOF
|
||||
return
|
||||
}
|
||||
|
||||
# The installed gateway, on the default config, serves .fips on its default
|
||||
# listen address. Each check needs the gateway running: a gateway that exited
|
||||
# fails the first one rather than letting the others pass on nothing.
|
||||
# Args: <name> <npub>, the daemon's npub to resolve through the gateway.
|
||||
check_gateway_default_listener() {
|
||||
local name="$1" npub="$2"
|
||||
local journal="" _i
|
||||
for _i in $(seq 1 5); do
|
||||
journal=$(docker exec "$name" journalctl -u fips-gateway.service --no-pager 2>/dev/null) || journal=""
|
||||
printf '%s\n' "$journal" | grep -q "fips-gateway running" && break
|
||||
sleep 1
|
||||
done
|
||||
if ! printf '%s\n' "$journal" | grep -q "fips-gateway running"; then
|
||||
fail "the installed fips-gateway did not reach 'fips-gateway running', so its default listener was not observed"
|
||||
echo " --- fips-gateway journal ---"
|
||||
printf '%s\n' "$journal" | tail -15
|
||||
return
|
||||
fi
|
||||
|
||||
local sockets
|
||||
sockets=$(docker exec "$name" ss -Hulnp 'sport = :5365' 2>/dev/null) || sockets=""
|
||||
if printf '%s\n' "$sockets" | grep -F '[::1]:5365' | grep -q 'fips-gateway'; then
|
||||
pass "fips-gateway listens on its default [::1]:5365"
|
||||
else
|
||||
fail "no fips-gateway socket on [::1]:5365: '$sockets'"
|
||||
fi
|
||||
|
||||
local answer
|
||||
answer=$(docker exec "$name" dig +short +tries=1 +time=3 @::1 -p 5365 AAAA "${npub}.fips" 2>&1)
|
||||
if printf '%s\n' "$answer" | grep -qE '^fd01::[0-9a-f]{1,4}$'; then
|
||||
pass "the gateway answers ${npub}.fips on [::1]:5365 from its fd01::/112 pool"
|
||||
else
|
||||
fail "the gateway did not answer ${npub}.fips on [::1]:5365 from its pool: '$answer'"
|
||||
fi
|
||||
}
|
||||
|
||||
# Purge the package with the DNS routing file planted and fips-dns stopped, and
|
||||
# check that postrm removes the file and restarts systemd-resolved.
|
||||
#
|
||||
@@ -709,6 +745,8 @@ DOCKERFILE
|
||||
docker exec "$name" journalctl -u fips-gateway.service --no-pager 2>&1 | tail -15
|
||||
fi
|
||||
|
||||
check_gateway_default_listener "$name" "$npub"
|
||||
|
||||
check_purge_clears_dns "$name" "$expected_backend"
|
||||
|
||||
cleanup_container "$name"
|
||||
|
||||
@@ -712,6 +712,47 @@ read_gateway_log() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# The gateway on its default config binds [::1]:5365 and gets past the
|
||||
# bind to the NAT step, logging one of the two NAT lines whichever way NAT
|
||||
# goes in this container. Those are the lines the held-port check requires
|
||||
# to be absent, so this shows the gateway still emits them in that text.
|
||||
check_gateway_default_bind() {
|
||||
local name="$1"
|
||||
local log=/var/log/fips-gateway.log
|
||||
local text="" read_ok=0 listening=0 nat_step=0 _i
|
||||
for _i in $(seq 1 5); do
|
||||
if text=$(read_gateway_log "$name" "$log"); then
|
||||
read_ok=1
|
||||
if printf '%s\n' "$text" | grep -q 'Gateway DNS resolver listening.*addr=\[::1\]:5365'; then
|
||||
listening=1
|
||||
fi
|
||||
if printf '%s\n' "$text" | grep -qE 'Created nftables table|Failed to create nftables table'; then
|
||||
nat_step=1
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [ "$read_ok" = "0" ]; then
|
||||
fail "could not read $log, so the gateway's default bind was not observed"
|
||||
return
|
||||
fi
|
||||
if [ "$listening" = "1" ]; then
|
||||
pass "fips-gateway listens on its default [::1]:5365"
|
||||
else
|
||||
fail "fips-gateway did not log listening on [::1]:5365"
|
||||
fi
|
||||
if [ "$nat_step" = "1" ]; then
|
||||
pass "fips-gateway gets past the DNS bind to the NAT step"
|
||||
else
|
||||
fail "fips-gateway logged neither NAT-step line after the DNS bind"
|
||||
fi
|
||||
if [ "$listening" = "0" ] || [ "$nat_step" = "0" ]; then
|
||||
echo " --- $log ---"
|
||||
printf '%s\n' "$text" | tail -20
|
||||
fi
|
||||
}
|
||||
|
||||
# The gateway exits at the DNS bind when its listen port is held. The
|
||||
# daemon in this container holds [::1]:5354, so a gateway configured to
|
||||
# listen there must exit non-zero with the hint naming the daemon, before
|
||||
@@ -980,9 +1021,10 @@ EOF'
|
||||
echo " --- fips-gateway log ---"
|
||||
docker exec "$name" tail -20 /var/log/fips-gateway.log 2>&1 || true
|
||||
fi
|
||||
# Stop the gateway (it will likely have failed past the upstream
|
||||
# check on something unrelated in this minimal container — we only
|
||||
# care that the upstream reachability step succeeded).
|
||||
check_gateway_default_bind "$name"
|
||||
|
||||
# Stop the gateway (it may have failed after the DNS bind on something
|
||||
# unrelated in this minimal container).
|
||||
docker exec "$name" pkill -f fips-gateway 2>/dev/null || true
|
||||
|
||||
check_gateway_exits_on_held_port "$name"
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
# FIPS Node Configuration
|
||||
|
||||
node:
|
||||
identity:
|
||||
# By default, a new ephemeral keypair is generated on each start.
|
||||
# Uncomment persistent to keep the same identity across restarts;
|
||||
# on first start a keypair is saved to fips.key/fips.pub next to
|
||||
# this config file (mode 0600/0644).
|
||||
# persistent: true
|
||||
#
|
||||
# Or set an explicit key (overrides persistent):
|
||||
# nsec: "nsec1..."
|
||||
# Mesh-lookup protocol (node.lookup.*): the overlay coordinate-lookup engine
|
||||
# (mesh address -> coordinates). Defaults shown; uncomment to override.
|
||||
# lookup:
|
||||
# ttl: 64
|
||||
# attempt_timeouts_secs: [1, 2, 4, 8]
|
||||
# recent_expiry_secs: 10
|
||||
# backoff_base_secs: 0
|
||||
# backoff_max_secs: 0
|
||||
# forward_min_interval_secs: 2
|
||||
rendezvous:
|
||||
# Optional Nostr-mediated overlay endpoint rendezvous.
|
||||
# nostr:
|
||||
# enabled: true
|
||||
# policy: configured_only # disabled | configured_only | open
|
||||
# open_discovery_max_pending: 64 # caps queued open-rendezvous retries
|
||||
# app: "fips-overlay-v1"
|
||||
# advertise: true
|
||||
# advert_relays:
|
||||
# - "wss://relay.damus.io"
|
||||
# - "wss://nos.lol"
|
||||
# - "wss://offchain.pub"
|
||||
# dm_relays:
|
||||
# - "wss://relay.damus.io"
|
||||
# - "wss://nos.lol"
|
||||
# - "wss://offchain.pub"
|
||||
# # Optional override. If omitted, FIPS uses the built-in STUN list.
|
||||
# # Built-in relay/STUN defaults are best-effort and should be
|
||||
# # overridden by operators for production use.
|
||||
# stun_servers:
|
||||
# - "stun:stun.l.google.com:19302"
|
||||
# - "stun:stun.cloudflare.com:3478"
|
||||
# - "stun:global.stun.twilio.com:3478"
|
||||
|
||||
# mDNS/DNS-SD peer rendezvous on the local link. Ships commented (the
|
||||
# daemon default is off); 'fips-ap-setup' uncomments it when creating
|
||||
# the access SSID — phone FIPS apps cannot see raw-Ethernet beacons,
|
||||
# so mDNS is how they find this router's daemon. Daemon-wide switch,
|
||||
# left enabled on 'fips-ap-setup remove'.
|
||||
# lan:
|
||||
# enabled: true
|
||||
|
||||
tun:
|
||||
enabled: true
|
||||
name: fips0
|
||||
mtu: 1280
|
||||
|
||||
dns:
|
||||
enabled: true
|
||||
# bind_addr defaults to "::1" (IPv6 loopback). The shipped
|
||||
# fips-dns-setup script configures systemd-resolved with a global
|
||||
# /etc/systemd/resolved.conf.d/fips.conf drop-in pointing at
|
||||
# [::1]:5354.
|
||||
#
|
||||
# Set "::" to expose the responder to mesh peers as well (e.g. for
|
||||
# gateway hosts that resolve .fips on behalf of LAN clients). The
|
||||
# mesh-interface filter in src/upper/dns.rs will still defend
|
||||
# /etc/fips/hosts aliases from cross-mesh enumeration.
|
||||
# bind_addr: "::1"
|
||||
port: 5354
|
||||
|
||||
transports:
|
||||
udp:
|
||||
# Dual-stack wildcard, not "0.0.0.0": access-SSID clients (phones) learn
|
||||
# this node's addresses from the mDNS advert and prefer the IPv6
|
||||
# link-local — a v4-only bind silently drops their Noise msg1.
|
||||
# OpenWrt is Linux (bindv6only=0), so "[::]" accepts v4 too.
|
||||
bind_addr: "[::]:2121"
|
||||
# advertise_on_nostr: true
|
||||
# public: false # false => advertise udp:nat; true => advertise bound host:port
|
||||
# accept_connections: true # default; refuse inbound msg1 when false
|
||||
# outbound_only: false # true => bind ephemeral, no listener on a
|
||||
# # known port. Forces advertise_on_nostr=false
|
||||
# # and accept_connections=false. Pure-client
|
||||
# # posture; bind_addr is ignored.
|
||||
|
||||
tcp:
|
||||
# Accepts inbound connections. No static outbound peers.
|
||||
bind_addr: "0.0.0.0:8443"
|
||||
# advertise_on_nostr: true
|
||||
|
||||
# Ethernet transport — physical port names, NOT bridge names.
|
||||
# Run 'ip link show' on the router to identify port names.
|
||||
ethernet:
|
||||
wan:
|
||||
interface: "eth0"
|
||||
listen: true
|
||||
announce: true
|
||||
auto_connect: true
|
||||
accept_connections: true
|
||||
wwan:
|
||||
interface: "phy0-sta0"
|
||||
listen: true
|
||||
announce: true
|
||||
auto_connect: true
|
||||
accept_connections: true
|
||||
lan:
|
||||
interface: "br-lan"
|
||||
listen: true
|
||||
announce: true
|
||||
auto_connect: true
|
||||
accept_connections: true
|
||||
|
||||
# 802.11s mesh backhaul between FIPS routers. These entries ship
|
||||
# commented out so a stock install that never creates fips-mesh*
|
||||
# logs no per-boot "interface missing" bind warning. Running
|
||||
# 'fips-mesh-setup <radio>' creates the interface AND uncomments the
|
||||
# matching block here (once per radio; radio0 -> fips-mesh0, radio1 ->
|
||||
# fips-mesh1); 'fips-mesh-setup remove' re-comments it. Restart fips
|
||||
# after — a transport whose interface is missing at startup is skipped,
|
||||
# not retried. Dual-band routers can mesh on both bands at once —
|
||||
# failover, not multipath: FIPS keeps one active link per peer, the
|
||||
# other band stands by. The mesh runs OPEN (no SAE) with 802.11s
|
||||
# forwarding off: FIPS's Noise handshake is the encryption and
|
||||
# authentication, and FIPS is the routing layer. See
|
||||
# docs/how-to/set-up-80211s-mesh-backhaul.md.
|
||||
# mesh0:
|
||||
# interface: "fips-mesh0"
|
||||
# listen: true
|
||||
# announce: true
|
||||
# auto_connect: true
|
||||
# accept_connections: true
|
||||
# mesh1:
|
||||
# interface: "fips-mesh1"
|
||||
# listen: true
|
||||
# announce: true
|
||||
# auto_connect: true
|
||||
# accept_connections: true
|
||||
|
||||
# Open "!FIPS" access SSID for phones and laptops running FIPS. These
|
||||
# entries ship commented out so a stock install that never creates
|
||||
# fips-ap* logs no per-boot "interface missing" bind warning. Running
|
||||
# 'fips-ap-setup <radio>' creates the interface AND uncomments the
|
||||
# matching block here (once per radio; radio0 -> fips-ap0, radio1 ->
|
||||
# fips-ap1); 'fips-ap-setup remove' re-comments it. Restart fips after
|
||||
# — a transport whose interface is missing at startup is skipped, not
|
||||
# retried. The SSID is OPEN and isolated on purpose: FIPS's Noise
|
||||
# handshake is the only security layer, and associated clients reach
|
||||
# nothing but the FIPS handshake surface. See
|
||||
# docs/how-to/set-up-open-access-ssid.md.
|
||||
# ap0:
|
||||
# interface: "fips-ap0"
|
||||
# listen: true
|
||||
# announce: true
|
||||
# auto_connect: true
|
||||
# accept_connections: true
|
||||
# ap1:
|
||||
# interface: "fips-ap1"
|
||||
# listen: true
|
||||
# announce: true
|
||||
# auto_connect: true
|
||||
# accept_connections: true
|
||||
|
||||
# No BLE transport: OpenWrt builds target musl, which has no BlueZ backend.
|
||||
|
||||
# Outbound LAN gateway. dnsmasq forwards .fips queries to listen=[::1]:5353
|
||||
# while it runs (configured by the fips-gateway init script). Requires IPv6
|
||||
# forwarding enabled.
|
||||
gateway:
|
||||
enabled: true
|
||||
pool: "fd01::/112"
|
||||
lan_interface: "br-lan"
|
||||
dns:
|
||||
listen: "[::1]:5353"
|
||||
upstream: "[::1]:5354"
|
||||
ttl: 60
|
||||
pool_grace_period: 60
|
||||
|
||||
peers: []
|
||||
# Static peers for bootstrapping (UDP or TCP):
|
||||
# - npub: "npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98"
|
||||
# alias: "gateway"
|
||||
# via_nostr: true
|
||||
# addresses:
|
||||
# - transport: udp
|
||||
# addr: "test-us01.fips.network:2121" # IP or hostname (e.g., "peer.example.com:2121")
|
||||
# - transport: udp
|
||||
# addr: "nat" # Use node.rendezvous.nostr for Nostr/STUN hole punching
|
||||
# connect_policy: auto_connect
|
||||
@@ -28,9 +28,22 @@ RELEASED_PRERM="$REPO/testing/openwrt/fixtures/released-prerm"
|
||||
INIT_GATEWAY="$REPO/packaging/openwrt-ipk/files/etc/init.d/fips-gateway"
|
||||
APK_SCRIPTS="${APK_SCRIPTS:-}"
|
||||
SHIPPED_YAML="$REPO/packaging/openwrt-ipk/files/etc/fips/fips.yaml"
|
||||
# The fips.yaml every release up to 0.5.1 shipped, from before the gateway's
|
||||
# default DNS port moved.
|
||||
RELEASED_YAML="$REPO/testing/openwrt/fixtures/released-fips.yaml"
|
||||
GATEWAY_RS="$REPO/src/config/gateway.rs"
|
||||
SETUP_SCRIPT="$REPO/packaging/openwrt-ipk/files/etc/uci-defaults/90-fips-setup"
|
||||
LEGACY_LISTEN=' listen: "[::1]:5353"'
|
||||
SHIPPED_LISTEN=' # listen: "[::1]:5365" # the default; the init script points dnsmasq at this port'
|
||||
MIGRATED_MSG='fips: moved gateway.dns.listen off the mDNS port 5353 to the default [::1]:5365'
|
||||
|
||||
WORK=/tmp/fips-openwrt-scenarios
|
||||
UPGRADE_MARKER=/tmp/fips-prerm-upgrade
|
||||
# The uci stub's state and the directory holding the executable stubs. Fixed
|
||||
# paths, because the .apk scripts run with only PATH in their environment.
|
||||
UCI_DIR=/tmp/fips-openwrt-uci
|
||||
STUB_BIN=/tmp/fips-openwrt-bin
|
||||
DNSMASQ_OPT='dhcp.@dnsmasq[0].server'
|
||||
|
||||
FAILURES=0
|
||||
CASES=0
|
||||
@@ -124,6 +137,27 @@ assert_not_called() {
|
||||
return 0
|
||||
}
|
||||
|
||||
assert_none_called_with_prefix() {
|
||||
# assert_none_called_with_prefix <prefix> <what it means>
|
||||
# Fails if any recorded call begins with <prefix>, whatever follows it.
|
||||
if [ ! -r "$CALLS" ]; then
|
||||
bad "$2 — the call log $CALLS cannot be read"
|
||||
return 0
|
||||
fi
|
||||
matched=""
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
"$1"*) matched="$matched$line;" ;;
|
||||
esac
|
||||
done < "$CALLS"
|
||||
if [ -n "$matched" ]; then
|
||||
bad "$2 — called: $matched"
|
||||
else
|
||||
ok "$2"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
assert_file_is() {
|
||||
# assert_file_is <file> <expected contents> <what it means>
|
||||
got="$(cat "$1" 2>/dev/null)"
|
||||
@@ -182,6 +216,76 @@ assert_order() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# Install an executable uci that keeps each option as a file of values, one per
|
||||
# line, under $UCI_DIR. Like the real uci, "get" prints a list on one line
|
||||
# separated by single spaces and fails for an option with no values, and
|
||||
# del_list removes every copy of the value. Other commands are only logged.
|
||||
install_uci_stub() {
|
||||
rm -rf "$UCI_DIR"
|
||||
mkdir -p "$UCI_DIR" "$STUB_BIN"
|
||||
{
|
||||
echo '#!/bin/sh'
|
||||
echo "dir=$UCI_DIR"
|
||||
cat <<'STUB'
|
||||
[ "${1:-}" = "-q" ] && shift
|
||||
cmd="${1:-}"
|
||||
[ $# -gt 0 ] && shift
|
||||
echo "uci $cmd $*" >> "$dir/log"
|
||||
file_of() {
|
||||
printf '%s/%s' "$dir" "$(printf '%s' "$1" | sed 's/[^A-Za-z0-9._-]/_/g')"
|
||||
}
|
||||
case "$cmd" in
|
||||
get)
|
||||
f="$(file_of "$1")"
|
||||
[ -s "$f" ] || exit 1
|
||||
tr '\n' ' ' < "$f" | sed 's/ $//'
|
||||
echo
|
||||
;;
|
||||
add_list)
|
||||
echo "${1#*=}" >> "$(file_of "${1%%=*}")"
|
||||
;;
|
||||
del_list)
|
||||
f="$(file_of "${1%%=*}")"
|
||||
if [ -f "$f" ]; then
|
||||
grep -vxF -- "${1#*=}" "$f" > "$f.new"
|
||||
mv "$f.new" "$f"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
STUB
|
||||
} > "$STUB_BIN/uci"
|
||||
chmod 0755 "$STUB_BIN/uci"
|
||||
|
||||
cat > /etc/init.d/dnsmasq <<'STUB'
|
||||
#!/bin/sh
|
||||
echo "dnsmasq $1" >> "$CALLS"
|
||||
STUB
|
||||
chmod 0755 /etc/init.d/dnsmasq
|
||||
return 0
|
||||
}
|
||||
|
||||
uci_seed() {
|
||||
# uci_seed <option> <value>...
|
||||
opt="$1"
|
||||
shift
|
||||
for value in "$@"; do
|
||||
"$STUB_BIN/uci" add_list "$opt=$value"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
uci_sorted() {
|
||||
# uci_sorted <option>: the option's values, sorted, on one line.
|
||||
"$STUB_BIN/uci" -q get "$1" | tr ' ' '\n' | sort | tr '\n' ' '
|
||||
return 0
|
||||
}
|
||||
|
||||
sorted_words() {
|
||||
printf '%s\n' "$@" | sort | tr '\n' ' '
|
||||
return 0
|
||||
}
|
||||
|
||||
run_apk_script() {
|
||||
# run_apk_script <phase> <args...>
|
||||
# Runs one captured .apk script the way apk-tools v3 does: executed
|
||||
@@ -357,9 +461,21 @@ scenario_start_service_guard() {
|
||||
reset_state
|
||||
CONFIG="$SHIPPED_YAML"
|
||||
start_service >/dev/null 2>&1
|
||||
assert_called "dnsmasq_swap 5353" "an enabled gateway still redirects dnsmasq"
|
||||
assert_called "dnsmasq_swap 5365" "an enabled gateway redirects dnsmasq to the default port"
|
||||
assert_called "procd_open_instance" "an enabled gateway still starts the daemon"
|
||||
|
||||
reset_state
|
||||
CONFIG="$WORK/explicit-5353.yaml"
|
||||
cat > "$CONFIG" <<'YAML'
|
||||
gateway:
|
||||
enabled: true
|
||||
pool: "fd01::/112"
|
||||
dns:
|
||||
listen: "[::1]:5353"
|
||||
YAML
|
||||
start_service >/dev/null 2>&1
|
||||
assert_called "dnsmasq_swap 5353" "dnsmasq follows an explicit gateway.dns.listen"
|
||||
|
||||
reset_state
|
||||
CONFIG="$WORK/disabled.yaml"
|
||||
cat > "$CONFIG" <<'YAML'
|
||||
@@ -368,7 +484,7 @@ gateway:
|
||||
pool: "fd01::/112"
|
||||
YAML
|
||||
start_service >/dev/null 2>&1
|
||||
assert_not_called "dnsmasq_swap 5353" "a disabled gateway does not redirect dnsmasq"
|
||||
assert_none_called_with_prefix "dnsmasq_swap " "a disabled gateway does not redirect dnsmasq"
|
||||
assert_not_called "add_global_prefix" "a disabled gateway does not add the LAN prefix"
|
||||
assert_not_called "add_ra_route" "a disabled gateway does not advertise the pool route"
|
||||
assert_not_called "procd_open_instance" "a disabled gateway does not start the daemon"
|
||||
@@ -434,6 +550,289 @@ scenario_apk_upgrade_disabled() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── 11. gateway_dns_port reads the port the gateway will bind ───────────────
|
||||
scenario_dns_port_reader() {
|
||||
note "scenario 11: gateway_dns_port"
|
||||
reset_state
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
. "$INIT_GATEWAY"
|
||||
|
||||
CONFIG="$SHIPPED_YAML"
|
||||
assert_equals "$(gateway_dns_port)" "5365" "the shipped fips.yaml reads as the default port"
|
||||
|
||||
CONFIG="$RELEASED_YAML"
|
||||
assert_equals "$(gateway_dns_port)" "5353" "the previously shipped fips.yaml reads as 5353"
|
||||
|
||||
CONFIG="$WORK/v4.yaml"
|
||||
cat > "$CONFIG" <<'YAML'
|
||||
gateway:
|
||||
enabled: true
|
||||
dns:
|
||||
listen: '127.0.0.1:5400'
|
||||
YAML
|
||||
assert_equals "$(gateway_dns_port)" "5400" "a single-quoted IPv4 listen address reads as its port"
|
||||
|
||||
CONFIG="$WORK/port-forward.yaml"
|
||||
cat > "$CONFIG" <<'YAML'
|
||||
gateway:
|
||||
enabled: true
|
||||
pool: "fd01::/112"
|
||||
port_forwards:
|
||||
- listen_port: 8080
|
||||
proto: tcp
|
||||
target: "[fd00::1]:80"
|
||||
YAML
|
||||
assert_equals "$(gateway_dns_port)" "5365" "a port forward's listen_port is not the DNS listen port"
|
||||
|
||||
CONFIG="$WORK/transport-listen.yaml"
|
||||
cat > "$CONFIG" <<'YAML'
|
||||
transports:
|
||||
ethernet:
|
||||
lan:
|
||||
interface: "br-lan"
|
||||
listen: true
|
||||
gateway:
|
||||
enabled: true
|
||||
pool: "fd01::/112"
|
||||
YAML
|
||||
assert_equals "$(gateway_dns_port)" "5365" "a listen key outside the gateway block is ignored"
|
||||
|
||||
CONFIG="$WORK/commented.yaml"
|
||||
cat > "$CONFIG" <<'YAML'
|
||||
gateway:
|
||||
enabled: true
|
||||
dns:
|
||||
# listen: "[::1]:5400"
|
||||
upstream: "[::1]:5354"
|
||||
YAML
|
||||
assert_equals "$(gateway_dns_port)" "5365" "a commented listen line is ignored"
|
||||
|
||||
CONFIG="$WORK/no-gateway.yaml"
|
||||
cat > "$CONFIG" <<'YAML'
|
||||
dns:
|
||||
enabled: true
|
||||
|
||||
peers: []
|
||||
YAML
|
||||
assert_equals "$(gateway_dns_port)" "5365" "a config with no gateway block reads as the default port"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── 12. The init script's default port matches the gateway's ───────────────
|
||||
scenario_default_port_parity() {
|
||||
note "scenario 12: GW_DNS_DEFAULT matches DEFAULT_DNS_LISTEN"
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
. "$INIT_GATEWAY"
|
||||
|
||||
rust_listen="$(sed -n 's/^const DEFAULT_DNS_LISTEN: &str = "\(.*\)";$/\1/p' "$GATEWAY_RS" 2>/dev/null)"
|
||||
rust_port="${rust_listen##*:}"
|
||||
case "$rust_port" in
|
||||
'' | *[!0-9]*)
|
||||
bad "could not read a port from DEFAULT_DNS_LISTEN in $GATEWAY_RS (got '$rust_listen')"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
assert_equals "${GW_DNS_DEFAULT:-}" "$rust_port" "the init script's GW_DNS_DEFAULT is the gateway's default port"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── 13. The swap clears every loopback .fips forward ────────────────────────
|
||||
# The real dnsmasq_swap_fips_upstream runs against the uci stub.
|
||||
scenario_swap_cleanup() {
|
||||
note "scenario 13: dnsmasq_swap_fips_upstream"
|
||||
reset_state
|
||||
install_uci_stub
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
. "$INIT_GATEWAY"
|
||||
|
||||
saved_path="$PATH"
|
||||
PATH="$STUB_BIN:$PATH"
|
||||
|
||||
uci_seed "$DNSMASQ_OPT" "/fips/::1#5353" "/fips/127.0.0.1#5400" "/fips/192.168.1.5#53" "/lan/192.168.1.2"
|
||||
lines="$(uci -q get "$DNSMASQ_OPT" | wc -l | tr -d ' ')"
|
||||
assert_equals "$lines" "1" "the uci stub prints a list on one line, as uci does"
|
||||
|
||||
dnsmasq_swap_fips_upstream 5365 >/dev/null 2>&1
|
||||
assert_equals "$(uci_sorted "$DNSMASQ_OPT")" \
|
||||
"$(sorted_words /lan/192.168.1.2 /fips/192.168.1.5#53 /fips/::1#5365)" \
|
||||
"the swap to 5365 clears every loopback .fips forward and keeps the others"
|
||||
|
||||
dnsmasq_swap_fips_upstream 5354 >/dev/null 2>&1
|
||||
assert_equals "$(uci_sorted "$DNSMASQ_OPT")" \
|
||||
"$(sorted_words /lan/192.168.1.2 /fips/192.168.1.5#53 /fips/::1#5354)" \
|
||||
"the swap back to 5354 leaves only the daemon's loopback forward"
|
||||
assert_called "dnsmasq restart" "the swap restarts dnsmasq"
|
||||
|
||||
PATH="$saved_path"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── 14. 90-fips-setup migrates the previously shipped listen line ──────────
|
||||
# The real 90-fips-setup runs, executed by the ipk postinst and the apk
|
||||
# post-upgrade script, and sourced in a subshell as OpenWrt's default_postinst
|
||||
# and first-boot uci-defaults run do. uci, logger, modprobe and sysctl are
|
||||
# stubs on PATH, and in /usr/sbin for the apk script, whose PATH is fixed.
|
||||
|
||||
install_setup_stubs() {
|
||||
install_uci_stub
|
||||
for cmd in logger modprobe sysctl; do
|
||||
printf '#!/bin/sh\necho "%s $*" >> %s/stub-calls\n' "$cmd" "$UCI_DIR" > "$STUB_BIN/$cmd"
|
||||
chmod 0755 "$STUB_BIN/$cmd"
|
||||
done
|
||||
mkdir -p /usr/sbin /etc/modules.d /etc/fips
|
||||
# rm first: cp onto a busybox applet link would overwrite busybox itself.
|
||||
for cmd in uci logger modprobe sysctl; do
|
||||
rm -f "/usr/sbin/$cmd"
|
||||
cp "$STUB_BIN/$cmd" "/usr/sbin/$cmd"
|
||||
done
|
||||
cp "$SETUP_SCRIPT" /etc/uci-defaults/90-fips-setup
|
||||
chmod 0755 /etc/uci-defaults/90-fips-setup
|
||||
return 0
|
||||
}
|
||||
|
||||
remove_setup_stubs() {
|
||||
for cmd in uci logger modprobe sysctl; do
|
||||
rm -f "/usr/sbin/$cmd"
|
||||
done
|
||||
rm -rf "$STUB_BIN" /etc/fips
|
||||
return 0
|
||||
}
|
||||
|
||||
install_config() {
|
||||
# install_config <file>: installed mode 0644, so a mode check can fail.
|
||||
cp "$1" /etc/fips/fips.yaml
|
||||
chmod 0644 /etc/fips/fips.yaml
|
||||
return 0
|
||||
}
|
||||
|
||||
assert_migrated() {
|
||||
# assert_migrated <how the script ran>
|
||||
CONFIG=/etc/fips/fips.yaml
|
||||
assert_equals "$(gateway_dns_port)" "5365" "$1: the init script reads the default port"
|
||||
changes="$(diff -U0 "$RELEASED_YAML" /etc/fips/fips.yaml 2>/dev/null | grep -v '^---' | grep -v '^+++' | grep '^[-+]')"
|
||||
assert_equals "$changes" "$(printf '%s\n%s' "-$LEGACY_LISTEN" "+$SHIPPED_LISTEN")" \
|
||||
"$1: only the listen line changed, to the shipped form"
|
||||
assert_equals "$(stat -c %a /etc/fips/fips.yaml 2>/dev/null)" "600" "$1: the file is mode 0600"
|
||||
return 0
|
||||
}
|
||||
|
||||
run_setup() {
|
||||
# run_setup <output file>: executes the installed 90-fips-setup.
|
||||
sh /etc/uci-defaults/90-fips-setup > "$1" 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
scenario_listen_migration() {
|
||||
note "scenario 14: 90-fips-setup migrates the shipped gateway listen line"
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
. "$INIT_GATEWAY"
|
||||
saved_path="$PATH"
|
||||
PATH="$STUB_BIN:$PATH"
|
||||
|
||||
# 1. Executed by the ipk postinst on an upgrade.
|
||||
reset_state
|
||||
install_setup_stubs
|
||||
install_config "$RELEASED_YAML"
|
||||
touch "$UPGRADE_MARKER"
|
||||
PKG_UPGRADE=1 sh "$POSTINST" configure > "$WORK/postinst.out" 2>&1
|
||||
assert_migrated "ipk upgrade"
|
||||
if grep -qxF "$MIGRATED_MSG" "$WORK/postinst.out"; then
|
||||
ok "ipk upgrade: the migration is reported on stdout"
|
||||
else
|
||||
bad "ipk upgrade: no migration message in: $(tr '\n' ';' < "$WORK/postinst.out")"
|
||||
fi
|
||||
assert_absent /etc/uci-defaults/90-fips-setup "ipk upgrade: the postinst removed 90-fips-setup, so it exited 0"
|
||||
|
||||
# 2. Executed by the apk post-upgrade script.
|
||||
reset_state
|
||||
install_setup_stubs
|
||||
install_config "$RELEASED_YAML"
|
||||
touch "$UPGRADE_MARKER"
|
||||
if run_apk_script post-upgrade 0.6.0-r1 0.5.2-r1; then
|
||||
assert_migrated "apk upgrade"
|
||||
assert_absent /etc/uci-defaults/90-fips-setup "apk upgrade: post-upgrade removed 90-fips-setup, so it exited 0"
|
||||
fi
|
||||
|
||||
# 3. Sourced in a subshell, without the functions earlier scenarios define.
|
||||
reset_state
|
||||
install_setup_stubs
|
||||
install_config "$RELEASED_YAML"
|
||||
(
|
||||
unset -f logger modprobe sysctl sleep 2>/dev/null
|
||||
cd /etc/uci-defaults && . ./90-fips-setup
|
||||
) > "$WORK/sourced.out" 2>&1
|
||||
assert_equals "$?" "0" "sourced: the subshell exits 0"
|
||||
assert_migrated "sourced"
|
||||
|
||||
# 4. Anything but the exact shipped line is left byte-identical.
|
||||
for variant in other-port indent comment no-gateway; do
|
||||
reset_state
|
||||
install_setup_stubs
|
||||
input="$WORK/$variant.yaml"
|
||||
case "$variant" in
|
||||
other-port) sed 's|^ listen: "\[::1\]:5353"$| listen: "[::1]:5400"|' "$RELEASED_YAML" > "$input" ;;
|
||||
indent) sed 's|^ listen: "\[::1\]:5353"$| listen: "[::1]:5353"|' "$RELEASED_YAML" > "$input" ;;
|
||||
comment) sed 's|^ listen: "\[::1\]:5353"$| listen: "[::1]:5353" # kept|' "$RELEASED_YAML" > "$input" ;;
|
||||
no-gateway) printf 'node:\n identity:\n persistent: true\n\npeers: []\n' > "$input" ;;
|
||||
esac
|
||||
if [ "$variant" != "no-gateway" ] && cmp -s "$input" "$RELEASED_YAML"; then
|
||||
bad "negative case $variant: the fixture edit changed nothing, so the case would prove nothing"
|
||||
continue
|
||||
fi
|
||||
install_config "$input"
|
||||
run_setup "$WORK/$variant.out"
|
||||
if cmp -s "$input" /etc/fips/fips.yaml; then
|
||||
ok "negative case $variant: the file is left byte-identical"
|
||||
else
|
||||
bad "negative case $variant: the file was changed"
|
||||
fi
|
||||
done
|
||||
|
||||
# 5. No config file.
|
||||
reset_state
|
||||
install_setup_stubs
|
||||
rm -f /etc/fips/fips.yaml
|
||||
run_setup "$WORK/no-config.out"
|
||||
assert_equals "$?" "0" "no config: the script exits 0"
|
||||
assert_absent /etc/fips/fips.yaml "no config: no file is created"
|
||||
if grep -qF "$MIGRATED_MSG" "$WORK/no-config.out"; then
|
||||
bad "no config: a migration was reported"
|
||||
else
|
||||
ok "no config: no migration is reported"
|
||||
fi
|
||||
|
||||
# 6. A second run changes nothing.
|
||||
reset_state
|
||||
install_setup_stubs
|
||||
install_config "$RELEASED_YAML"
|
||||
run_setup "$WORK/first.out"
|
||||
cp /etc/fips/fips.yaml "$WORK/migrated.yaml"
|
||||
run_setup "$WORK/second.out"
|
||||
if cmp -s "$WORK/migrated.yaml" /etc/fips/fips.yaml; then
|
||||
ok "a second run leaves the migrated file byte-identical"
|
||||
else
|
||||
bad "a second run changed the migrated file"
|
||||
fi
|
||||
|
||||
# 7. Stale loopback gateway entries for 5353 are removed; others are kept.
|
||||
reset_state
|
||||
install_setup_stubs
|
||||
install_config "$SHIPPED_YAML"
|
||||
uci_seed "$DNSMASQ_OPT" "/fips/::1#5353" "/fips/127.0.0.1#5353" "/fips/192.168.1.5#53"
|
||||
run_setup "$WORK/entries.out"
|
||||
assert_equals "$(uci_sorted "$DNSMASQ_OPT")" \
|
||||
"$(sorted_words /fips/192.168.1.5#53 /fips/::1#5354)" \
|
||||
"the legacy 5353 entries are removed and the daemon's entry is added"
|
||||
|
||||
PATH="$saved_path"
|
||||
remove_setup_stubs
|
||||
return 0
|
||||
}
|
||||
|
||||
echo "OpenWrt maintainer-script scenarios (shell: $(readlink -f /proc/$$/exe 2>/dev/null || echo sh))"
|
||||
echo " postinst: $POSTINST"
|
||||
echo " prerm: $PRERM"
|
||||
@@ -449,6 +848,10 @@ scenario_start_service_guard
|
||||
scenario_apk_fresh_install
|
||||
scenario_apk_upgrade_enabled
|
||||
scenario_apk_upgrade_disabled
|
||||
scenario_dns_port_reader
|
||||
scenario_default_port_parity
|
||||
scenario_swap_cleanup
|
||||
scenario_listen_migration
|
||||
|
||||
echo ""
|
||||
if [ "$FAILURES" -eq 0 ]; then
|
||||
|
||||
Reference in New Issue
Block a user