Merge pull request 'Report the addressing mode, and catch a lease that stopped renewing' (#3) from lease-health into main
Reviewed-on: #3 Reviewed-by: branderson <brad@branderson.io>
This commit was merged in pull request #3.
This commit is contained in:
+114
-1
@@ -108,6 +108,56 @@ forge_host() {
|
||||
|
||||
resolve_ok() { getent hosts "$1" >/dev/null 2>&1; }
|
||||
|
||||
# --- addressing -------------------------------------------------------------
|
||||
#
|
||||
# Which mode this host uses decides whether it needs the two DHCP rules on the
|
||||
# hypervisor, and that question has the nastiest failure in the whole design.
|
||||
# A DHCP guest behind these rules renews through a path the firewall may be
|
||||
# dropping. Nothing breaks at boot, because the first lease was obtained before
|
||||
# the rules mattered. The VM simply stops renewing and falls off the network up
|
||||
# to a full lease time later, looking nothing like a firewall rule.
|
||||
|
||||
INTERFACES_FILE=${INTERFACES_FILE:-/etc/network/interfaces}
|
||||
LEASE_GLOB=${LEASE_GLOB:-/var/lib/dhcp/dhclient*.leases}
|
||||
|
||||
lease_file() {
|
||||
local f
|
||||
# shellcheck disable=SC2086
|
||||
f=$(ls -t $LEASE_GLOB 2>/dev/null | head -1)
|
||||
[ -n "$f" ] || return 1
|
||||
printf '%s\n' "$f"
|
||||
}
|
||||
|
||||
# static | dhcp | unknown
|
||||
#
|
||||
# Static is checked first, and that order is the whole correctness of this
|
||||
# function. A host pinned by dejima-devbox-static-ip keeps the lease files from
|
||||
# whatever it was doing before, so letting a lease decide would tell every
|
||||
# pinned host it needs rules it does not need.
|
||||
addressing_mode() {
|
||||
if [ -r "$INTERFACES_FILE" ] \
|
||||
&& grep -qE '^[[:space:]]*iface[[:space:]]+\S+[[:space:]]+inet[[:space:]]+static' "$INTERFACES_FILE"; then
|
||||
printf 'static\n'; return 0
|
||||
fi
|
||||
if { [ -r "$INTERFACES_FILE" ] \
|
||||
&& grep -qE '^[[:space:]]*iface[[:space:]]+\S+[[:space:]]+inet[[:space:]]+dhcp' "$INTERFACES_FILE"; } \
|
||||
|| lease_file >/dev/null; then
|
||||
printf 'dhcp\n'; return 0
|
||||
fi
|
||||
# Not guessed. An unrecognised setup is a thing to look at, not a thing to
|
||||
# assume the friendlier answer about.
|
||||
printf 'unknown\n'
|
||||
}
|
||||
|
||||
# dhclient writes "renew 3 2026/08/10 04:12:33;" in UTC. Prints epoch seconds.
|
||||
lease_time() { # lease_time FILE KEY
|
||||
local v
|
||||
v=$(grep -E "^[[:space:]]*$2 " "$1" 2>/dev/null | tail -1 \
|
||||
| sed -E "s/^[[:space:]]*$2 [0-9] //; s/;[[:space:]]*$//") || return 1
|
||||
[ -n "$v" ] || return 1
|
||||
date -u -d "$v" +%s 2>/dev/null || return 1
|
||||
}
|
||||
|
||||
# Does the network demonstrably work?
|
||||
#
|
||||
# This is the whole reason a timeout to the gateway can mean anything. If the
|
||||
@@ -163,6 +213,47 @@ cmd_reach() {
|
||||
return $rc
|
||||
}
|
||||
|
||||
# Is the lease actually being renewed?
|
||||
#
|
||||
# Detected without keeping any state between runs, which matters: the obvious
|
||||
# design is to record the expiry each run and see whether it advances, and that
|
||||
# means this program starts writing files. It does not need to. dhclient
|
||||
# rewrites the lease file on every successful renewal, so the file's own mtime
|
||||
# against the renewal time it records answers the question in one pass.
|
||||
#
|
||||
# Past T1 with an mtime older than T1 means dhclient asked and got nothing
|
||||
# back. That is precisely the shape of a dropped reply, and it is visible hours
|
||||
# before the lease expires and the VM disappears.
|
||||
lease_report() {
|
||||
local f now renew expire
|
||||
if ! f=$(lease_file); then
|
||||
say " No lease file found, so renewal cannot be checked here."
|
||||
return 0
|
||||
fi
|
||||
now=$(date -u +%s)
|
||||
renew=$(lease_time "$f" renew) || renew=""
|
||||
expire=$(lease_time "$f" expire) || expire=""
|
||||
|
||||
if [ -n "$expire" ]; then
|
||||
say " Lease expires in $(( (expire - now) / 3600 ))h."
|
||||
fi
|
||||
if [ -z "$renew" ]; then
|
||||
say " No renewal time in $f, so renewal cannot be checked."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$now" -gt "$renew" ] && [ "$(stat -c %Y "$f" 2>/dev/null || echo 0)" -lt "$renew" ]; then
|
||||
say " THIS LEASE HAS NOT BEEN RENEWED since its renewal time passed."
|
||||
say " dhclient asked and got nothing back, which is what a dropped"
|
||||
say " reply looks like. Check that BOTH DHCP rules are on the"
|
||||
say " hypervisor: the outbound one alone gets the request out and the"
|
||||
say " lease still never completes."
|
||||
say " This is visible now and costs you the network when the lease"
|
||||
say " expires, which is the whole reason it is worth reporting early."
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# What root on this VM would obtain.
|
||||
#
|
||||
# This looks like the target list the audit refuses to keep, and it is not. The
|
||||
@@ -173,7 +264,7 @@ cmd_reach() {
|
||||
#
|
||||
# Paths and modes. Never contents.
|
||||
cmd_radius() {
|
||||
local f found=0 newest
|
||||
local f found=0 newest mode
|
||||
|
||||
say ""
|
||||
say "== What root on this VM would get =="
|
||||
@@ -214,6 +305,28 @@ cmd_radius() {
|
||||
say " no ~/.claude/projects on this host"
|
||||
fi
|
||||
|
||||
say ""
|
||||
say "Addressing:"
|
||||
mode=$(addressing_mode)
|
||||
case $mode in
|
||||
static)
|
||||
say " static in the guest. Needs no DHCP rules on the hypervisor,"
|
||||
say " which is the tightest of the three modes at the boundary."
|
||||
;;
|
||||
dhcp)
|
||||
say " DHCP. The hypervisor needs BOTH halves of the DHCP pair - out"
|
||||
say " udp/67 and in udp/68 - including when the address is reserved"
|
||||
say " for you on the router. A reservation does not stop the guest"
|
||||
say " speaking DHCP."
|
||||
lease_report
|
||||
;;
|
||||
*)
|
||||
say " could not tell. Neither a static nor a dhcp stanza was found"
|
||||
say " in $INTERFACES_FILE and there is no lease file, so whether"
|
||||
say " this host needs the DHCP rules is an open question."
|
||||
;;
|
||||
esac
|
||||
|
||||
say ""
|
||||
say "Patch currency:"
|
||||
say " running kernel: $(uname -r)"
|
||||
|
||||
@@ -64,6 +64,30 @@ Plain DHCP is more workable here than in the sister project, because the inbound
|
||||
rules are scoped to client subnets rather than to this VM's own address, so a
|
||||
changed lease breaks nothing in the rule set.
|
||||
|
||||
### What the audit reports about addressing
|
||||
|
||||
`dejima-devbox-audit radius` names the mode and, on DHCP, checks the lease.
|
||||
|
||||
Static is decided before any lease file is consulted, deliberately: a host
|
||||
pinned by `dejima-devbox-static-ip` keeps the leases from whatever it was doing
|
||||
before, and letting those decide would tell every pinned host it needs rules it
|
||||
does not need. A setup matching neither is reported as `unknown` rather than
|
||||
assumed to be the friendlier answer.
|
||||
|
||||
**The lease check exists for the worst failure in this design.** A DHCP guest
|
||||
behind these rules renews through a path the firewall may be dropping. Nothing
|
||||
breaks at boot, because the first lease was obtained before the rules mattered.
|
||||
The VM stops renewing and falls off the network up to a full lease time later,
|
||||
looking nothing like a firewall rule.
|
||||
|
||||
It is detected without keeping any state between runs. The obvious design
|
||||
records the expiry each run and watches whether it advances, which means this
|
||||
program starts writing files. It does not need to: `dhclient` rewrites the lease
|
||||
file on every successful renewal, so the file's own mtime against the renewal
|
||||
time it records answers the question in one pass. Past T1 with an mtime older
|
||||
than T1 means `dhclient` asked and got nothing back, which is exactly the shape
|
||||
of a dropped reply, and it is visible hours before the lease expires.
|
||||
|
||||
## Configuration
|
||||
|
||||
`/etc/dejima-devbox/config`, one key:
|
||||
|
||||
@@ -194,3 +194,87 @@ setup() {
|
||||
[[ "$output" == *"Patch currency"* ]]
|
||||
[[ "$output" == *"not a detail"* ]]
|
||||
}
|
||||
|
||||
# --- addressing mode and lease health ---------------------------------------
|
||||
#
|
||||
# The spec asked for this and the first implementation left it out. It matters
|
||||
# because it is the one failure in this design that is invisible for up to a
|
||||
# full lease time: a DHCP guest behind these rules renews through a path the
|
||||
# firewall may be dropping, nothing breaks at boot, and the VM falls off the
|
||||
# network much later looking nothing like a firewall rule.
|
||||
|
||||
@test "a static interface is reported as static" {
|
||||
INTERFACES_FILE="$BATS_TEST_TMPDIR/interfaces"
|
||||
printf 'iface ens18 inet static\n address 192.0.2.5/24\n' > "$INTERFACES_FILE"
|
||||
run addressing_mode
|
||||
[ "$output" = static ]
|
||||
}
|
||||
|
||||
@test "a dhcp interface is reported as dhcp" {
|
||||
INTERFACES_FILE="$BATS_TEST_TMPDIR/interfaces"
|
||||
printf 'iface ens18 inet dhcp\n' > "$INTERFACES_FILE"
|
||||
run addressing_mode
|
||||
[ "$output" = dhcp ]
|
||||
}
|
||||
|
||||
@test "static wins over a stale lease left behind by an earlier dhcp life" {
|
||||
# A host pinned by dejima-devbox-static-ip keeps its old lease files. If
|
||||
# those decided the answer, every pinned host would be told it needs DHCP
|
||||
# rules it does not need.
|
||||
INTERFACES_FILE="$BATS_TEST_TMPDIR/interfaces"
|
||||
printf 'iface ens18 inet static\n' > "$INTERFACES_FILE"
|
||||
lease_file() { printf '%s\n' "$BATS_TEST_TMPDIR/old.leases"; }
|
||||
printf 'lease {\n expire 0 2020/01/01 00:00:00;\n}\n' > "$BATS_TEST_TMPDIR/old.leases"
|
||||
run addressing_mode
|
||||
[ "$output" = static ]
|
||||
}
|
||||
|
||||
@test "neither is reported as unknown rather than guessed" {
|
||||
INTERFACES_FILE="$BATS_TEST_TMPDIR/none"
|
||||
lease_file() { return 1; }
|
||||
run addressing_mode
|
||||
[ "$output" = unknown ]
|
||||
}
|
||||
|
||||
@test "a lease already past its renewal time is reported as not renewing" {
|
||||
INTERFACES_FILE="$BATS_TEST_TMPDIR/interfaces"
|
||||
printf 'iface ens18 inet dhcp\n' > "$INTERFACES_FILE"
|
||||
# Not $f: cmd_radius declares `local f`, which shadows it and made this
|
||||
# stub return an empty path. A fixture variable a function might shadow is
|
||||
# a trap; give it a name nothing else would pick.
|
||||
fixture_lease="$BATS_TEST_TMPDIR/cur.leases"
|
||||
lease_file() { printf '%s\n' "$fixture_lease"; }
|
||||
past=$(date -u -d '2 hours ago' '+%Y/%m/%d %H:%M:%S')
|
||||
future=$(date -u -d '6 hours' '+%Y/%m/%d %H:%M:%S')
|
||||
printf 'lease {\n renew 0 %s;\n expire 0 %s;\n}\n' "$past" "$future" > "$fixture_lease"
|
||||
touch -d '3 hours ago' "$fixture_lease"
|
||||
run cmd_radius
|
||||
[[ "$output" == *"NOT BEEN RENEWED"* ]]
|
||||
}
|
||||
|
||||
@test "a lease still inside its renewal window is not reported as failing" {
|
||||
INTERFACES_FILE="$BATS_TEST_TMPDIR/interfaces"
|
||||
printf 'iface ens18 inet dhcp\n' > "$INTERFACES_FILE"
|
||||
fixture_lease="$BATS_TEST_TMPDIR/cur.leases"
|
||||
lease_file() { printf '%s\n' "$fixture_lease"; }
|
||||
future=$(date -u -d '6 hours' '+%Y/%m/%d %H:%M:%S')
|
||||
later=$(date -u -d '12 hours' '+%Y/%m/%d %H:%M:%S')
|
||||
printf 'lease {\n renew 0 %s;\n expire 0 %s;\n}\n' "$future" "$later" > "$fixture_lease"
|
||||
run cmd_radius
|
||||
# Both halves. Asserting only the absence of the warning passes on a
|
||||
# report that says nothing about leases at all, which is how this test
|
||||
# went green before the feature existed.
|
||||
[[ "$output" == *"Addressing"* ]]
|
||||
[[ "$output" != *"NOT BEEN RENEWED"* ]]
|
||||
}
|
||||
|
||||
@test "an unparseable lease file does not take the report down with it" {
|
||||
INTERFACES_FILE="$BATS_TEST_TMPDIR/interfaces"
|
||||
printf 'iface ens18 inet dhcp\n' > "$INTERFACES_FILE"
|
||||
fixture_lease="$BATS_TEST_TMPDIR/junk.leases"
|
||||
lease_file() { printf '%s\n' "$fixture_lease"; }
|
||||
printf 'this is not a lease\n' > "$fixture_lease"
|
||||
run cmd_radius
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"not a detail"* ]]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user