diff --git a/cmd/mesh-host/main.go b/cmd/mesh-host/main.go index c10562e..183755d 100644 --- a/cmd/mesh-host/main.go +++ b/cmd/mesh-host/main.go @@ -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 // its firewall changed, because that is how a predecessor still writing is caught // (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) } 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. // // Signature checking happens before this is called, in the link. By the time anything here runs, diff --git a/cmd/mesh-host/main_test.go b/cmd/mesh-host/main_test.go index b4e84bc..d5a27d1 100644 --- a/cmd/mesh-host/main_test.go +++ b/cmd/mesh-host/main_test.go @@ -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") } } + +// **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") + } +}