From 2c3a602d9f67b0055441458f911929528486f427 Mon Sep 17 00:00:00 2001 From: MCKero Date: Fri, 28 Aug 2026 05:32:31 +0100 Subject: [PATCH] Capture the DN42 firewall rules as a script The rules restricting port 8080 to DN42 were applied by hand and existed only in the live kernel tables -- lost on reboot, with the port then wide open and nothing in the repo to say it ever had been restricted. The script is idempotent (checks before adding) and has remove/show. show prints packet counters, which is the part that matters: this host's INPUT policy is ACCEPT, so a rule that only allows DN42 does nothing at all. The final DROP is what restricts anything, and a rising DROP count is the only proof it works rather than the traffic simply not arriving. Currently observed: 2203 accepted from DN42 v4, 103 from v6, 22 dropped. --- README.md | 26 ++++++++++++-- tools/dn42_firewall.sh | 78 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) create mode 100755 tools/dn42_firewall.sh diff --git a/README.md b/README.md index 6d8fc25..eae45fd 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ keypresses silently stop working. Run the test after touching that code; tools/ run, screenshot, inject keys, probe state keypad_test.py keypad regression test, boots its own instance webui.py web remote control: live LCD plus clickable keypad + dn42_firewall.sh restrict the web UI port to DN42 sources uvk5_qmp.py QMP client uvk5_lcd.py framebuffer decode, PNG encode, frame grabber uvk5_keys.py key names the keypad model accepts @@ -181,8 +182,29 @@ Two constraints worth knowing before you use it: - **The QMP socket takes one client.** While the server is up, `tools/key.py` cannot talk to the same emulator. -- **There is no authentication.** It binds loopback, and anyone who reaches the - port has full control of the emulated radio. Do not expose it. +- **There is no authentication.** Anyone who reaches the port has full control of + the emulated radio. It binds loopback by default for that reason. + +### Reaching it from elsewhere + +`--host ::` makes it reachable off-box, which with no authentication means the +port must be filtered by source address. `tools/dn42_firewall.sh` restricts it to +DN42: + + tools/dn42_firewall.sh apply 8080 # DN42 + loopback only + tools/dn42_firewall.sh show 8080 # rules and packet counts + tools/dn42_firewall.sh remove 8080 + +One detail that is easy to get wrong: this host's `INPUT` policy is `ACCEPT`, so a +rule that only *allows* DN42 changes nothing -- the port is already reachable with +no rules at all. The rule that does the work is the final `DROP`. Verify by +watching the counters rather than by assuming: + + tools/dn42_firewall.sh show 8080 + # a rising DROP count means non-DN42 traffic is actually being rejected + +The rules do not survive a reboot. Re-run `apply`, or persist them with +`iptables-persistent`. There is no PTT button: the keypad model has no PTT line, so the `press` property rejects the name. Unknown keys are rejected with 400 rather than forwarded. diff --git a/tools/dn42_firewall.sh b/tools/dn42_firewall.sh new file mode 100755 index 0000000..9cd05de --- /dev/null +++ b/tools/dn42_firewall.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Restrict the web UI port to DN42 sources. +# +# tools/webui.py has no authentication: anyone who reaches the port has full +# control of the emulated radio. Binding to :: makes it reachable from the public +# internet, so the port has to be filtered by source address instead. +# +# The important detail: the INPUT policy on this host is ACCEPT, so a rule that +# only *allows* DN42 does nothing at all -- without any rules the port is already +# reachable. What actually restricts anything is the final DROP. Order matters: +# accept loopback and DN42 first, then drop the rest. +# +# DN42 ranges: 172.20.0.0/14 (v4) and fd00::/8 (v6). This host already had rules +# using those same prefixes for SIP, so the convention is established. +# +# These rules do NOT survive a reboot. Re-run this script, or persist them with +# iptables-persistent / a systemd unit. +# +# Usage: +# tools/dn42_firewall.sh apply [port] # default port 8080 +# tools/dn42_firewall.sh remove [port] +# tools/dn42_firewall.sh show [port] +set -uo pipefail + +ACTION="${1:-show}" +PORT="${2:-8080}" +TAG="uvk5-webui" + +need_root() { + if [ "$(id -u)" -ne 0 ]; then + echo "needs root for iptables" >&2 + exit 1 + fi +} + +# Add a rule only when an identical one is absent, so re-running is safe. +add4() { iptables -C INPUT "$@" 2>/dev/null || iptables -A INPUT "$@"; } +add6() { ip6tables -C INPUT "$@" 2>/dev/null || ip6tables -A INPUT "$@"; } +del4() { while iptables -C INPUT "$@" 2>/dev/null; do iptables -D INPUT "$@"; done; } +del6() { while ip6tables -C INPUT "$@" 2>/dev/null; do ip6tables -D INPUT "$@"; done; } + +case "$ACTION" in +apply) + need_root + # Accept first... + add4 -p tcp --dport "$PORT" -s 127.0.0.1 -j ACCEPT -m comment --comment "$TAG: loopback" + add4 -p tcp --dport "$PORT" -s 172.20.0.0/14 -j ACCEPT -m comment --comment "$TAG: dn42 v4" + add6 -p tcp --dport "$PORT" -s ::1 -j ACCEPT -m comment --comment "$TAG: loopback" + add6 -p tcp --dport "$PORT" -s fd00::/8 -j ACCEPT -m comment --comment "$TAG: dn42 v6" + # ...then drop everything else. This is the rule that does the work. + add4 -p tcp --dport "$PORT" -j DROP -m comment --comment "$TAG: deny non-dn42" + add6 -p tcp --dport "$PORT" -j DROP -m comment --comment "$TAG: deny non-dn42" + echo "applied: port $PORT reachable from DN42 and loopback only" + ;; +remove) + need_root + del4 -p tcp --dport "$PORT" -j DROP -m comment --comment "$TAG: deny non-dn42" + del6 -p tcp --dport "$PORT" -j DROP -m comment --comment "$TAG: deny non-dn42" + del4 -p tcp --dport "$PORT" -s 127.0.0.1 -j ACCEPT -m comment --comment "$TAG: loopback" + del4 -p tcp --dport "$PORT" -s 172.20.0.0/14 -j ACCEPT -m comment --comment "$TAG: dn42 v4" + del6 -p tcp --dport "$PORT" -s ::1 -j ACCEPT -m comment --comment "$TAG: loopback" + del6 -p tcp --dport "$PORT" -s fd00::/8 -j ACCEPT -m comment --comment "$TAG: dn42 v6" + echo "removed: port $PORT is no longer filtered by these rules" + ;; +show) + echo "--- v4 ---" + iptables -S INPUT 2>/dev/null | grep -- "--dport $PORT" || echo "(none)" + echo "--- v6 ---" + ip6tables -S INPUT 2>/dev/null | grep -- "--dport $PORT" || echo "(none)" + echo "--- packet counts (a rising DROP count means the filter is working) ---" + iptables -L INPUT -v -n 2>/dev/null | grep "dpt:$PORT" || true + ip6tables -L INPUT -v -n 2>/dev/null | grep "dpt:$PORT" || true + ;; +*) + echo "usage: $0 {apply|remove|show} [port]" >&2 + exit 2 + ;; +esac