Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7651461be2 | |||
| a0c88e5183 | |||
| 2d5840ba11 |
+30
-1
@@ -1,4 +1,33 @@
|
||||
## Unreleased (2026-10-21) — every guest may swap as much as its RAM (R-1192, the 8 GB freeze) — COMMITTED, NOT RELEASED (the release is the operator's word)
|
||||
## Unreleased (2026-10-22 night) — the tunnel's future-stamped handshake is reported (R-1204) — COMMITTED, NOT RELEASED (no agent release tonight); root SSH by key only (R-982, D4) is a READY PATCH, not on main
|
||||
|
||||
- **R-982 / D4 — a ready patch, NOT on main.** Built and proven the same night (felhom.eu
|
||||
`audits/night-2026-10-22/P2-D4/`): a bundle drop-in `/etc/ssh/sshd_config.d/10-felhom-root-key-only.conf`
|
||||
(`PermitRootLogin prohibit-password`), a `sshd` content check (exactly `prohibit-password`; `sshd -t -f`), and after
|
||||
an install that wrote it `sshd -t` + `systemctl reload ssh`; tests `RootKeyOnly` (3), red-proved. Live on a fresh
|
||||
nested box from ISO 1.30.0: root password over SSH refused, root key works, the console and the web page still take
|
||||
the password. It was committed (`a0c88e5`) and moved back out (this commit): the installer's uninstall must remove
|
||||
every bundle file (felhom.eu `scripts/test_hostinstall.py`), and a push of `felhom-host-install.sh` publishes the
|
||||
installer to every new box — forbidden tonight. Both halves are patches: `felhom-agent-bundle-D4.patch` and
|
||||
`felhom.eu-installer-D4.patch` in that folder; apply them together with the agent release.
|
||||
- **R-1204 — `wireguard.handshake_future_s`.** A box that ran once with its clock ahead keeps a handshake stamped in the
|
||||
future; ep0 refuses every newer one, and `last_handshake_age_s` (clamped at 0) read „just now" for ever. The heartbeat
|
||||
now carries how many seconds the handshake lies ahead (> 60 s only); the age keeps its clamp. The hub alarms on it
|
||||
(felhom.eu hub `wg_tunnel_stale`). Test `TestR1204_AFutureHandshakeIsReported`, red-proved.
|
||||
|
||||
## v0.162.0 — every guest may swap as much as its RAM (R-1192, the 8 GB freeze); the host's swap is reported and checked; no false „operator access degraded" on a fresh box (R-1194) (2026-10-22) — RELEASED on the operator's yes; the vouch is the operator's
|
||||
|
||||
Released 2026-10-22 07:40 UTC (operator yes, the morning answers) with `scripts/release-agent.sh` from a clean clone at
|
||||
`b310773`: binary sha256 `5fd7a23bfe319cb521c4bc399612d5fb3413f1e695f79bc663c386f69c757928` (the same bytes as the
|
||||
night's candidate), config bundle sha256 `ac772967fd4669045e837a70bf5acca239c58d55f7f00212e472a8facd60b5a5`, tag
|
||||
`v0.162.0`; no `configs/` change since v0.161.0. Delivered by one signed `agent_update` job per box to demo-hp, Tester 1
|
||||
and demo-felhom (07:49–07:58 UTC): each `signed op COMPLETED`, `capabilities self-check ok=76 total=76`, `update
|
||||
committed`, then `guest-swap: raised` for its 9201 (512 MB → its RAM). Evidence felhom.eu
|
||||
`documentation/audits/morning-2026-10-22/release/04-agent-delivery.txt`. **Found at once:** demo-hp (host swap 8191 MB,
|
||||
guest swap 25898 MB) and demo-felhom (8191 MB vs 12288 MB) logged the host-swap WARN and the hub mailed
|
||||
`host_swap_short` — the check compares the host's swap with the guests' whole swap allowance, which a guest larger
|
||||
than the installer's 8 GB host swap always exceeds (felhom.eu register R-1216). Soak (night of 2026-10-21, 8 GB box):
|
||||
11 h, no freeze, the host's swap peaked at 2.7 GB used.
|
||||
|
||||
|
||||
The freeze of the rehearsal's first afternoon, reproduced and measured on demo-hp (felhom.eu
|
||||
`audits/fixes-2026-10-21/P4-freeze/`): Proxmox gives an LXC a 512 MB swap cap when nothing sets `swap`, and the agent
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
# REPORT — agent, the night of 2026-10-21: the host's swap is reported; no false emergency-door alarm on a new box — COMMITTED, NOT RELEASED
|
||||
# REPORT — agent v0.162.0 released (2026-10-22 morning, operator yes) — NOT VOUCHED (the vouch is the operator's)
|
||||
|
||||
**Brief:** the unattended night brief of 2026-10-21 (Parts 1, 2, 5). Architecture: felhom.eu `03-host-agent.md` §9 (the
|
||||
new swap paragraph). Both join the swap fix (`286a7e3`) in the coming 0.162.0 — the operator's release and vouch (D26 = A).
|
||||
**What:** the swap fix (R-1192: every guest may swap as much as its RAM), the host-swap report and check, and R-1194 (no
|
||||
false „operator access degraded" on a fresh box). Proven the night before on a fresh 8 GB box: 11 h, no freeze; with the
|
||||
host's swap off the freeze comes back (felhom.eu `REPORT-night-2026-10-21.md`).
|
||||
|
||||
- **Host swap (R-1192 follow-up), `0da792e`:** the host report carries `host.host_swap_mb` (Proxmox node status = the
|
||||
kernel's SwapTotal) and `host.guest_swap_mb` (the guests' `swap` sum; each guest spec `swap_bytes`); one WARN when the
|
||||
first is below the second, one INFO when covered again; `GET /guest/memory` carries both for the controller's install
|
||||
page. The agent never creates host swap (register R-1207). Proven live on the soak box: the hub stored
|
||||
`host_swap_mb 7935, guest_swap_mb 4096`.
|
||||
- **R-1194, `0da792e`:** no OOB stanza until the felhom-sshd loop's first pass has run — the first report on a fresh box
|
||||
read a config not yet rendered and the hub mailed `oob_degraded` (reproduced 2026-10-21 20:34 CEST on a fresh install).
|
||||
- **The swap fix itself** ran as a hand-placed fixture on the 8 GB soak box: guest swap 512 → 4096 MB one minute after
|
||||
start; the 2-hour household load answered every probe (felhom.eu `audits/night-2026-10-21/P1-soak/`).
|
||||
- Release candidate built from a clean clone at `0da792e` with the release flags, version `0.162.0`:
|
||||
sha256 `5fd7a23bfe319cb521c4bc399612d5fb3413f1e695f79bc663c386f69c757928` (not published).
|
||||
- Tests (red-proved): `TestR1192_HostReportCarriesSwapAndWarnsOnce`, `TestR1192_GuestMemoryCarriesHostAndGuestSwap`,
|
||||
`TestR1194_NoOOBStanzaBeforeTheFirstPass`. CI job 2176 green.
|
||||
- **Released** with `scripts/release-agent.sh 0.162.0` from a clean clone at `b310773`: binary sha256
|
||||
`5fd7a23bfe319cb521c4bc399612d5fb3413f1e695f79bc663c386f69c757928` (the same bytes as the night's candidate), bundle
|
||||
`ac772967…60b5a5`, tag `v0.162.0`, round trip verified by the script.
|
||||
- **Delivered** by one signed `agent_update` job per box to demo-hp, Tester 1 and demo-felhom: all three `update
|
||||
committed` with `capabilities self-check ok=76 total=76`; each raised its 9201's swap from 512 MB to its RAM. Evidence
|
||||
felhom.eu `documentation/audits/morning-2026-10-22/release/04-agent-delivery.txt`.
|
||||
- **Found:** the host-swap check is too strict for a guest larger than 8 GB. demo-hp (host 8191 MB vs guests 25898 MB)
|
||||
and demo-felhom (8191 vs 12288) warned and the hub mailed `host_swap_short`; the soak used at most 2.7 GB of host swap.
|
||||
Filed felhom.eu R-1216 with two options for the operator.
|
||||
- **Not done:** the vouch (Configs → Day-0 artifacts, agent 0.162.0 / the sha above, the bundle with it) — the operator's
|
||||
click; CC's token cannot. Tester 2's box was not sent the update (not a test box; the operator's word).
|
||||
|
||||
@@ -292,7 +292,11 @@ type WireguardStatus struct {
|
||||
Registered bool `json:"registered"` // the registration marker exists
|
||||
Active bool `json:"active"` // wg-quick@wg-felhom is-active
|
||||
LastHandshakeAgeS *int64 `json:"last_handshake_age_s,omitempty"`
|
||||
AssignedIP string `json:"assigned_ip,omitempty"` // from the marker, e.g. "10.77.0.2/32"
|
||||
// HandshakeFutureS (R-1204): seconds the latest handshake lies in the FUTURE by this box's clock — set (> 0) only
|
||||
// then. A box that ran once with its clock ahead keeps a handshake stamped in the future; WireGuard on ep0 then
|
||||
// refuses every newer handshake, and LastHandshakeAgeS (clamped at 0) reads „just now" for ever. The hub alarms on it.
|
||||
HandshakeFutureS *int64 `json:"handshake_future_s,omitempty"`
|
||||
AssignedIP string `json:"assigned_ip,omitempty"` // from the marker, e.g. "10.77.0.2/32"
|
||||
}
|
||||
|
||||
// SystemInfo is the `system` stanza (see HostReport.System).
|
||||
|
||||
@@ -661,6 +661,9 @@ func (m *Manager) Status(ctx context.Context) *hub.WireguardStatus {
|
||||
if age, ok := parseHandshakeAge(string(out), m.now()); ok {
|
||||
st.LastHandshakeAgeS = &age
|
||||
}
|
||||
if fut, ok := handshakeFuture(string(out), m.now()); ok {
|
||||
st.HandshakeFutureS = &fut
|
||||
}
|
||||
}
|
||||
}
|
||||
if st.Pubkey == "" && !st.Registered && !st.Active {
|
||||
@@ -669,6 +672,28 @@ func (m *Manager) Status(ctx context.Context) *hub.WireguardStatus {
|
||||
return st
|
||||
}
|
||||
|
||||
// handshakeFuture (R-1204) reports how many seconds the latest handshake lies in the future, ok only when it does
|
||||
// (by more than a minute — clock jitter is not the case). parseHandshakeAge clamps that case to 0 („just now"), which
|
||||
// is what made a box whose clock had run ahead look healthy while ep0 refused its tunnel. Pinned by
|
||||
// TestR1204_AFutureHandshakeIsReported.
|
||||
func handshakeFuture(out string, now time.Time) (int64, bool) {
|
||||
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
|
||||
f := strings.Fields(line)
|
||||
if len(f) != 2 {
|
||||
continue
|
||||
}
|
||||
epoch, err := strconv.ParseInt(f[1], 10, 64)
|
||||
if err != nil || epoch <= 0 {
|
||||
continue
|
||||
}
|
||||
if fut := epoch - now.Unix(); fut > 60 {
|
||||
return fut, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// parseHandshakeAge parses `wg show <if> latest-handshakes` output ("<peer-pub>\t<epoch>").
|
||||
// epoch 0 = no handshake yet → not ok (nil in the report; nil ≠ 0).
|
||||
func parseHandshakeAge(out string, now time.Time) (int64, bool) {
|
||||
|
||||
@@ -696,3 +696,31 @@ func TestInitialResolveFailureNoTeardown(t *testing.T) {
|
||||
t.Errorf("did not recover after DNS returned: %v", rr.calls)
|
||||
}
|
||||
}
|
||||
|
||||
// R-1204: a box that ran once with its clock ahead keeps a handshake stamped in the FUTURE — ep0 then refuses every
|
||||
// newer one. The age stays clamped at 0 (unchanged contract), and the new field says how far ahead it lies; a
|
||||
// handshake in the past (or within a minute of now) never sets it.
|
||||
// RED-PROOF: with Status not setting HandshakeFutureS the first assertion fails — seen failing 2026-10-22.
|
||||
func TestR1204_AFutureHandshakeIsReported(t *testing.T) {
|
||||
fh := &fakeHub{}
|
||||
m, _, active, _ := testManager(t, fh)
|
||||
ctx := context.Background()
|
||||
m.Apply(ctx, false, nil)
|
||||
*active = true
|
||||
m.now = func() time.Time { return time.Unix(1783107115-2*24*3600, 0) } // the clock came back two days earlier
|
||||
st := m.Status(ctx)
|
||||
if st.HandshakeFutureS == nil || *st.HandshakeFutureS != 2*24*3600 {
|
||||
t.Fatalf("handshake_future_s = %v, want %d", st.HandshakeFutureS, 2*24*3600)
|
||||
}
|
||||
if st.LastHandshakeAgeS == nil || *st.LastHandshakeAgeS != 0 {
|
||||
t.Fatalf("the age stays clamped at 0: %v", st.LastHandshakeAgeS)
|
||||
}
|
||||
m.now = func() time.Time { return time.Unix(1783107115+100, 0) }
|
||||
if st = m.Status(ctx); st.HandshakeFutureS != nil {
|
||||
t.Fatalf("a past handshake must not set handshake_future_s: %v", *st.HandshakeFutureS)
|
||||
}
|
||||
m.now = func() time.Time { return time.Unix(1783107115-30, 0) } // 30 s of jitter is not the case
|
||||
if st = m.Status(ctx); st.HandshakeFutureS != nil {
|
||||
t.Fatalf("clock jitter must not set handshake_future_s")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user