Skip to content

os/v3/puppy: the firewall drops every mDNS query, so 1bit-os.local is announced but never answers - #1016

Merged
bong-water-water-bong merged 7 commits into
mainfrom
os/v3-c23-mdns
Nov 4, 2026
Merged

bong-water-water-bong merged 7 commits into
mainfrom
os/v3-c23-mdns

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

Board, c22 stick (rc-2026-11-04-c22), 4 Nov 12:5x AST — TOS3-C23-NOTES.md item 1: "mDNS is announced but
unreachable: avahi says '1bit-os.local is announced', yet a raw mDNS query from ryzen gets no answer."

Root cause

#952/#953 put the responder in the image and it runs — the c22 image logs 1bit-os.local is announced on this network. What was never opened is the path for a query, and the cause is the same rc.firewall that
zz-onboard-ports already works around for TCP. Read out of the packed c22 puppy SFS, etc/init.d/rc.firewall:

-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
     udp_inbound: -p UDP --dport 137/138 -j DROP
                  -p UDP --sport 67 --dport 68 -j ACCEPT
                  -p UDP -j RETURN                    # back to the DROP policy
-A INPUT -m pkttype --pkt-type broadcast -j DROP
-A OUTPUT -p ALL -j ACCEPT                           # why the announcements DID reach the wire

Every NEW inbound UDP packet falls through to DROP, and an mDNS query is exactly that packet. The appliance's own
announcements are outbound and always left, which is why avahi reported success while nothing answered.

Fix (one file, one function)

os/v3/puppy/embed/etc/init.d/zz-onboard-ports — an INPUT ACCEPT for udp/5353:

  • LAN-only, the same four ranges the same file already adds to hosts.allow (10/8, 172.16/12, 192.168/16)
    plus 169.254/16 for avahi's no-lease fallback: announcing to a phone on the LAN is the point, being a public
    responder is not;
  • inserted at position 1, i.e. ahead of udp_inbound and of the broadcast drop, which is the whole reason a port
    rule for UDP has to live in this file;
  • -C first, so a re-run is a no-op, same shape as the two port rules already here;
  • in zz-onboard-ports and not 18onboard-mdns: the name is the mechanism, rc.firewall sorts under "r" and
    would wipe a rule set by a 18- service.
  • IPv6 is deliberately not opened (shortcut: comment in the file): this image's IPv6 is unconfigured and the board
    reaches it at 192.168.50.110, so a v6 rule would be untested code.

vm-check.sh gains U3 mdns rule, which fails an image without the rule (the same reasoning as the existing
U3 port rule row) and prints the responder's own last log line.

Measured (booted c22 image, ~/wt/os3i/c23-mdns-probe6.sh, one controlled announcement burst per arm)

The counters are the rules' own, so this is the packet path talking, not a grep:

arm policy DROP the fix's INPUT rule
shipped firewall 5 → 12 packets (+7 / 2095 B) absent
+ one -I INPUT 1 -p udp --dport 5353 -s 10.0.0.0/8 -j ACCEPT 12 / 2868 B (unchanged) 0 → 7 packets / 2095 bytes
rule removed again 12 → 19 (+7 / 4963 B) absent

The seven packets dropped in arm 1 are the seven the rule accepts in arm 2, byte for byte, and arm 3 puts them back
on the drop path. udp_inbound counted 16 packets in arm 1, so the burst really did traverse the chain.

Two limits, stated rather than implied:

  1. Those packets are avahi's own multicast announcements looped back (IP_MULTICAST_LOOP), not a query from a
    peer. Same packet class (udp → dport 5353, inbound, real interface), same chain, and the rule is blind to who
    sent it — but "a peer on the LAN resolves 1bit-os.local and gets :13305 200" is the board row and stays
    SKIP U4 board in the harness.
  2. QEMU user networking cannot carry inbound UDP at all: a hostfwd=udp:…-:5353 delivers nothing to a guest
    listener even with iptables -F; -P INPUT ACCEPT (policy counter 0 packets). A veth pair does not help either —
    both ends are one host's addresses, so the route is local via lo — and this image's ip is BusyBox, which has
    no netns subcommand. All three dead ends are recorded in the probe scripts so the next lane does not pay for
    them again.

Closes #1011.

… announced but never answers

The c22 board (4 Nov 12:5x AST) logged `avahi-daemon up: 1bit-os.local is announced on this network` and a raw
mDNS query from ryzen got no answer. #952/#953 put the responder in the image; what was never opened is the path
for a *query*, and the cause is the same `rc.firewall` that `zz-onboard-ports` already works around for tcp.

