From 6899fc36266792515227e55c835a1d5c5d85539e Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Mon, 13 Jul 2026 23:32:22 +0000 Subject: [PATCH] =?UTF-8?q?fix(drill):=20the=20NIC=20inside=20a=20VM=20is?= =?UTF-8?q?=20enp5s0,=20not=20eth0=20=E2=80=94=20read=20by=20subnet=20inst?= =?UTF-8?q?ead?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A3, the one probe the whole audit exists for, has never fired in six runs. It was never the network: the profile names the DEVICE eth0, but inside a VM guest predictable naming renames it enp5s0, so every address lookup — first the '(eth0)' CSV match, then 'ip addr show dev eth0' — was hunting an interface that does not exist. I fixed that symptom twice without ever questioning the assumption underneath it. Read the address from inside the box and select by SUBNET (10.87.x, what claudenet hands out) rather than by interface name. docker0's 172.17.x is the decoy; the NIC's name is the guest's business, not ours. A3 also gains a guard it should have had from the start: if the peer and the source hold the SAME address, refuse to probe. That is not hypothetical — clones were inheriting their source's machine-id, hence its DHCP lease, hence its address, so 'archive → peer' was archive probing itself and would have reported a cheerful 'reachable' as an isolation failure. Co-Authored-By: Claude Fable 5 --- drill/RUNS.md | 17 ++++++++++++----- drill/drill.sh | 43 +++++++++++++++++++++++++++---------------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/drill/RUNS.md b/drill/RUNS.md index 06afbb0..ed22e92 100644 --- a/drill/RUNS.md +++ b/drill/RUNS.md @@ -54,11 +54,18 @@ Read this before adding a probe. Every one of these cost a run. and it is why the drill now runs **no listener anywhere**. It does not need one: `curl` exit `7` (refused) means the packet *arrived*, `28` (timeout) means it was *dropped*. A closed port answers the question. -4. **`incus list` name filters are not regexes.** `incus list "^peer$"` matches - nothing and returns empty — silently. This is how A3 went unprobed for - three runs. Read addresses from inside the box (`ip -4 -o addr show dev - eth0`), not out of `incus list` CSV (which also quotes multi-address boxes - across lines). +4. **The box's address is hard to read, and every way of getting it wrong was + tried.** (a) `incus list` name filters are **not regexes** — `incus list + "^peer$"` silently matches nothing. (b) Its CSV quotes a multi-address box + across lines. (c) **The interface is not `eth0`.** The *profile* names the + device `eth0`, but inside a **VM guest** predictable naming renames it + **`enp5s0`** — so `ip addr show dev eth0` finds nothing either. That is the + real reason A3 went unprobed for six runs, through two "fixes" of mine that + never questioned the interface name. Read it from inside the box and select + by **subnet** (`10.87.x`), not by interface name: docker0 (`172.17.x`) is + the decoy, and the NIC's name is the guest's business. + *Lesson: when the same probe fails three different ways, stop patching the + probe and go look at the thing itself.* 5. **`incus delete -f a b c` aborts at the first MISSING name.** One interrupted run then poisons the next: stale boxes survive cleanup and cascade into half a dozen unrelated FAILs. Delete one name at a time. diff --git a/drill/drill.sh b/drill/drill.sh index 8514ed3..b2129cd 100755 --- a/drill/drill.sh +++ b/drill/drill.sh @@ -86,17 +86,20 @@ in_box() { return "$rc" } -eth0_ip() { # the box's address on claudenet — eth0 exactly; a box running - # docker has several addresses, so never just "the first IP". - # ('incus list' name filters are NOT regexes — the anchored - # "^b$" form matched nothing, which is how A3 went unprobed for - # three runs — and its CSV quotes multi-address boxes across - # lines. Reading 'ip -o' inside the box is unambiguous.) - # Retries: the agent answers before DHCP hands out the address. +# The box's address ON CLAUDENET. Three ways to get this wrong, all of them hit: +# · 'incus list' name filters are NOT regexes ("^b$" silently matches nothing) +# · its CSV quotes a multi-address box across lines +# · and the interface is NOT called eth0. The PROFILE names the device eth0, +# but inside a VM guest predictable naming renames it enp5s0. Six runs of +# A3 "not probed" were this, not the network. +# So: read it from inside the box, and select by SUBNET (10.87.x, what claudenet +# hands out) rather than by interface name — docker0 (172.17.x) is the decoy, +# and the NIC's name is the guest's business, not ours. +claudenet_ip() { local b="$1" ip _i for _i in $(seq 1 15); do - ip="$(in_box "$b" ip -4 -o addr show dev eth0 \ - | awk '{ for (i = 1; i < NF; i++) if ($i == "inet") { split($(i+1), a, "/"); print a[1]; exit } }')" + ip="$(in_box "$b" ip -4 -o addr show scope global \ + | awk '{ for (i = 1; i < NF; i++) if ($i == "inet" && $(i+1) ~ /^10\.87\./) { split($(i+1), a, "/"); print a[1]; exit } }')" [ -n "$ip" ] && { printf '%s\n' "$ip"; return 0; } sleep 2 done @@ -457,9 +460,17 @@ esac # port answers the question just as well (refused = the packet arrived), and # the listener was what kept wedging the run. Ping corroborates: if the two # disagree, say so rather than pick one. -PEER_IP="$(eth0_ip peer)" -if [ -n "$PEER_IP" ]; then - inf "probing archive → peer ($PEER_IP), no listener: refused means it arrived, timeout means it was dropped" +PEER_IP="$(claudenet_ip peer)" +ARCH_IP_PRE="$(claudenet_ip archive)" +if [ -n "$PEER_IP" ] && [ "$PEER_IP" = "$ARCH_IP_PRE" ]; then + # Guard, because this actually happened: a clone inherited its source's + # machine-id, hence its DHCP lease, hence its ADDRESS. Probing "archive → + # peer" was archive probing itself, and would have reported a cheerful + # "reachable" as a sibling-isolation failure. Never let A3 answer this. + no "archive and peer hold the SAME address ($PEER_IP) — the clone did not get its own identity; A3 cannot be probed" + aud "A3 sibling: NOT PROBED — clone/source IP collision (see the clone-identity fix)" +elif [ -n "$PEER_IP" ]; then + inf "probing archive ($ARCH_IP_PRE) → peer ($PEER_IP), no listener: refused means it arrived, timeout means it was dropped" rc="$(box_curl archive "http://$PEER_IP:8088")" v="$(verdict "$rc")" timeout -k 5 30 incus exec archive -- ping -c1 -W2 "$PEER_IP" >/dev/null 2>&1 /dev/null 2>&1 hv="$(verdict $?)" @@ -521,7 +532,7 @@ if [ -n "$ARCH_IP" ]; then aud "A7 inbound host→box: INCONCLUSIVE ($hv)" ;; esac else - no "could not read archive's eth0 address — the inbound probe never ran" + no "could not read archive's claudenet address — the inbound probe never ran" aud "A7 inbound host→box: NOT PROBED" fi