Merge pull request 'A converged machine can speak unasked' (#48) from fix/a-converged-machine-can-speak-unasked into main

This commit was merged in pull request #48.
This commit is contained in:
2026-09-28 23:13:13 +00:00
2 changed files with 63 additions and 1 deletions
+28 -1
View File
@@ -1198,7 +1198,17 @@ func holdTheMachine(ctx context.Context, opts options, mine identity.Identity, s
// A reconcile is otherwise silent. On an adopted node it speaks when what it holds or // A reconcile is otherwise silent. On an adopted node it speaks when what it holds or
// its firewall changed, because that is how a predecessor still writing is caught // its firewall changed, because that is how a predecessor still writing is caught
// (novox/hq ADR 0100); publish decides whether anything did. // (novox/hq ADR 0100); publish decides whether anything did.
if publish != nil && report.Refused == "" && (len(report.Held) > 0 || report.Firewall != "") { //
// **And on any node, when it can say which links face outside** (novox/hq ADR 0140). A
// converged node holds nothing and found no firewall, so this gate closed on it and the
// node could speak only in reply to a declaration — while the mesh composes no declaration
// for a node that has not said which links face outside. A machine waiting for a push that
// was waiting for the machine, and measured: three converged machines sat silent while the
// control plane refused to send them a filter.
//
// Offered, not published: whether it is news is still the watch's to decide, so an
// unchanged answer costs one comparison every reconcile and nothing on the bus.
if publish != nil && worthSaying(report) {
publish(report) publish(report)
} }
switch { switch {
@@ -1210,6 +1220,23 @@ func holdTheMachine(ctx context.Context, opts options, mine identity.Identity, s
} }
} }
// worthSaying is whether a reconcile's report carries anything the mesh needs to hear unasked.
//
// **Named rather than written into the loop**, so the rule can be tested. It was a condition inline
// and it was wrong in a way nothing could catch: it asked only what an adopted node reports, so a
// converged node — which holds nothing and found no firewall — could speak only in reply to a
// declaration, while the mesh composes no declaration for a node that has not said which links face
// outside (novox/hq ADR 0140). Three machines sat silent waiting for a push that was waiting for them.
//
// A refused report says nothing about the machine, so it is not news; the refusal is said on the
// console where a person reads it.
func worthSaying(report link.Report) bool {
if report.Refused != "" {
return false
}
return len(report.Held) > 0 || report.Firewall != "" || len(report.Outward) > 0
}
// applyDeclared applies a declaration that has already been proved to come from the mesh. // applyDeclared applies a declaration that has already been proved to come from the mesh.
// //
// Signature checking happens before this is called, in the link. By the time anything here runs, // Signature checking happens before this is called, in the link. By the time anything here runs,
+35
View File
@@ -533,3 +533,38 @@ func TestANewOutwardLinkIsSaidUnasked(t *testing.T) {
t.Fatal("a changed outward link is not said, so the filter stays written around the old one") t.Fatal("a changed outward link is not said, so the filter stays written around the old one")
} }
} }
// **A converged machine can say which links face outside, unasked.**
//
// A reconcile is otherwise silent, and the condition deciding when it speaks asked only what an
// adopted node reports — what it holds, and the firewall it found. A converged node has neither, so
// it could speak only in reply to a declaration, and the mesh composes no declaration for a node
// that has not said which links face outside (novox/hq ADR 0140). Measured: three converged machines
// sat silent while the control plane refused to send them a filter.
func TestAConvergedMachineSaysItsOutwardLinksUnasked(t *testing.T) {
// A converged node's reconcile: nothing held, no found firewall, and the links it can see.
converged := link.Report{Outward: []string{"eth0"}}
if !worthSaying(converged) {
t.Fatal("a converged machine cannot say which links face outside, so it can never be " +
"sent a filter — a machine waiting for a push that is waiting for the machine")
}
// An adopted node's reasons still hold, because that is how a predecessor still writing is caught.
if !worthSaying(link.Report{Firewall: "ufw"}) {
t.Fatal("an adopted machine no longer says which firewall it found")
}
if !worthSaying(link.Report{Held: []link.Held{{ID: "a-file"}}}) {
t.Fatal("an adopted machine no longer says what it holds")
}
// And a reconcile with nothing to say stays silent, or every machine speaks every five minutes
// about nothing.
if worthSaying(link.Report{}) {
t.Fatal("a reconcile with nothing to say speaks anyway")
}
// A refused report says nothing about the machine; the refusal is for the console.
if worthSaying(link.Report{Outward: []string{"eth0"}, Refused: "not for this node"}) {
t.Fatal("a refused report is offered as news about the machine")
}
}