Read out of the packed c22 puppy SFS, `etc/init.d/rc.firewall`:

    -P INPUT DROP
    -A INPUT -p ALL -d 224.0.0.1 -j DROP
    -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
    -A OUTPUT -p ALL -j ACCEPT

so every NEW inbound UDP packet falls through to the DROP policy, and a query on 5353 is exactly that; the
appliance's own announcements leave fine, which is why avahi reported success while nothing answered. This file
already opens tcp/13305 and tcp/22 for the same reason - mDNS is the UDP half. LAN-only, the same ranges
hosts.allow already names below plus link-local for avahi's no-lease fallback, ahead of udp_inbound and the
broadcast drop, idempotent via -C, and in `zz-onboard-ports` rather than `18onboard-mdns`, whose name sorts
before `rc.firewall` and would be wiped by it.

Measured in a booted c22 image (`~/wt/os3i/c23-mdns-probe6.sh`, one controlled announcement burst per arm, the
rules' own packet counters):

  arm                                   policy DROP        the fix's INPUT rule
  shipped firewall                      5 -> 12 (+7/2095)  absent
  + one `-I INPUT 1 ... dport 5353`     12 (unchanged)     0 -> 7 packets / 2095 bytes
  rule removed again                    12 -> 19 (+7/4963) absent

the same seven packets that were dropped are the ones the rule accepts, byte for byte. Two caveats stated rather
than implied: those packets are avahi's own multicast announcements looped back (IP_MULTICAST_LOOP), not a query
from a peer - same packet class, same chain, and a peer's query is what the board row must still prove; and QEMU
user networking cannot carry inbound UDP at all (a `hostfwd=udp` delivers nothing even with the firewall
flushed), which is why this is measured on the appliance's own interface.

Gate: a `U3 mdns rule` row in vm-check.sh that fails an image without the rule.

closes #1011
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit b27ba43)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

952 - Partially compliant

Compliant requirements:

  • The image now includes the mDNS responder and client name resolution components.
  • The boot service 18onboard-mdns starts the D-Bus system bus and avahi-daemon.
  • The nsswitch.conf is updated to include mdns4_minimal.
  • The image refuses to be published without both components.
  • The vm-check now asserts that the name resolves on the box.

Non-compliant requirements:

  • None

Requires further human verification:

  • End-to-end verification that a peer on the LAN resolves 1bit-os.local and gets :13305 200 (requires physical hardware).

953 - Partially compliant

Compliant requirements:

  • The appliance now announces 1bit-os.local.
  • The blank-key check no longer asks for a key.
  • The appliance correctly sends no header in blank mode.
  • The appliance correctly sends the real key in keyed mode.
  • The release-check.sh now correctly asserts that the check passes.

Non-compliant requirements:

  • None

Requires further human verification:

  • None

1011 - Partially compliant

Compliant requirements:

  • The firewall no longer drops mDNS queries.
  • The appliance accepts mDNS queries on UDP port 5353.
  • The fix is implemented in zz-onboard-ports to open UDP port 5353 for LAN ranges.
  • The U3 mdns rule row is added to vm-check.sh to verify the rule exists.

Non-compliant requirements:

  • None

Requires further human verification:

  • End-to-end verification that a peer on the LAN resolves 1bit-os.local and gets :13305 200 (requires physical hardware).
⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Firewall Rule Insertion

The script adds iptables rules to allow mDNS queries on UDP port 5353 from specific LAN ranges. It uses -I INPUT 1 to insert the rules at the beginning of the INPUT chain, ensuring they are processed before other rules that might drop the packets. This is a correct approach to ensure that mDNS queries are not dropped by the firewall.

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
Self-test Script

The self-test script mdns-ports-selftest.sh verifies that the zz-onboard-ports script correctly inserts the necessary iptables rules for mDNS. It uses a stub iptables command to simulate rule insertion and checks that the rules are inserted correctly and idempotently. This ensures that the script behaves as expected.

cat > "$work/bin/iptables" <<'STUB'
#!/bin/sh
# the rule's identity is "<chain> <spec...>", so `-I INPUT 1 <spec>` and `-C INPUT <spec>` 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)"

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY

The diagnosis is right and I reproduced both halves of it in a private network namespace — no box firewall touched,
unshare -rn with lo up, rc.firewall's UDP shape rebuilt from the diff's own listing:

  before the rule:   listener TIMED OUT (no query arrived)      <- the c22 board symptom, reproduced
  after  iptables -I INPUT 1 -p udp --dport 5353 -s <src> -j ACCEPT:
                     listener RECEIVED 10 bytes from 127.0.0.1
  chain afterwards:  -P INPUT DROP
                     -A INPUT -s <src>/… -p udp -m udp --dport 5353 -j ACCEPT
                     -A INPUT -d 224.0.0.1/32 -j DROP
                     -A INPUT -p udp -j udp_inbound

