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:
Johnathan Corgan
2026-09-27 19:35:48 +00:00
parent 84976b1fa2
commit 28f9fc007a
15 changed files with 869 additions and 52 deletions
+5 -3
View File
@@ -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
+7 -6
View File
@@ -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
+9 -4
View File
@@ -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
+2 -2
View File
@@ -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
+7 -6
View File
@@ -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/
+1 -1
View File
@@ -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
+5
View File
@@ -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
View File
@@ -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#"
+38
View File
@@ -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"
+45 -3
View File
@@ -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"
+190
View File
@@ -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
+405 -2
View File
@@ -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