diff --git a/cmd/mesh-controller/foundation_scope_test.go b/cmd/mesh-controller/foundation_scope_test.go new file mode 100644 index 0000000..9ff8591 --- /dev/null +++ b/cmd/mesh-controller/foundation_scope_test.go @@ -0,0 +1,33 @@ +package main + +// The broker opening belongs only on the node that listens on it (novox/hq: it leaked onto +// every enrolled node's declaration, opening a from-anywhere hole for a port nothing there +// serves). foundationPortsFor is the scope. + +import ( + "testing" + + "github.com/novox/mesh-controller/internal/catalogue" +) + +func TestTheBrokerHostGetsTheFoundationOpening(t *testing.T) { + broker := catalogue.Manifest{Module: "lavinmq", Listens: []catalogue.Listening{ + {Port: 5671, Protocol: "tcp", From: "mesh"}, + {Port: 5672, Protocol: "tcp", From: "mesh"}, + }} + got := foundationPortsFor(5671, []catalogue.Manifest{broker}) + if len(got) != 1 || got[0] != 5671 { + t.Fatalf("the node that listens on the broker port keeps it; got %v", got) + } +} + +func TestANodeThatOnlyDialsTheBrokerGetsNoOpening(t *testing.T) { + // ace's set: things that reach the broker as a client, none listening on 5671. + ace := []catalogue.Manifest{ + {Module: "plex", Listens: []catalogue.Listening{{Port: 32400, Protocol: "tcp", From: "anywhere"}}}, + {Module: "postgres", Listens: []catalogue.Listening{{Port: 5432, Protocol: "tcp", From: "mesh"}}}, + } + if got := foundationPortsFor(5671, ace); got != nil { + t.Fatalf("a node that only dials out opens nothing for the broker; got %v", got) + } +} diff --git a/cmd/mesh-controller/plan.go b/cmd/mesh-controller/plan.go index e744e2d..6927def 100644 --- a/cmd/mesh-controller/plan.go +++ b/cmd/mesh-controller/plan.go @@ -534,11 +534,19 @@ func renderingFor(ctx context.Context, open *stores, node string, // The ports the mesh itself needs open, which no module declares. Read from the broker this // control plane was told about rather than written down twice: the address a node is handed in // its token and the port its machine must accept on are the same fact. + // + // **Only on the node that listens on it** (novox/hq issue: the broker opening leaked onto + // every node). The opening exists to WIDEN the broker's port to from-anywhere — a machine + // enrolling is not on the mesh yet, so the broker's own `from: mesh` listen would refuse its + // first dial. That widening belongs on the broker's host and nowhere else: a node that only + // dials out needs no incoming rule, and an opening for a port nothing here listens on is a + // from-anywhere hole for a dead port. So the foundation port is kept only when a module + // resolved onto THIS node actually listens on it. var foundation []int if b, err := broker.FromEnvironment(); err == nil { if _, port, err := net.SplitHostPort(b.Address); err == nil { if n, err := strconv.Atoi(port); err == nil { - foundation = append(foundation, n) + foundation = foundationPortsFor(n, plan.Modules) } } } @@ -1135,3 +1143,20 @@ func portsOn( } return out, nil } + +// foundationPortsFor is the broker port, kept only when a module resolved onto this node listens +// on it (novox/hq issue: the broker opening leaked onto every node). The foundation opening +// exists to WIDEN the broker's `from: mesh` port to from-anywhere, because a machine enrolling is +// not on the mesh yet and its first dial would be refused. That widening belongs on the broker's +// host alone: a node that only dials out needs no incoming rule, and an opening for a port +// nothing here listens on is a from-anywhere hole for a dead port. +func foundationPortsFor(brokerPort int, modules []catalogue.Manifest) []int { + for _, m := range modules { + for _, l := range m.Listens { + if l.Port == brokerPort { + return []int{brokerPort} + } + } + } + return nil +}