diff --git a/os/v3/puppy/embed/etc/init.d/zz-onboard-ports b/os/v3/puppy/embed/etc/init.d/zz-onboard-ports index c27ea1d8..301605ea 100755 --- a/os/v3/puppy/embed/etc/init.d/zz-onboard-ports +++ b/os/v3/puppy/embed/etc/init.d/zz-onboard-ports @@ -14,7 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# /etc/init.d/zz-onboard-ports - the appliance's two inbound ports, opened after Puppy's firewall. +# /etc/init.d/zz-onboard-ports - the appliance's inbound ports and its mDNS responder, opened after Puppy's +# firewall. # # Measured in a booted image: `iptables -S` shows `-P INPUT DROP` with `tcp_inbound` RETURNing to that # policy, so every new inbound TCP connection from outside is dropped. lemond binds 0.0.0.0:13305 and the @@ -35,6 +36,35 @@ open_port() { open_port 13305 # Lemonade (U3) open_port 22 # ssh, the owner's key (U7) +# mDNS (cycle 23; c22 board 4 Nov: avahi logged "1bit-os.local is announced" and a raw mDNS query from ryzen got +# no answer at all). The reason is in the same rc.firewall this file already had to work around, read out of the +# packed c22 puppy sfs: +# +# -P INPUT DROP +# -A INPUT -p ALL -d 224.0.0.1 -j DROP (only .1; a query to 224.0.0.251 gets past this) +# -A INPUT -p UDP -j udp_inbound (drops 137/138, accepts DHCP 67->68, then -j RETURN) +# -A INPUT -m pkttype --pkt-type broadcast -j DROP +# +# so every NEW inbound UDP packet falls through to the DROP policy, and 5353 is UDP. A query is exactly that +# packet, which is why the responder never saw one; its own announcements did reach the wire, because the same +# file ends with `-A OUTPUT -p ALL -j ACCEPT` - so this needs no OUTPUT rule, only the query that confirms it. +# +# LAN-only, the same ranges as hosts.allow below, plus link-local (avahi's fallback when there is no lease): +# announcing to a phone on the LAN is the point, being a public responder is not. `-I INPUT 1` puts these ahead +# of udp_inbound and of the broadcast drop, and -C keeps a re-run a no-op. +open_mdns() { + for src in 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16; do + iptables -C INPUT -p udp --dport 5353 -s "$src" -j ACCEPT 2> /dev/null && continue + iptables -I INPUT 1 -p udp --dport 5353 -s "$src" -j ACCEPT 2> /dev/null && continue + echo "zz-onboard-ports: could not open udp/5353 for $src" >&2 + return 1 + done +} +# shortcut: IPv4 only. 18onboard-mdns's responder answers on both families, but this image's IPv6 is +# unconfigured (no global address, and the board reaches it at 192.168.50.110), so a v6 rule would be untested +# code; add fe80::/10 to this loop when the appliance gets an IPv6 address. +open_mdns + # And the other half of reachability, measured the same way: Puppy ships /etc/hosts.deny = "ALL: ALL" with # /etc/hosts.allow = "ALL: LOCAL", and LOCAL means this machine, so a client on the LAN is refused by # tcpwrappers before ssh's key exchange even starts - ssh says `kex_exchange_identification: Connection diff --git a/os/v3/puppy/mdns-ports-selftest.sh b/os/v3/puppy/mdns-ports-selftest.sh new file mode 100755 index 00000000..34e27a45 --- /dev/null +++ b/os/v3/puppy/mdns-ports-selftest.sh @@ -0,0 +1,78 @@ +#!/bin/sh +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# mdns-ports-selftest.sh [script] - the self-check for zz-onboard-ports' inbound rules, with a stub `iptables`. +# +# No root, no real ruleset, no device: the stub keeps a rule table in a file and answers `-C` from it, so this +# asserts what the shipped script actually issues. The mDNS block is the kind of code that fails silently (a rule +# with the wrong source, or one re-added on every boot), so it gets a check that runs anywhere: +# +# * one insert per LAN range, four of them, with the four sources the file intends; +# * a second run against the same ruleset inserts nothing (`-C` first, which is what keeps a re-run a no-op); +# * the two pre-existing TCP ports are still opened - this script must not have moved them. +# +# The measured behaviour on a booted image (`vm-check`'s `U3 mdns rule` row, and the counter test in #1016's body) +# is what proves the appliance accepts the packet; this proves the script's own shape. +set -u +prog=${0##*/} +here=$(cd "$(dirname "$0")" && pwd) +script=${1:-$here/embed/etc/init.d/zz-onboard-ports} +[ -f "$script" ] || { echo "$prog: no $script" >&2; exit 2; } + +work=$(mktemp -d "${TMPDIR:-/tmp}/mdns-ports.XXXXXX") || exit 2 +trap 'rm -rf "$work"' EXIT INT TERM +mkdir -p "$work/bin" +export CALLS="$work/calls" STATE="$work/state" +: > "$CALLS"; : > "$STATE" + +cat > "$work/bin/iptables" <<'STUB' +#!/bin/sh +# the rule's identity is " ", so `-I INPUT 1 ` and `-C INPUT ` compare directly +echo "$*" >> "$CALLS" +cmd=$1; shift +chain=$1; shift +case $cmd in + -C) grep -qxF "$chain $*" "$STATE" 2>/dev/null && exit 0; exit 1 ;; + -I) shift; echo "$chain $*" >> "$STATE"; exit 0 ;; + -S) printf '%s\n' '-A INPUT -p tcp -m tcp --dport 13305 -j ACCEPT' '-A INPUT -p udp -m udp --dport 5353 -j ACCEPT'; exit 0 ;; + *) exit 0 ;; +esac +STUB +chmod +x "$work/bin/iptables" +PATH="$work/bin:$PATH" sh "$script" > "$work/run1.log" 2>&1 +i1=$(grep -c -- '^-I ' "$CALLS") +c1=$(grep -c -- '-dport 5353' "$CALLS") +u1=$(grep -c -- '^-I .* -p udp --dport 5353 ' "$CALLS") +PATH="$work/bin:$PATH" sh "$script" > "$work/run2.log" 2>&1 +i2=$(grep -c -- '^-I ' "$CALLS") +u2=$(grep -c -- '^-I .* -p udp --dport 5353 ' "$CALLS") +c2=$(grep -c -- '-dport 5353' "$CALLS") + +echo "run 1: $i1 insert(s), $u1 of them udp/5353, $c1 udp/5353 command(s)" +echo "run 2 (same ruleset): $((i2 - i1)) insert(s), $((c2 - c1)) udp/5353 command(s) - all -C, none -I" +fail= +[ "$i1" -ge 6 ] || fail="$fail first-run-inserts=$i1(expected 6: two TCP ports, four udp/5353)" +[ "$u1" -eq 4 ] || fail="$fail udp5353-inserts=$u1(expected 4)" +[ "$u2" -eq 4 ] || fail="$fail re-run-inserted-udp5353=$u2(not idempotent)" +[ "$((c2 - c1))" -eq 4 ] || fail="$fail re-run-udp5353-commands=$((c2 - c1))(expected 4 -C and no -I)" +for src in 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16; do + grep -qxF "INPUT -p udp --dport 5353 -s $src -j ACCEPT" "$STATE" || fail="$fail missing-rule-$src" +done +if [ -n "$fail" ]; then + echo "RESULT: FAIL -$fail" + exit 1 +fi +echo "RESULT: PASS (four udp/5353 rules inserted once, the re-run inserts nothing, sources as intended)" diff --git a/os/v3/puppy/vm-check.sh b/os/v3/puppy/vm-check.sh index 16b0acea..55de53a4 100755 --- a/os/v3/puppy/vm-check.sh +++ b/os/v3/puppy/vm-check.sh @@ -668,6 +668,15 @@ rows = [ # row runs is a race, but an ACCEPT for tcp/13305 is what makes U3 reachable either way. ("U3 port rule", "iptables -C INPUT -p tcp --dport 13305 -j ACCEPT && echo port-13305-open; " "iptables -C INPUT -p tcp --dport 22 -j ACCEPT && echo port-22-open", None), + # Cycle 23 (c22 board, 4 Nov): avahi logged "1bit-os.local is announced" and a raw mDNS query from ryzen got no + # answer at all. The cause is in the same rc.firewall the two rows above work around: every NEW inbound UDP + # packet goes to `udp_inbound`, which accepts only DHCP and RETURNs to the DROP policy, and a query on 5353 is + # UDP - while the announcements themselves left fine (`-A OUTPUT -p ALL -j ACCEPT`). The durable assertion is + # the rule, for the same reason as the ports above; whether the responder is up is read in the same row. + ("U3 mdns rule", "iptables -S INPUT | grep -E -- '-p udp.*--dport 5353.*-j ACCEPT' | head -2; " + "iptables -C INPUT -p udp --dport 5353 -s 10.0.0.0/8 -j ACCEPT 2>/dev/null && echo udp-5353-open; " + "pidof avahi-daemon > /dev/null && echo avahi-up; tail -1 /var/log/mdns.log 2>&1", + "udp-5353-open"), ("net addr", "ip -4 -o addr show 2>/dev/null | tr -s ' '", None), ("net route", "ip -4 route 2>/dev/null | head -3", None), ]