So "announced but never answers" is exactly what -P INPUT DROP + udp_inbound's -j RETURN does to a query, and
-I INPUT 1 is the position that fixes it (ahead of the 224.0.0.1 drop and of udp_inbound). The substitute I made
is the source: 127.0.0.1/32 for one of the four LAN ranges, since a loopback source is what a single netns can produce;
the match semantics are the same and the ranges themselves are declared and -C-tested by the new row.

Idempotency, which is the other thing a rule-inserting init script can get wrong — ran the function three times in
the same namespace:

run1 rc=0 rules=4   run2 rc=0 rules=4   run3 rc=0 rules=4

one rule per range, stable, because -C short-circuits before -I.

bash -n clean on both files and shellcheck reports 0 findings on zz-onboard-ports (8 lines of open_mdns included).

Three notes, none a gate:

  1. The broadcast drop is not what was biting, and the test says so. My query was unicast, so the path proven is
    udp_inbound -> DROP policy; -m pkttype --pkt-type broadcast -j DROP never matched (mDNS is multicast, not
    broadcast). Worth knowing if someone later "simplifies" the ordering — the -I INPUT 1 is doing the work for the
    common case, and the 224.0.0.1 DROP would only matter for a query sent to the all-hosts address, which mDNS does not.
  2. open_mdns's return value is ignored at the call site, exactly like the two open_port calls above it — so a failed
    insert prints to stderr and the script still finishes 0. Consistent with the file, but it means a boot with no
    iptables would be quiet in the exit status. The new U3 mdns rule row (asserting udp-5353-open) is what actually
    catches it, which is the right place for it.
  3. IPv4-only is declared as a shortcut: with the condition for upgrading (a global v6 address), which is the form I
    want to see. No mDNS over v6 on this image today, so nothing is lost.

Not run by me: the guest rows themselves (U3 mdns rule, and the responder's own log line) — they need the VM or the
c22 stick, and I mount nothing on this box.

@bong-water-water-bong bong-water-water-bong added the tprs-ready TPRS: review verdict READY posted by the standing reviewer; merge gate satisfied label Nov 4, 2026
…nnot regress silently

`mdns-ports-selftest.sh` runs the shipped `zz-onboard-ports` against a stub `iptables` (a rule table in a file, `-C`
answered from it): no root, no ruleset, no device. It asserts four `udp/5353` inserts with the four intended
sources, that a second run inserts nothing, and that the two TCP ports this file already opened are still opened.

Measured both ways before committing:

    shipped script    run 1: 6 inserts, 4 of them udp/5353   run 2: 0 inserts, 4 -C   -> RESULT: PASS
    origin/main's     run 1: 2 inserts, 0 of them udp/5353                            -> RESULT: FAIL

`sh -n`, `shellcheck -S warning` and `tools/copyright.py --check` are clean.
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit 9c0b734

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY (re-verdicted at 9c0b73487)

The push adds os/v3/puppy/mdns-ports-selftest.sh (+78), which is the right answer to "this is the kind of code that
fails silently". I ran it, and then ran it against the pre-fix script as a control:

$ sh os/v3/puppy/mdns-ports-selftest.sh  os/v3/puppy/embed/etc/init.d/zz-onboard-ports
run 1: 6 insert(s), 4 of them udp/5353, 8 udp/5353 command(s)
run 2 (same ruleset): 0 insert(s), 4 udp/5353 command(s) - all -C, none -I
RESULT: PASS (four udp/5353 rules inserted once, the re-run inserts nothing, sources as intended)

$ sh os/v3/puppy/mdns-ports-selftest.sh  <the same file at origin/main>
RESULT: FAIL - first-run-inserts=2(expected 6: two TCP ports, four udp/5353) udp5353-inserts=0(expected 4)
        re-run-inserted-udp5353=0(not idempotent) … missing-rule-10.0.0.0/8 missing-rule-172.16.0.0/12
        missing-rule-192.168.0.0/16 missing-rule-169.254.0.0/16

So the check discriminates in both directions and it also pins that the two pre-existing TCP ports are still opened —
which is the regression a wrapper like this most plausibly causes. sh -n clean on both files.

This stands alongside the netns result from my earlier review (query dropped before the rule, delivered after it, rule at
the top of INPUT), and it does not need root, a device or a real ruleset.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

#1016 ready for review at 9c0b734

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at d0b5f7194 — merge-only update; the delta is #1007 arriving from main.

git diff 9c0b7348 d0b5f7194 -- os/ is os/v3/puppy/g3-probe.sh | 5 +++++-, i.e. main's -1bit release-spelling fix
and nothing of this PR's own three files. So the evidence carries: the netns result (query dropped before the rule,
delivered after, rule at the top of INPUT), the rule's idempotency (4 rules after three runs), and the new
mdns-ports-selftest.sh (PASS at the head, RESULT: FAIL against the pre-fix script).

