From f0634e11f474e22219695bc6db33448055847471 Mon Sep 17 00:00:00 2001 From: jochens Date: Wed, 30 Sep 2026 16:58:23 +0200 Subject: [PATCH] A short-form port keeps its protocol and still gets its machine port "3478/udp" read as one token was not a port, so it passed through and the runtime published it wherever it liked: on ace, unifi's STUN and discovery landed on random machine ports while every TCP pin beside them held. The protocol is split off, the number is assigned as for any short form, and the suffix rides along on the outside. --- internal/catalogue/declaration.go | 13 ++++++++-- .../short_form_port_protocol_test.go | 24 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 internal/catalogue/short_form_port_protocol_test.go diff --git a/internal/catalogue/declaration.go b/internal/catalogue/declaration.go index 5af2bb8..e361d84 100644 --- a/internal/catalogue/declaration.go +++ b/internal/catalogue/declaration.go @@ -1576,14 +1576,23 @@ func publishedOn(resource map[string]any, module string, with Rendering) { out = append(out, givenOuter(written, with.Given[module])) continue } - wanted, err := strconv.Atoi(strings.TrimSpace(written)) + // A short form may carry the protocol — `"3478/udp"` — and the number is what the mesh + // assigns for; the protocol rides along. Read as one token, the `/udp` made the whole + // entry "not a port", and passing it through let the runtime publish it wherever it + // liked: unifi's STUN and discovery landed on random machine ports while every TCP pin + // beside them held. + mapping, protocol := written, "" + if cut := strings.LastIndex(written, "/"); cut >= 0 { + mapping, protocol = written[:cut], written[cut:] + } + wanted, err := strconv.Atoi(strings.TrimSpace(mapping)) if err != nil { // Not a port at all. Passed through, so the host refuses it with its own words rather // than this quietly dropping something somebody meant. out = append(out, written) continue } - out = append(out, fmt.Sprintf("%d:%d", with.machinePort(module, wanted), wanted)) + out = append(out, fmt.Sprintf("%d:%d%s", with.machinePort(module, wanted), wanted, protocol)) } resource["ports"] = out } diff --git a/internal/catalogue/short_form_port_protocol_test.go b/internal/catalogue/short_form_port_protocol_test.go new file mode 100644 index 0000000..625227e --- /dev/null +++ b/internal/catalogue/short_form_port_protocol_test.go @@ -0,0 +1,24 @@ +package catalogue + +import ( + "fmt" + "testing" +) + +// A short-form port may name its protocol — "3478/udp" — and the mesh assigns the number exactly +// as it does for "3478": the protocol rides along on the outside. Read as one token, the suffix made +// the entry "not a port" and the runtime published it on a random machine port (found on ace: unifi's +// STUN and discovery, while every TCP pin beside them held). +func TestAShortFormPortKeepsItsProtocolAndGetsItsMachinePort(t *testing.T) { + container := map[string]any{"type": "container", "ports": []any{"3478/udp", "8443", "10001/udp"}} + with := Rendering{ + Given: map[string]map[int]int{"unifi": {3478: 3478}}, + Ports: map[string]map[int]int{"unifi": {8443: 20010, 10001: 20011}}, + } + publishedOn(container, "unifi", with) + got := fmt.Sprint(container["ports"]) + want := "[3478:3478/udp 20010:8443 20011:10001/udp]" + if got != want { + t.Fatalf("published %s, want %s", got, want) + } +}