forked from heavy-duty/box
fix(drill): the NIC inside a VM is enp5s0, not eth0 — read by subnet instead
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 <noreply@anthropic.com>
This commit is contained in:
parent
d8731b4772
commit
6899fc3626
2 changed files with 39 additions and 21 deletions
|
|
@ -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
|
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)
|
one: `curl` exit `7` (refused) means the packet *arrived*, `28` (timeout)
|
||||||
means it was *dropped*. A closed port answers the question.
|
means it was *dropped*. A closed port answers the question.
|
||||||
4. **`incus list` name filters are not regexes.** `incus list "^peer$"` matches
|
4. **The box's address is hard to read, and every way of getting it wrong was
|
||||||
nothing and returns empty — silently. This is how A3 went unprobed for
|
tried.** (a) `incus list` name filters are **not regexes** — `incus list
|
||||||
three runs. Read addresses from inside the box (`ip -4 -o addr show dev
|
"^peer$"` silently matches nothing. (b) Its CSV quotes a multi-address box
|
||||||
eth0`), not out of `incus list` CSV (which also quotes multi-address boxes
|
across lines. (c) **The interface is not `eth0`.** The *profile* names the
|
||||||
across lines).
|
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
|
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
|
run then poisons the next: stale boxes survive cleanup and cascade into
|
||||||
half a dozen unrelated FAILs. Delete one name at a time.
|
half a dozen unrelated FAILs. Delete one name at a time.
|
||||||
|
|
|
||||||
|
|
@ -86,17 +86,20 @@ in_box() {
|
||||||
return "$rc"
|
return "$rc"
|
||||||
}
|
}
|
||||||
|
|
||||||
eth0_ip() { # the box's address on claudenet — eth0 exactly; a box running
|
# The box's address ON CLAUDENET. Three ways to get this wrong, all of them hit:
|
||||||
# docker has several addresses, so never just "the first IP".
|
# · 'incus list' name filters are NOT regexes ("^b$" silently matches nothing)
|
||||||
# ('incus list' name filters are NOT regexes — the anchored
|
# · its CSV quotes a multi-address box across lines
|
||||||
# "^b$" form matched nothing, which is how A3 went unprobed for
|
# · and the interface is NOT called eth0. The PROFILE names the device eth0,
|
||||||
# three runs — and its CSV quotes multi-address boxes across
|
# but inside a VM guest predictable naming renames it enp5s0. Six runs of
|
||||||
# lines. Reading 'ip -o' inside the box is unambiguous.)
|
# A3 "not probed" were this, not the network.
|
||||||
# Retries: the agent answers before DHCP hands out the address.
|
# 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
|
local b="$1" ip _i
|
||||||
for _i in $(seq 1 15); do
|
for _i in $(seq 1 15); do
|
||||||
ip="$(in_box "$b" ip -4 -o addr show dev eth0 \
|
ip="$(in_box "$b" ip -4 -o addr show scope global \
|
||||||
| awk '{ for (i = 1; i < NF; i++) if ($i == "inet") { split($(i+1), a, "/"); print a[1]; exit } }')"
|
| 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; }
|
[ -n "$ip" ] && { printf '%s\n' "$ip"; return 0; }
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
|
|
@ -457,9 +460,17 @@ esac
|
||||||
# port answers the question just as well (refused = the packet arrived), and
|
# 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
|
# the listener was what kept wedging the run. Ping corroborates: if the two
|
||||||
# disagree, say so rather than pick one.
|
# disagree, say so rather than pick one.
|
||||||
PEER_IP="$(eth0_ip peer)"
|
PEER_IP="$(claudenet_ip peer)"
|
||||||
if [ -n "$PEER_IP" ]; then
|
ARCH_IP_PRE="$(claudenet_ip archive)"
|
||||||
inf "probing archive → peer ($PEER_IP), no listener: refused means it arrived, timeout means it was dropped"
|
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")"
|
rc="$(box_curl archive "http://$PEER_IP:8088")"
|
||||||
v="$(verdict "$rc")"
|
v="$(verdict "$rc")"
|
||||||
timeout -k 5 30 incus exec archive -- ping -c1 -W2 "$PEER_IP" >/dev/null 2>&1 </dev/null
|
timeout -k 5 30 incus exec archive -- ping -c1 -W2 "$PEER_IP" >/dev/null 2>&1 </dev/null
|
||||||
|
|
@ -482,8 +493,8 @@ if [ -n "$PEER_IP" ]; then
|
||||||
aud "A3 sibling: INCONCLUSIVE (curl exit $rc, ping exit $png)" ;;
|
aud "A3 sibling: INCONCLUSIVE (curl exit $rc, ping exit $png)" ;;
|
||||||
esac
|
esac
|
||||||
else
|
else
|
||||||
no "could not read peer's eth0 address — the sibling probe never ran"
|
no "could not read peer's claudenet address — the sibling probe never ran"
|
||||||
aud "A3 sibling: NOT PROBED (no eth0 address on peer)"
|
aud "A3 sibling: NOT PROBED (no 10.87.x address on peer)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# C5 — DNS enumeration (#15 A4): #12 predicts this LEAKS today. Either way it
|
# C5 — DNS enumeration (#15 A4): #12 predicts this LEAKS today. Either way it
|
||||||
|
|
@ -505,7 +516,7 @@ fi
|
||||||
|
|
||||||
# C7 — inbound, host → box (#15 A7): the ACL's default ingress drop. Same
|
# C7 — inbound, host → box (#15 A7): the ACL's default ingress drop. Same
|
||||||
# listener-free logic, run from the host this time.
|
# listener-free logic, run from the host this time.
|
||||||
ARCH_IP="$(eth0_ip archive)"
|
ARCH_IP="$(claudenet_ip archive)"
|
||||||
if [ -n "$ARCH_IP" ]; then
|
if [ -n "$ARCH_IP" ]; then
|
||||||
curl -sS -m 5 -o /dev/null "http://$ARCH_IP:8087" >/dev/null 2>&1
|
curl -sS -m 5 -o /dev/null "http://$ARCH_IP:8087" >/dev/null 2>&1
|
||||||
hv="$(verdict $?)"
|
hv="$(verdict $?)"
|
||||||
|
|
@ -521,7 +532,7 @@ if [ -n "$ARCH_IP" ]; then
|
||||||
aud "A7 inbound host→box: INCONCLUSIVE ($hv)" ;;
|
aud "A7 inbound host→box: INCONCLUSIVE ($hv)" ;;
|
||||||
esac
|
esac
|
||||||
else
|
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"
|
aud "A7 inbound host→box: NOT PROBED"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue