From c7ff0b9026e5acc7a2d05879c56eec73e190b702 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 22 Sep 2026 19:58:37 +0200 Subject: [PATCH] Refuse an endpoint whose port is not the one the private network's hub binds (hq ADR 0100) --- internal/bootstrap/phase2.go | 29 +++++++++++++++++++++++++++++ internal/bootstrap/phase2_test.go | 23 ++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/internal/bootstrap/phase2.go b/internal/bootstrap/phase2.go index ee2319e..ede6305 100644 --- a/internal/bootstrap/phase2.go +++ b/internal/bootstrap/phase2.go @@ -3,6 +3,7 @@ package bootstrap import ( "context" "encoding/json" + "errors" "fmt" "net" "strconv" @@ -133,6 +134,10 @@ func PlaceOnTheNetwork(ctx context.Context, o Options, control controlPlane, return fmt.Errorf("the private network needs an endpoint other machines can dial, and " + "nothing said one: pass --endpoint, or --broker-address so one can be derived") } + endpoint, err = endpointAgrees(endpoint, o.Ports.orDefaults().Hub) + if err != nil { + return err + } if _, err := control.tell(ctx, "assign", o.Node, module); err != nil { return err @@ -210,6 +215,30 @@ func builds(manifest []byte) bool { return m.Build != nil && len(m.Build.Artifacts) > 0 } +// endpointAgrees holds the endpoint other machines dial to the port this node gave the private +// network's hub (novox/hq ADR 0100): the hub binds what --hub-port says, so an endpoint naming +// another port is an address nothing answers on. A host alone takes the hub's port. +func endpointAgrees(endpoint string, hub int) (string, error) { + _, portText, err := net.SplitHostPort(endpoint) + var missing *net.AddrError + if errors.As(err, &missing) && missing.Err == "missing port in address" { + return net.JoinHostPort(strings.Trim(endpoint, "[]"), strconv.Itoa(hub)), nil + } + if err != nil { + return "", fmt.Errorf("--endpoint %q is not host:port: %w", endpoint, err) + } + port, err := strconv.Atoi(portText) + if err != nil { + return "", fmt.Errorf("--endpoint %q does not end in a port", endpoint) + } + if port != hub { + return "", fmt.Errorf("--endpoint %s names port %d and the private network's hub binds %d "+ + "(--hub-port): other machines would dial a port nothing answers on. Give one port for the "+ + "hub; nothing was changed", endpoint, port, hub) + } + return endpoint, nil +} + // derivedEndpoint is the default place other machines dial for the private network: the same host // they already dial for the broker, on WireGuard's ordinary port. One fact, not two. func derivedEndpoint(brokerAddress string, hub int) string { diff --git a/internal/bootstrap/phase2_test.go b/internal/bootstrap/phase2_test.go index 918a553..b1b46f3 100644 --- a/internal/bootstrap/phase2_test.go +++ b/internal/bootstrap/phase2_test.go @@ -1,6 +1,9 @@ package bootstrap -import "testing" +import ( + "strings" + "testing" +) // The endpoint other machines dial defaults to the host they already dial — the broker's — on // WireGuard's port. One fact, not two that drift. @@ -23,3 +26,21 @@ func TestOnlyAManifestWithArtifactsBuilds(t *testing.T) { t.Fatal("a manifest with nothing to build was built anyway") } } + +// Defends novox/hq ADR 0100: the hub's port is the node's, and the endpoint other machines dial +// must name it — an endpoint on another port is an address nothing answers on. +func TestTheEndpointAgreesWithTheHubsPort(t *testing.T) { + if got, err := endpointAgrees("192.0.2.10:51820", 51820); err != nil || got != "192.0.2.10:51820" { + t.Errorf("an endpoint on the hub's port: %q %v", got, err) + } + if got, err := endpointAgrees("192.0.2.10", 51821); err != nil || got != "192.0.2.10:51821" { + t.Errorf("a host alone did not take the hub's port: %q %v", got, err) + } + _, err := endpointAgrees("192.0.2.10:51820", 51821) + if err == nil || !strings.Contains(err.Error(), "--hub-port") { + t.Errorf("two ports for one hub were accepted: %v", err) + } + if _, err := endpointAgrees("192.0.2.10:not-a-port", 51820); err == nil { + t.Error("an endpoint whose port is not a number was accepted") + } +}