Note for the merge sweep: #1020 is stacked on this PR (merge-base --is-ancestor on this head -> yes), so #1016 goes
first and #1020 second, or #1020 needs a rebase.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit b9991d8

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit b27ba43

@bong-water-water-bong
bong-water-water-bong merged commit 3002cea into main Nov 4, 2026
9 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the os/v3-c23-mdns branch November 4, 2026 18:30
bong-water-water-bong pushed a commit that referenced this pull request Nov 4, 2026
…t - rc.firewall drops inbound mDNS

Cycle 23 (#1016) opened udp/5353 in zz-onboard-ports for the LAN ranges and
added the `U3 mdns rule` gate row. Section 1 said only the responder and the
LAN could leave you reading an address, which reads as "works on a c22 stick"
when the measured c22 board shows the opposite. Stated what was measured, with
the fix's citations, and kept the one hop nobody has proved: the packets the
rule accepts were avahi's own loopback announcements, not a peer's query.
Also re-points the blank-mode U3 citation (:740-754 -> :749-762) after #1016.
bong-water-water-bong added a commit that referenced this pull request Nov 6, 2026
…rget (#859)

* docs: 1bit OS is the Puppy Linux appliance, and os/mini is a CI/VM target

1bit OS is Puppy Linux (ResolutePup64, woof-CE on the Ubuntu 26.04 resolute
base) with Lemonade Server and the engine embedded, our MOK-signed 7.3
perfopt kernel, headless by default with a 1bit OS (desktop) boot entry,
bash/sudo/apt, data and settings on 1BIT-DATA, Secure Boot on. The BusyBox
os/mini image is the CI/VM target and is no longer called 1bit OS anywhere.
No release is claimed: the next release carries the appliance.

Files: README.md (get-started steps + a milestone row), os/README.md (the 1bit
OS page is the appliance; os/mini, os/v2 and os/v3/puppy laid out), os/v2/README.md
(title and 'ships today' line), os/LICENSES.md (which image carries what),
docs/releases.md (top note, artifact row, known issues, os/mini build note),
docs/os-v3.md (Puppy is v3, with the board result; the Ubuntu plan is the fallback),
docs/os-v3-live-puppy-eval.md (dated update: the four blockers are closed and it
booted on the board), docs/kernel.md (the kernel reaches 1bit OS as woof-CE's
kernel package), docs/PORTING.md (same, one clause).

* docs: install 1bit OS (stick, disk, USB) and connect a client to Lemonade

docs/install.md covers the options the owner asked for, each with who it is for, the steps and what is
erased: run from the stick, install to an internal disk or another USB drive with 1bit-install (the
seven steps, the refusals, the firmware entry), keep the existing OS (no dual-boot wizard in this
release), the engine on an existing Linux (advanced, no PPA route yet), Secure Boot and the MOK, and the
Bosgame front-port note (#823). docs/clients.md covers the address and the key, curl, the web app,
Lemonade Mobile on iPhone and Android, the OpenAI SDKs and any OpenAI-compatible app, the lemonade CLI
from another machine, and the security/rotation rules.

Every example was run against the cycle-13 appliance image booted in QEMU with Secure Boot on
(~/wt/rlk-vm/verify.sh, results.txt): the key is enforced (401/401/200, /api/v1 and /v1), the web app is
served at :13305, the curl and OpenAI-SDK chat examples returned 200 with content, the CLI flags and the
LAN beacon work, and the firewall opens only 13305 and 22. The install ran end to end on a bare disk.

Three defects it found are filed as #865 (an installed disk does not boot: init resolves pupsfs=<label>
but not pupsfs=UUID=...), #866 (step 7's BootNext lands on the BootManagerMenuApp) and #867 (a dd-written
stick never gets an API key minted, so Lemonade serves the LAN unauthenticated); the pages state each
where it matters, with the measured label A/B that isolates #865.

README: step 4 of Get started points at both pages.

* docs/install: no unmeasured install duration

* docs: the 1bit OS image ships from Hugging Face, not a GitHub release asset

The Puppy image is 9,128,902,656 B written / 2,212,389,580 B as 1bit-os-puppy.img.xz, and a GitHub
release asset is capped at 2 GiB, so the image lives at
https://huggingface.co/1bit-MONSTER/1bit-OS with its SHA256SUMS. README, docs/install.md and
docs/releases.md now say so and give the same two write lines everywhere
(sha256sum -c SHA256SUMS --ignore-missing, then xz -dc ... | dd; balenaEtcher and Rufus DD mode take
the .img.xz directly). The GitHub releases page keeps the lemonade-onebit package, the release notes
and the release SHA256SUMS, and links to the image.

History rows are untouched: the c18-c20 candidates really did ship the BusyBox os/mini image as
1bit-os-<tag>.img.zst, and those sizes and shas stay as recorded.

* docs: the Hugging Face image repository is private until v2026.44 is published

Every place a reader acts on the download (README quickstart, the install page's image
row and write block, the release table's image row) now says the repository is private
until v2026.44 is published, so the link is the download from publication day and an
earlier release carries only the BusyBox os/mini image, the CI/VM target.

Addresses the CHANGES on #859: the URLs are correct and the upload landed, but they
answer 401 while the repository is private.

* docs: the appliance boots as 1bit-os, and installer #866 is fixed

Two claims in this branch were true of the cycle-13 image and are not true of
the image that ships next:

- docs/clients.md said the hostname is Puppy's per-machine puppypc<MAC>; the
  image sets /etc/hostname to 1bit-os (os/v3/puppy/embed/etc/hostname, and the
  c16 board boot read back 1bit-os). Keep the cycle-13 observation as history
  and tell the reader the name, not a guessed .local.
- docs/install.md said installer step 7 and the pupsfs=UUID boot are both open.
  #866 is fixed on main by #889 (merged 2026-11-03); #865's fix is #890, open.

* docs/clients: say puppypc<digits>, not <MAC digits> - the MAC derivation is unverified

* docs/install: both installed-disk fixes are in main, and a dd-only stick does get a key now

Three claims went stale since this branch opened, all measured against origin/main
today rather than against the cycle-13 artifact:

- the installed-disk blockquote said "until both are in an image". #890 (#865's
  init fix) merged 2026-11-03T12:45Z on top of #889 (#866), and its own probe
  boots a disk installed by `1bit-install` as the only disk in QEMU; an image
  built from main boots the installed disk as written.
- the measurement table's #865 row still said "fix open as #890".
- the dd-only stick paragraph said a stick with no key serves every endpoint
  unauthenticated and that #867 is an open defect of this image. On main it is
  not: 05onboard-data no longer pre-creates an empty lemond.conf and 10lemonade
  mints on "is there a key" rather than "is there a file" (#828). The cycle-13
  measured behaviour is kept as history, with the fix pointer and how to read
  the minted key (console 1 -> Show API key).

* docs: both MOKs are needed, the real menu order, the save layer, #860 not in tree, the shipped artifact name

Six Copilot review findings, each checked against the sources rather than the prose:

- README and install.md §6 said Secure Boot "can stay on" with "shim, grub, the kernel and its modules
  all signed with our MOK". The shim is Microsoft-signed (os/v3/puppy/mkimage.sh:75-80 pulls
  shimx64.efi.signed), and on a machine that has never trusted our key the firmware refuses the kernel
  and the certificates are not on the stick — so a fresh machine has to enroll first or turn Secure Boot
  off. Both places now say that and link to the enrollment steps.
- install.md step 1 named the third menu entry as the serial shell; the recipe orders headless, desktop,
  install-to-a-disk, serial shell (os/v3/puppy/mkimage.sh:152-166), so the documented ordinal opened the
  installer. All four are now named with their flags.
- the RAM-only paragraph contradicted the PUPMODE=12 save layer above it. It now separates the two
  measured cases: no save folder + `poweroff -f` (marker gone, the cycle-13 observation) versus a save
  folder + normal shutdown, where G7 measured an /etc marker surviving
  (os/v3/puppy/g7-probe.sh:28-33,93-99), with 05onboard-data's shutdown sync/unmount named.
- install.md advertised `scripts/mk-plugin-package.sh` as an existing route; it is on PR #860, not in
  this tree. Marked as such.
- the enrollment row imported only `MOK-Kernel.der`; kernel.md's table has a second key, `MOK.der`, for
  every module we build, so a machine that enrolled only the kernel key still refuses the NPU and `ec`
  modules. The row now imports both, and §6 states the split.
- releases.md's artifact row listed `1bit-os-<tag>.efi` as the appliance file; the Puppy recipe builds a
  disk image (`1bit-os-puppy.img.xz` + SHA256SUMS) and there is no standalone `.efi` for it. The `.efi`
  and `.img.zst` names stay with the earlier BusyBox os/mini candidates, which the same page's
  gated-candidate table already lists. The Hugging Face privacy clause is unchanged.

copyright.py --check rc 0; site builds 53 pages, one <h1> per touched page.

* docs: the console is a menu that prints how to connect, not a login prompt

The cycle-18 image puts a CONNECT block (address, web UI, API, ssh) above the
console menu, and 1bit-console replaces plogin on a headless boot, so README's
"console login" and the bare "a console with no X" no longer describe what
comes up. Also names the desktop entry by its grub.cfg label,
`1bit OS (desktop, JWM)` (os/v3/puppy/mkimage.sh:156).

* docs: the appliance ships with no API key (cycle 20), and the console's item 1 is the toggle

#934 made blank the shipped mode: 10lemonade starts with LEMONADE_API_KEY unset and
binds 0.0.0.0:13305 anyway, so every request works with no header until a person turns
a key on from console item 1 (which mints 48 hex into /etc/1bit/lemond.conf, 0600, on
/data, restarts the service and prints it), or prepare-stick --mint writes one offline.

install.md and clients.md still described the #867/#828 behaviour - 'the appliance mints
one on its first boot', 'read the key with show API key', 'every request needs the API
key', '401 without the key', 'Lemonade Mobile: on 1bit OS it is required'. All of those
are now false of the shipped default and wrong in the unsafe direction (they imply the
box is closed when it is open). Rewritten against the sources, with file:line:
10lemonade:43-62,78-81; 1bit-console-menu:238,242,300-370; console-tui/src/ui.rs:203-215,
280-292; prepare-stick.sh:346-410; vm-check.sh:710-730; 1bit-webapp-proxy:37-44.
The cycle-13 measured rows stay as history, marked as that image's behaviour, and the
os/mini media difference (it still mints) is stated per docs/os-v3.md.

* docs/clients: a non-1bit-os hostname is a person's rename, not a fault (cycle-16 u2-hostname)

The cycle-16 board booted as `strix` while the frozen image bytes still read
`1bit-os`: a person renamed it at the console, and the release lane's
`u2-hostname` red was that reading, not an image defect. Section 1 told a
reader `hostname` prints `1bit-os` with no way to tell a rename from the
`puppypc<digits>` regression it is contrasted with, so a reader on a renamed
box would go looking for a fault. Name the console path
(3 Network -> 3 Set the hostname, `1bit-console-menu:437,463-466,632`) and
say to check the address instead of the name.

* docs/os-v3: where v3 stands, as of cycle 20 — 33-row board runs and a blank key by default

The standing paragraph was written on 2026-11-02 and had gone wrong in two ways
in the same breath. It quoted '20 of puppy-check's 25 rows PASS' as the
appliance's current state (the checker has 33 rows now; cycle 19 came back 33
rows, 0 failed, 1 skipped, and cycle 20's two reds were the acceptance script's
own defect, engine#945, fix open as #951), and it said 'Lemonade answers on
:13305 with its key' — false and unsafe since engine#934 made blank mode the
shipped default (10lemonade:43-62 binds 0.0.0.0:13305 with LEMONADE_API_KEY
unset; console item 1 turns a key on).

Restate it against the c19/c20 board verdicts and the #934 sources, and add one
sentence to the eval page's dated 2026-11-02 update so the 'with its key'
reading there is marked as that cycle's behaviour rather than today's.

* docs/os-v3: #945's fix landed as #951, so the sentence stops calling it open

* docs/clients: the appliance announces 1bit-os.local from cycle 21 on

The page told a reader 'mDNS is not claimed' and to read the address off the box. Cycle 21
item 10 (#953, merged b8a07df) ships the responder, asserts it at build time and prints it in
the console's CONNECT block, so that sentence is now false in the direction that costs a
reader a step. Said what the sources say, with the two limits kept: the announced name is
$(hostname).local (so it follows a rename), the CONNECT row says when avahi is not running,
and a peer resolving it over multicast is still not claimed on this page.

* docs/clients: two citations point at the lines that carry the claim

container-build.sh gained 12 lines in #976, so the mdns pack-checks it cites
moved from 496/505 to 508/517; and the webapp proxy's foreign-Origin refusal
is at 45-49 (the rule and ALLOWED_ORIGINS) and 74-77 (the code), not 37-44
- which is the config block and the sentence that introduces the key
attachment. Both numbers read back out of the merged tree this pass, not
carried over.

* docs/clients: the 8080 chat page is not served on the c19/c20 boards

Both boards log `OSError: [Errno 98] Address already in use` for 127.0.0.1:8080
(c19 finding, unchanged on c20, and #959's own body says so), so the proxy's
chat page is not reachable there while Lemonade's own page on :13305 is. A
reader sent to :8080 chases a dead port; the sentence names it and points at
:13305, with the init script's own two lines cited.

* docs: re-point two citations at the lines that now hold them (CMakeLists 322, serve.cpp 202-213, ui.rs 218-222)

* docs/os-v3-live-puppy-eval: a bare `</dev/null` code span truncated the rendered page at that table row

python-markdown reads the angle-bracket close tag as raw HTML and drops
everything after it: 46,922 B of markdown rendered to 21,897 B, cutting the
page mid-table in front of the whole cycle-13/19/20 acceptance evidence.
Reworded to plain text (same meaning); the page now renders 61,087 B with the
cycle-13 rows, and the site build is 56 pages, rc 0.

* docs/clients: re-point the two vm-check.sh citations after #1001/#1007 moved them (616-621, 740-754)

* docs/clients: the c22 board announced the name and nothing answered it - rc.firewall drops inbound mDNS

Cycle 23 (#1016) opened udp/5353 in zz-onboard-ports for the LAN ranges and
added the `U3 mdns rule` gate row. Section 1 said only the responder and the
LAN could leave you reading an address, which reads as "works on a c22 stick"
when the measured c22 board shows the opposite. Stated what was measured, with
the fix's citations, and kept the one hop nobody has proved: the packets the
rule accepts were avahi's own loopback announcements, not a peer's query.
Also re-points the blank-mode U3 citation (:740-754 -> :749-762) after #1016.

* docs: the image's kernel is 7.4.0-rc1-1bit since cycle 21, and the cycle-23 board is green

os-v3.md said the appliance ships `7.3.0-rc4-1bit-perfopt`. Measured in the
frozen images rather than taken from a note: the c20 image carries
`7.3.0-rc4-1bit-perfopt+`, and the c21, c22 and c23 images carry
`7.4.0-rc1-1bit` (string present in each), so the page now says which cycle
moved it. The same paragraph's newest board was cycle 20; cycle 23 (4 Nov)
is in, with its row counts and the two facts the c23 verdict records - the
single red is a bug in the check's own u7-mdns row, not the image.

clients.md: three citations re-pointed at the lines that now carry them
after #1020 moved vm-check.sh and container-build.sh by 13 and 1 lines.

* docs/clients: re-point the two vm-check.sh citations #989 moved again

#989 added the U4 kernel rows above them, so the U3 mdns rule row is at
711-714 (was 689-692) and the blank/keyed U3 network block at 784-797
(was 762-775). The citations now also name the row, because this file has
moved under them three times tonight.

* docs/os-v3-live-puppy-eval: CMakeLists.txt:322 -> 331-332, moved by #1021

#1021 added nine lines to CMakeLists.txt above the HRX block; 331-332 is
still target_compile_definitions(... ONEBIT_HRX_LIBHSA=...).

* docs/clients: the mDNS fix now states the outbound leg too, and two citations #1025 moved

#1025 (c24 prep) adds the outbound 5353 rule to open_mdns - intent, not a
fix: the packed c23 image accepts all output, and the c23 board's "out:
NONE" was the row demanding a rule the image correctly lacks. The page now
says which half each cycle opened and why the second one exists, and the
gate has a row per half. Citations: open_mdns 55-66 -> 56-74, the
blank/keyed U3 block 784-797 -> 789-802.

* docs/clients: re-point the three vm-check.sh citations after the file grew ~190 lines

The gate script gained rows since these numbers were written, so the three
citations in clients.md pointed at unrelated rows (616-621 landed in the
service-up/U3-key rows, 711-714 in the driver-signature row, 789-802 in the
engine-sha row). Re-read them against the merged tree: U3 mdns rule 849-852,
U3 mdns out 884-886, U3 auth (blank vs keyed) 670-676, getent hosts 722-723.

* docs/clients: the CONNECT name rows are ui.rs:236-238, not 213-216

Caught by re-reading every citation in the page with sed instead of trusting
the string: 213-216 is the 'web UI: waiting for network' line. The name/mDNS
rows the paragraph quotes are at 236-238 on main (#1041/#1059 moved them).

* docs: the 1bit OS kernel is 7.4.0-rc1-1bit, and the boot entry label says so

The appliance images from cycle 22 on carry 7.4.0-rc1-1bit (c22/c24/c26
hand-offs, and the c24 release verdict), but four current-state lines in this
PR still named 7.3.0-rc4-1bit-perfopt: README's 1bit OS paragraph,
os/README.md's kernel row, and install.md's boot step plus its Secure Boot
chain. install.md's boot step is the sharpest: mkimage.sh builds the menu
title from the vmlinuz version (`menuentry "1bit OS (headless console,
${kver})"`), so the label a reader sees is the kernel's own string.

The dated cycle-13 measurement rows keep 7.3.0-rc4-1bit-perfopt+ - they say
what that image carried, and install.md:277 and clients.md:254 say so.

Also os/README.md names the second boot entry as mkimage.sh words it,
`1bit OS (desktop, JWM)`.

* docs/clients, docs/install: four citations point at the rows the prose names

Re-read every file:line in both pages out of origin/main (fadcc1e) instead of
trusting the earlier pass - the pattern that has failed in this lane before.
Three had rotted under the cycle-25 console/logging work:

- clients.md:78-81 -> 104-108 for "blank mode is LAN-reachable on purpose";
  78-81 is now the cycle-25 log_level comment, the LAN comment moved down.
- clients.md:632 -> 645 for the console's top-level Network row; 632 is the
  closing brace of the CLI dispatcher.
- install.md:218-222 -> 240-243 for the TUI's api row; 218-222 is the "web
  UI: waiting for network" block, the api/keyed rows are 240-244.

And one was under-cited: clients.md:88 cited 10lemonade:43-62 for the
0.0.0.0:13305 bind, which is line 107-108. Now 43-62,107-108.

Checked mechanically: /tmp citcheck over both pages, every ref printed with its
first line, all twelve in clients.md and eight in install.md name what the
sentence says.

* docs/os-v3-live-puppy-eval: the update box's numbers are cycle 12's, and one citation went stale

Two claims on this page had drifted, both found by checking them against today's tree rather than
re-reading the prose:

1. The update box led with '20 of `puppy-check`'s 25 rows PASS' as if it were the appliance's
   acceptance state. That was cycle 12, the first hardware boot; cycle 19 came back 33 rows / 0 failed
   and cycle 23 41 rows / 40 PASS / 1 FAIL (the FAIL being the checker's own `u7-mdns` row). Within
   this same PR, docs/os-v3.md already carries those. The numbers stay, labelled as cycle 12's - they
   are what decided the plan - and the page now points at os-v3.md as the page of record instead of
   growing a second, diverging copy.
2. `ONEBIT_HRX_LIBHSA` is a CMake compile definition, cited at `CMakeLists.txt:331-332`; those lines are
   the npu_sources block now. It is at `CMakeLists.txt:356` (verified against origin/main).

The other nine file:line citations in this branch were checked the same way and are correct
(ui.rs:236-238 and 240-243, container-build.sh:508 and 518-522, vm-check.sh:670-676 / 722-723 /
849-852, prepare-stick.sh:346-410, g7-probe.sh:28-33, app/serve.cpp:202-213).

* docs: the .img.xz size is approximate, because the exact bytes change with every cut

pi-prs flagged 2,212,389,580 B as the one figure on the download pages a
reader can falsify with `ls -l`: the only .xz on this box is
~/wt/hf-1bitos/1bit-os-puppy.img.xz at 2,212,388,768 B (3 Nov, an earlier
cut), and the Hugging Face repository is private, so the published file
cannot be read to check. Every re-cut changes it (c26 and c27 are both
9,128,902,656 B written but their images differ), so an exact compressed
byte count is a number this page cannot keep true.

Dropped it in favour of "about 2.2 GB compressed" (README, install.md,
releases.md x2) and kept the figure that is stable across cuts, the
9,128,902,656 B written, which install.md:41 now pairs with the rule that
matters: the size to check is the one `ls -l` prints after the download.
No sha or exact size now depends on a cut this page cannot see.

---------

Co-authored-by: pi-rlk <pi-rlk@1bit.gg>
Co-authored-by: agent <agent@local>
Co-authored-by: pi-rlk <rlk@1bit.local>
Co-authored-by: bong-water-water-bong <bong-water-water-bong@users.noreply.github.com>
Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
Co-authored-by: pi-os3 <pi-os3@1bit.local>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Review effort 4/5 tprs-ready TPRS: review verdict READY posted by the standing reviewer; merge gate satisfied

Projects

None yet

Development

Successfully merging this pull request may close these issues.

os/v3/puppy: the firewall drops every mDNS query, so 1bit-os.local is announced but never answers

1 participant