Files
hq/04-ISSUES/149-a-declaration-that-shrinks-to-empty-is-skipped-not-sent/00-report.md
T
jschoubben 96bdffa9bc Two records were numbered 127; the second becomes 149, and is resolved
A number identifies a record, and two were given 127 on 2026-09-27. The one
three documents and three source files cite by number keeps it; the other
becomes 149, says so in its own heading, and its two inbound references are
repointed.

It is also resolved: an empty declaration is sent carrying owns_nothing rather
than skipped, and the host refuses an empty body that does not carry it, so
emptiness cannot be read as a truncated declaration.
2026-09-29 22:33:40 +02:00

3.0 KiB

status, opened, located-in, fixed-by
status opened located-in fixed-by
resolved 2026-09-27
mesh-controller cmd/mesh-controller/push.go
mesh-controller cmd/mesh-controller/sendable.go
mesh-controller sendable.go and push.go — an empty declaration is sent carrying `owns_nothing`, and the host refuses an empty body that does not carry it

149 — a declaration that shrinks to empty is skipped, so the node keeps what it should drop

Opened as 127 and renumbered on 2026-09-29: two records were given that number on the same day. The other kept it, because three documents and three source files cite it by number and nothing cited this one but a decision and a sibling issue, both corrected with this move.

What was observed

Fixing the broker-opening leak (the foundation port scoped to the broker's host) made ace's declaration compose to zero resources — ace is adopted with nothing assigned, and the stray opening was its only resource. push ace then printed ace is assigned nothing — skipped and sent nothing. ace goes on holding adoption.opening-tcp-5671-incoming in its ufw, because it was never told the resource is gone.

composeEach (push.go) skips any node whose composed declaration has no resources. That is right for a node that never had anything. It is wrong for a node that had resources and now composes to none: the empty declaration is the correction, and skipping it leaves the last non-empty one in force forever.

Why it matters

Any adopted node whose openings (or other baseline resources) are all removed keeps the stale ones until something else pushes a non-empty declaration to it. Converge is unaffected — it composes the full ruleset fresh — so this is an incremental-push gap, not a firewall-safety one. But "the mesh cannot tell a node to drop its last resource" is a real hole in reconcile.

The fix, roughly

Send the empty declaration when the node's last-sent declaration was non-empty — i.e. skip only when empty-and-was-already-empty. Requires push to know (or the host to be told) that the node held something. Simplest: always send to a placed, enrolled node; let an empty declaration mean "own nothing", which the host already applies correctly when it receives one.

Workaround used

On ace, one command drops it permanently (the corrected controller never re-composes it): sudo ufw delete allow 5671. At ace's converge it would clear on its own.

Closed

2026-09-29, in a grooming pass. Both halves are on main and both name this issue.

  • The control plane sends it: a declaration that composes to no resources goes out with owns_nothing, and push says sent, not skipped.
  • The host refuses an empty body that does not carry it, so a truncated or mis-composed declaration can never be read as "own nothing" — which is the failure the fix had to avoid while making the empty case expressible.

Closed by reading the code rather than by watching a machine let go of a stray resource; the record says so rather than implying a run.