mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 11:08:25 +00:00
shellcheck reports SC2013 on the for loop over a command substitution in gateway_dns_held_by, and the OpenWrt Package workflow fails its lint step on it. Capture the awk output in a variable and iterate that instead; the words the loop sees are the same. cat stays in front of awk because busybox awk gives up on a missing /proc/net/udp6, and a while-read loop at the end of the pipe would run in a subshell under ash, where returning from the function is not possible.
374 lines
13 KiB
Bash
Executable File
374 lines
13 KiB
Bash
Executable File
#!/bin/sh /etc/rc.common
|
|
# FIPS outbound LAN gateway — procd init script for OpenWrt
|
|
#
|
|
# Not enabled by default. Enable with:
|
|
# service fips-gateway enable
|
|
# service fips-gateway start
|
|
#
|
|
# Requires: fips daemon running, gateway section in /etc/fips/fips.yaml,
|
|
# IPv6 forwarding enabled.
|
|
|
|
USE_PROCD=1
|
|
START=96
|
|
STOP=09
|
|
|
|
# procd runs "supervise" (below) rather than the gateway itself.
|
|
EXTRA_COMMANDS="supervise"
|
|
EXTRA_HELP="\tsupervise Run the gateway, pointing dnsmasq at it while it listens (run by procd)\n"
|
|
|
|
PROG=/usr/bin/fips-gateway
|
|
CONFIG=/etc/fips/fips.yaml
|
|
|
|
# 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
|
|
# Held while dnsmasq's .fips upstream is changed, so a stopping gateway's swap
|
|
# back to the daemon and a starting gateway's swap to itself cannot interleave.
|
|
DNS_LOCK=/var/lock/fips-gateway-dns.lock
|
|
# The pid of the gateway the current supervise runs, so an exiting instance
|
|
# can tell its successor's socket from anything else holding the port.
|
|
GW_PIDFILE=/var/run/fips-gateway.pid
|
|
# Seconds a stopping gateway gets after SIGTERM before it is killed; under
|
|
# procd's own 5 s, after which procd kills only the supervise shell.
|
|
GW_KILL_AFTER=3
|
|
# Left by the package's preinst when the gateway is not enabled; see below.
|
|
INSTALL_HOLD=/var/run/fips-gateway-install-hold
|
|
|
|
# Global-scope IPv6 prefix assigned to br-lan so Android/Chrome clients
|
|
# believe they have full IPv6 and actually send AAAA queries.
|
|
# Uses RFC 5180 (benchmarking) range — not routed on the public internet.
|
|
GLOBAL_PREFIX="2001:2:f1b5::1/64"
|
|
|
|
start_service() {
|
|
# OpenWrt's generic postinst enables and starts every init script a
|
|
# package ships. A package built from the SDK feed Makefile runs it, and
|
|
# its preinst leaves this hold unless the gateway was enabled, so that
|
|
# installing or upgrading the package does not switch the gateway on.
|
|
# Honoured once.
|
|
if [ -e "$INSTALL_HOLD" ]; then
|
|
rm -f "$INSTALL_HOLD"
|
|
disable
|
|
logger -t fips-gateway "left disabled and stopped by the package installation"
|
|
return 0
|
|
fi
|
|
|
|
# The gateway daemon exits when gateway.enabled is not true, so without
|
|
# this check starting a disabled gateway would still take dnsmasq's .fips
|
|
# upstream away from the daemon and point it at a port nothing listens on.
|
|
if [ "$(gateway_config_enabled)" != "true" ]; then
|
|
logger -t fips-gateway "gateway.enabled is not true in $CONFIG; not starting"
|
|
return 1
|
|
fi
|
|
|
|
# Apply gateway sysctls (proxy_ndp, IPv6 forwarding).
|
|
sysctl -p /etc/sysctl.d/fips-gateway.conf 2>/dev/null || true
|
|
|
|
# Load conntrack module for /proc/net/nf_conntrack.
|
|
modprobe nf_conntrack 2>/dev/null || true
|
|
|
|
# Add a global-scope IPv6 prefix to br-lan so Android/Chrome clients
|
|
# send AAAA queries (they suppress AAAA when only ULA addresses exist).
|
|
# Also set ra_default=2 so odhcpd advertises a default route even
|
|
# without upstream IPv6 — needed for clients to accept the prefix.
|
|
gateway_add_global_prefix
|
|
|
|
# Advertise the virtual IP pool via Router Advertisement so LAN clients
|
|
# learn a route to the pool automatically.
|
|
gateway_add_ra_route
|
|
|
|
# dnsmasq's .fips forwarding moves to the gateway only once the gateway
|
|
# holds its DNS port, and back to the daemon (5354) whenever the gateway
|
|
# exits, so a gateway that fails to start, before or after binding, does
|
|
# not leave LAN clients' .fips lookups going to a dead port. See supervise.
|
|
procd_open_instance
|
|
procd_set_param command /etc/init.d/fips-gateway supervise
|
|
procd_set_param respawn 3600 5 5
|
|
procd_set_param stdout 1
|
|
procd_set_param stderr 1
|
|
procd_close_instance
|
|
}
|
|
|
|
stop_service() {
|
|
# Restore dnsmasq .fips forwarding back to the daemon.
|
|
dns_locked dnsmasq_swap_fips_upstream "$DAEMON_DNS_PORT"
|
|
|
|
# Remove the RA route for the virtual IP pool.
|
|
gateway_remove_ra_route
|
|
|
|
# Remove the global prefix and ra_default override.
|
|
gateway_remove_global_prefix
|
|
}
|
|
|
|
reload_service() {
|
|
restart
|
|
}
|
|
|
|
# Run the gateway as procd's instance. dnsmasq's .fips upstream is pointed at
|
|
# the gateway's DNS port once the gateway itself has that port bound, and back
|
|
# at the daemon's port when the gateway exits for any reason: a config it
|
|
# cannot parse, a port it cannot bind, a failure after binding, a crash, or the
|
|
# SIGTERM procd sends to stop it. That SIGTERM is passed on so the gateway can
|
|
# remove its NAT table and routes, and a gateway still running $GW_KILL_AFTER
|
|
# seconds later is killed, because procd kills only this shell. The exit
|
|
# status is the gateway's, so procd's respawn sees the gateway's failures.
|
|
supervise() {
|
|
local port pid="" watcher rc stopping="" killer=""
|
|
|
|
port="$(gateway_dns_port)"
|
|
# Set before the gateway starts, so a stop that arrives first still
|
|
# reaches it.
|
|
trap 'stopping=1; gateway_stop' TERM INT
|
|
"$PROG" --config "$CONFIG" &
|
|
pid=$!
|
|
mkdir -p "${GW_PIDFILE%/*}" 2>/dev/null
|
|
echo "$pid" > "$GW_PIDFILE"
|
|
[ -n "$stopping" ] && gateway_stop
|
|
|
|
# Polled from a child of its own so this shell sits in wait and reaps the
|
|
# gateway the moment it exits; a polling loop here would see an unreaped
|
|
# gateway as still alive.
|
|
(
|
|
while kill -0 "$pid" 2>/dev/null; do
|
|
dns_locked gateway_dns_claim "$port" "$pid" && exit 0
|
|
sleep 1
|
|
done
|
|
exit 0
|
|
) &
|
|
watcher=$!
|
|
|
|
# A trapped signal ends wait early, so wait until the gateway is gone.
|
|
rc=0
|
|
wait "$pid" || rc=$?
|
|
while kill -0 "$pid" 2>/dev/null; do
|
|
rc=0
|
|
wait "$pid" || rc=$?
|
|
done
|
|
[ -n "$killer" ] && kill "$killer" 2>/dev/null
|
|
# The watcher has nothing left to do, and would otherwise sleep out its
|
|
# poll; any change it is part way through finishes under the lock first.
|
|
kill "$watcher" 2>/dev/null
|
|
wait "$watcher"
|
|
|
|
dns_locked gateway_dns_release "$port" "$pid"
|
|
[ "$(cat "$GW_PIDFILE" 2>/dev/null)" = "$pid" ] && rm -f "$GW_PIDFILE"
|
|
logger -t fips-gateway "gateway exited with status $rc"
|
|
return "$rc"
|
|
}
|
|
|
|
# supervise's stop: SIGTERM to the gateway now, SIGKILL if it is still running
|
|
# $GW_KILL_AFTER seconds later. Reads and sets supervise's pid and killer.
|
|
gateway_stop() {
|
|
[ -n "$pid" ] || return 0
|
|
kill -TERM "$pid" 2>/dev/null
|
|
if [ -z "$killer" ]; then
|
|
( sleep "$GW_KILL_AFTER"; kill -KILL "$pid" 2>/dev/null ) &
|
|
killer=$!
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Run "$@" holding $DNS_LOCK. The lock is released when the subshell, and
|
|
# every process that inherited its descriptor, has exited.
|
|
dns_locked() {
|
|
mkdir -p "${DNS_LOCK%/*}" 2>/dev/null
|
|
(
|
|
flock 9 || logger -t fips-gateway "could not lock $DNS_LOCK; changing dnsmasq anyway"
|
|
"$@"
|
|
) 9>"$DNS_LOCK"
|
|
}
|
|
|
|
# Succeed when process $2 itself holds a UDP socket bound to port $1, on any
|
|
# address, IPv4 or IPv6: a socket inode from /proc/net/udp{,6} for that port
|
|
# is among the process's open descriptors. Another process holding the port,
|
|
# such as an mDNS responder on 5353, does not count.
|
|
gateway_dns_held_by() {
|
|
local hex inodes inode fd
|
|
[ -n "$2" ] || return 1
|
|
hex="$(printf '%04X' "$1")"
|
|
# cat rather than awk's own file arguments: busybox awk gives up on a
|
|
# missing /proc/net/udp6. A while-read loop on the pipe would run in a
|
|
# subshell under ash, where the return below could not leave this function.
|
|
inodes="$(cat /proc/net/udp /proc/net/udp6 2>/dev/null |
|
|
awk -v want=":$hex" 'substr($2, length($2) - 4) == want { print $10 }')"
|
|
for inode in $inodes; do
|
|
for fd in /proc/"$2"/fd/*; do
|
|
[ "$(readlink "$fd" 2>/dev/null)" = "socket:[$inode]" ] && return 0
|
|
done
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Point dnsmasq at the gateway's port $1 once gateway $2 holds it. Fails,
|
|
# changing nothing, until then.
|
|
gateway_dns_claim() {
|
|
gateway_dns_held_by "$1" "$2" || return 1
|
|
dnsmasq_swap_fips_upstream "$1"
|
|
logger -t fips-gateway "gateway DNS is listening on port $1; dnsmasq forwards .fips to it"
|
|
return 0
|
|
}
|
|
|
|
# Point dnsmasq back at the daemon after gateway $2 on port $1 has exited,
|
|
# unless the gateway procd started in its place (the pid in $GW_PIDFILE) holds
|
|
# the port already; that one claims dnsmasq itself.
|
|
gateway_dns_release() {
|
|
local next
|
|
next="$(cat "$GW_PIDFILE" 2>/dev/null)"
|
|
if [ -n "$next" ] && [ "$next" != "$2" ] && gateway_dns_held_by "$1" "$next"; then
|
|
return 0
|
|
fi
|
|
dnsmasq_swap_fips_upstream "$DAEMON_DNS_PORT"
|
|
return 0
|
|
}
|
|
|
|
# Extract the gateway "enabled" flag from fips.yaml.
|
|
# Prints the value indented under the top-level "gateway:" block, or nothing
|
|
# when there is no such block.
|
|
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() {
|
|
awk '/^gateway:/{found=1; next} found && /^[^ ]/{found=0} found && /pool:/{gsub(/.*pool:[[:space:]]*/, ""); gsub(/["'"'"']/, ""); print; exit}' "$CONFIG"
|
|
}
|
|
|
|
# Add an RA-advertised route for the virtual IP pool so LAN clients
|
|
# automatically learn how to reach virtual IPs.
|
|
gateway_add_ra_route() {
|
|
local pool
|
|
pool="$(gateway_pool_cidr)"
|
|
[ -z "$pool" ] && return 0
|
|
|
|
# Add a kernel route on br-lan (needed for RA to advertise it).
|
|
ip -6 route replace "$pool" dev br-lan proto static 2>/dev/null || true
|
|
|
|
# Add a UCI route6 entry for odhcpd to include in Router Advertisements.
|
|
# Remove any stale entry first.
|
|
gateway_remove_ra_route_uci
|
|
uci add dhcp route6 >/dev/null
|
|
uci set dhcp.@route6[-1].interface='lan'
|
|
uci set dhcp.@route6[-1].target="$pool"
|
|
uci commit dhcp
|
|
/etc/init.d/odhcpd restart 2>/dev/null || true
|
|
}
|
|
|
|
# Remove the RA route.
|
|
gateway_remove_ra_route() {
|
|
local pool
|
|
pool="$(gateway_pool_cidr)"
|
|
[ -z "$pool" ] && return 0
|
|
|
|
ip -6 route del "$pool" dev br-lan proto static 2>/dev/null || true
|
|
gateway_remove_ra_route_uci
|
|
/etc/init.d/odhcpd restart 2>/dev/null || true
|
|
}
|
|
|
|
# Remove any existing UCI route6 entries for the pool.
|
|
gateway_remove_ra_route_uci() {
|
|
local i=0
|
|
while uci -q get "dhcp.@route6[$i]" >/dev/null 2>&1; do
|
|
if [ "$(uci -q get "dhcp.@route6[$i].target")" = "$(gateway_pool_cidr)" ]; then
|
|
uci delete "dhcp.@route6[$i]"
|
|
uci commit dhcp
|
|
return 0
|
|
fi
|
|
i=$((i + 1))
|
|
done
|
|
}
|
|
|
|
# Add a global-scope IPv6 prefix to br-lan and enable RA default route.
|
|
gateway_add_global_prefix() {
|
|
# Add the global prefix via UCI (idempotent — remove first).
|
|
local current
|
|
current="$(uci -q get network.lan.ip6addr 2>/dev/null || echo "")"
|
|
if [ "$current" != "$GLOBAL_PREFIX" ]; then
|
|
uci set network.lan.ip6addr="$GLOBAL_PREFIX"
|
|
uci commit network
|
|
fi
|
|
|
|
# Force odhcpd to advertise a default route even without upstream IPv6.
|
|
local ra_default
|
|
ra_default="$(uci -q get dhcp.lan.ra_default 2>/dev/null || echo "")"
|
|
if [ "$ra_default" != "2" ]; then
|
|
uci set dhcp.lan.ra_default='2'
|
|
uci commit dhcp
|
|
fi
|
|
|
|
# Apply network change immediately (odhcpd restarted by gateway_add_ra_route).
|
|
/etc/init.d/network reload 2>/dev/null || true
|
|
}
|
|
|
|
# Remove the global prefix and ra_default override.
|
|
gateway_remove_global_prefix() {
|
|
local current
|
|
current="$(uci -q get network.lan.ip6addr 2>/dev/null || echo "")"
|
|
if [ "$current" = "$GLOBAL_PREFIX" ]; then
|
|
uci delete network.lan.ip6addr
|
|
uci commit network
|
|
/etc/init.d/network reload 2>/dev/null || true
|
|
fi
|
|
|
|
uci -q delete dhcp.lan.ra_default 2>/dev/null || true
|
|
uci commit dhcp
|
|
/etc/init.d/odhcpd restart 2>/dev/null || true
|
|
}
|
|
|
|
# Swap the dnsmasq .fips forwarding port via UCI and restart dnsmasq.
|
|
# $1 = target port number
|
|
dnsmasq_swap_fips_upstream() {
|
|
local port="$1"
|
|
local server
|
|
|
|
# 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
|
|
|
|
# Restart dnsmasq to pick up the change.
|
|
/etc/init.d/dnsmasq restart 2>/dev/null || true
|
|
}
|