From bb85af1821bf0e53956f85abcdc79d16c1aaea44 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 29 Sep 2026 01:00:26 +0200 Subject: [PATCH] A machine says which links face outside without being asked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mesh composes no filter for a machine that has not said (ADR 0140), and a converged machine only speaks unasked when its adoption fingerprint changes. The links were not in that fingerprint, so a machine that had just learnt to say could only speak when a declaration arrived — and a declaration cannot be composed until it has spoken. A machine waiting for a push that is waiting for the machine. Found before it bit: every machine in this mesh is in exactly that state right now, having just been given a host that reports the links to a control plane that does not yet read them. --- cmd/mesh-host/main.go | 9 +++++++-- cmd/mesh-host/main_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/cmd/mesh-host/main.go b/cmd/mesh-host/main.go index 30d91ef..c10562e 100644 --- a/cmd/mesh-host/main.go +++ b/cmd/mesh-host/main.go @@ -1077,7 +1077,12 @@ type adoptionWatch struct { // is what the controller previews a flip from, so a port that opens or closes between deliveries // must reach it too (novox/hq ADR 0100). func adoptionFingerprint(r link.Report) string { - parts := []string{"firewall=" + r.Firewall} + // **Which links face outside is part of it, though it is not about adoption** (novox/hq ADR + // 0140). The mesh composes no filter for a machine that has not said, so a machine whose links + // changed — or which has only just learnt to say — has to say so without being asked. Left out, + // it could only speak when a declaration arrived, and a declaration cannot be composed until it + // has spoken: a machine waiting for a push that is waiting for the machine. + parts := []string{"firewall=" + r.Firewall, "outward=" + strings.Join(r.Outward, ",")} for _, h := range r.Held { parts = append(parts, "held "+h.ID+"="+h.Changed) } @@ -1085,7 +1090,7 @@ func adoptionFingerprint(r link.Report) string { parts = append(parts, fmt.Sprintf("reach %s %s:%d %s %v %d", reach.Protocol, reach.Address, reach.Port, reach.By, reach.Published, reach.ContainerPort)) } - sort.Strings(parts[1:]) + sort.Strings(parts[2:]) return strings.Join(parts, "\n") } diff --git a/cmd/mesh-host/main_test.go b/cmd/mesh-host/main_test.go index e8621b5..b4e84bc 100644 --- a/cmd/mesh-host/main_test.go +++ b/cmd/mesh-host/main_test.go @@ -501,3 +501,35 @@ func TestReconcileAfterAControllerDeclarationDoesNotReapplyTheBundle(t *testing. t.Errorf("with nothing said, reconcile did not reach for the carried bundle: %v", err) } } + +// **A machine says which links face outside without being asked.** +// +// The mesh composes no filter for a machine that has not said (novox/hq ADR 0140), and a machine only +// speaks unasked when this fingerprint changes. Left out of it, a machine that has just learnt to say +// could speak only when a declaration arrived — and a declaration cannot be composed until it has +// spoken. A machine waiting for a push that is waiting for the machine. +func TestANewOutwardLinkIsSaidUnasked(t *testing.T) { + w := &adoptionWatch{} + first := link.Report{Firewall: "none"} + if !w.differs(first) { + t.Fatal("the first report should differ from nothing") + } + w.said(first) + + // Only the links changed, and nothing about adoption. + learnt := link.Report{Firewall: "none", Outward: []string{"eth0"}} + if !w.differs(learnt) { + t.Fatal("a machine that has just learnt which links face outside would never say so, " + + "and could then never be sent a filter") + } + w.said(learnt) + if w.differs(link.Report{Firewall: "none", Outward: []string{"eth0"}}) { + t.Fatal("the same links are reported as a change, so the machine would speak on every reconcile") + } + + // And a link that changes — a laptop moving from a cable to a radio — is said too, because the + // filter is written around the old one until it is. + if !w.differs(link.Report{Firewall: "none", Outward: []string{"wlan0"}}) { + t.Fatal("a changed outward link is not said, so the filter stays written around the old one") + } +} -- 2.54.0