diff --git a/pkg/compose/relay.go b/pkg/compose/relay.go index 645763633..d2f851d87 100644 --- a/pkg/compose/relay.go +++ b/pkg/compose/relay.go @@ -21,6 +21,7 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "net" "os" "sort" "strings" @@ -50,6 +51,8 @@ func relayImage() string { // relayRoutesSpec renders endpoints as the relay's RELAY_ROUTES value, // canonically ordered so it doubles as the identity the relay label hashes. +// Upstreams are rewritten for the relay's vantage point (relayUpstream) +// before rendering, so the identity follows what the relay actually dials. func relayRoutesSpec(endpoints map[int]string) string { ports := make([]int, 0, len(endpoints)) for port := range endpoints { @@ -58,11 +61,35 @@ func relayRoutesSpec(endpoints map[int]string) string { sort.Ints(ports) routes := make([]string, 0, len(ports)) for _, port := range ports { - routes = append(routes, fmt.Sprintf("%d=%s", port, endpoints[port])) + routes = append(routes, fmt.Sprintf("%d=%s", port, relayUpstream(endpoints[port]))) } return strings.Join(routes, ",") } +// relayUpstream rewrites a host-relative upstream for the relay's vantage +// point. Providers express endpoints from the host's perspective — a loopback +// or unspecified address names the machine compose runs on — but the relay +// dials from its own network namespace, where those addresses name the relay +// container itself. host.docker.internal resolves natively on Docker Desktop +// and is provisioned through ExtraHosts (host-gateway) on plain Linux +// engines. Anything else (a LAN IP, a DNS name) is reachable as-is from the +// relay and passes verbatim. +func relayUpstream(endpoint string) string { + host, port, err := net.SplitHostPort(endpoint) + if err != nil { + // validated at parse time (parseEndpointMessage); keep verbatim + return endpoint + } + hostRelative := host == "" || strings.EqualFold(host, "localhost") + if ip := net.ParseIP(host); ip != nil && (ip.IsLoopback() || ip.IsUnspecified()) { + hostRelative = true + } + if !hostRelative { + return endpoint + } + return net.JoinHostPort("host.docker.internal", port) +} + func relayIdentity(routes string) string { digest := sha256.Sum256([]byte(relayImage() + "|" + routes)) return hex.EncodeToString(digest[:])[:12] diff --git a/pkg/compose/relay_test.go b/pkg/compose/relay_test.go index 55767b22d..3b646afe6 100644 --- a/pkg/compose/relay_test.go +++ b/pkg/compose/relay_test.go @@ -67,6 +67,35 @@ func TestRelayRoutesSpec(t *testing.T) { assert.Assert(t, id1 != relayIdentity("80=host.docker.internal:49153")) } +// Providers express endpoints from the host's perspective, the relay dials +// from a container: host-relative addresses must be rewritten to +// host.docker.internal, everything else passes verbatim. +func TestRelayUpstream(t *testing.T) { + for endpoint, want := range map[string]string{ + "localhost:5734": "host.docker.internal:5734", + "LOCALHOST:5734": "host.docker.internal:5734", + "127.0.0.1:5734": "host.docker.internal:5734", + "127.1.2.3:5734": "host.docker.internal:5734", + "[::1]:5734": "host.docker.internal:5734", + "0.0.0.0:5734": "host.docker.internal:5734", + "[::]:5734": "host.docker.internal:5734", + ":5734": "host.docker.internal:5734", + "192.168.1.10:5734": "192.168.1.10:5734", + "[fdcb::2]:5734": "[fdcb::2]:5734", + "some.host.corp:5734": "some.host.corp:5734", + "host.docker.internal:5734": "host.docker.internal:5734", + } { + assert.Equal(t, relayUpstream(endpoint), want, "endpoint %q", endpoint) + } +} + +// The rewrite happens inside relayRoutesSpec, so the relay identity hashes +// what the relay actually dials. +func TestRelayRoutesSpecRewritesHostRelativeUpstreams(t *testing.T) { + routes := relayRoutesSpec(map[int]string{80: "localhost:5734"}) + assert.Equal(t, routes, "80=host.docker.internal:5734") +} + // The relay joins the networks of the services depending on the provider — // its consumers — and falls back to the project default network. func TestRelayNetworks(t *testing.T) {