From 511c9a8dbb6a4100fc53182aadc99af33ecbef01 Mon Sep 17 00:00:00 2001 From: GFedor481 Date: Fri, 30 Oct 2026 14:38:11 -0400 Subject: [PATCH] fix(network): match native mappings to IPv4 publication --- docs/network-model.md | 16 ++++++++-------- network/doc.go | 2 +- network/native_loopback_test.go | 33 +++++++++++++++++++++++++++++++++ network/runtime_manager.go | 8 ++++++-- network/runtime_manager_test.go | 4 ++-- 5 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 network/native_loopback_test.go diff --git a/docs/network-model.md b/docs/network-model.md index 9aaf355aa..48f9965a8 100644 --- a/docs/network-model.md +++ b/docs/network-model.md @@ -6,7 +6,7 @@ Connection strings change across environments: | Environment | Postgres address | |---|---| -| Local (native) | `localhost:5432` | +| Local (native) | `127.0.0.1:5432` | | Local (Docker) | `host.docker.internal:5432` | | k8s (same namespace) | `postgres.default.svc:5432` | | Production | `db.prod.internal:5432` | @@ -21,7 +21,7 @@ Every endpoint gets a **NetworkMapping** containing multiple **NetworkInstances* mapping := &basev0.NetworkMapping{ Endpoint: grpcEndpoint, // the logical endpoint Instances: []*basev0.NetworkInstance{ - Native(endpoint, port), // localhost:PORT + Native(endpoint, port), // 127.0.0.1:PORT Container(endpoint, port), // host.docker.internal:PORT Public(endpoint, port), // configurable hostname:PORT }, @@ -33,14 +33,14 @@ At runtime, the consumer filters for the access type matching its context: ```go access := resources.NewNativeNetworkAccess() // or Container, Public instance := resources.FilterNetworkInstance(ctx, mapping.Instances, access) -// instance.Address = "localhost:34523" +// instance.Address = "127.0.0.1:34523" ``` ### Access Types | Type | Hostname | When to use | |---|---|---| -| `native` | `localhost` | Process runs directly on host | +| `native` | `127.0.0.1` | Process runs directly on host | | `container` | `host.docker.internal` | Process runs in Docker, connecting to host | | `public` | configurable | Production, external access | @@ -106,7 +106,7 @@ configs := []*basev0.Configuration{ { Name: "connection", ConfigurationValues: []*basev0.ConfigurationValue{ - {Key: "url", Value: "postgresql://localhost:5432/mydb"}, + {Key: "url", Value: "postgresql://127.0.0.1:5432/mydb"}, {Key: "password", Value: "secret", Secret: true}, }, }, @@ -120,7 +120,7 @@ configs := []*basev0.Configuration{ The CLI resolves the dependency graph, collects configs from all upstream services, and injects them as environment variables: ``` -CODEFLY__SERVICE_CONFIGURATION__STORE__POSTGRES__CONNECTION__URL=postgresql://localhost:5432/mydb +CODEFLY__SERVICE_CONFIGURATION__STORE__POSTGRES__CONNECTION__URL=postgresql://127.0.0.1:5432/mydb CODEFLY__SERVICE_SECRET_CONFIGURATION__STORE__POSTGRES__CONNECTION__PASSWORD=secret ``` @@ -534,10 +534,10 @@ When the CLI starts: 1. Starts `postgres` agent → Docker container on deterministic port (e.g., 23450) 2. Generates NetworkMapping: - - Native: `localhost:23450` + - Native: `127.0.0.1:23450` - Container: `host.docker.internal:23450` 3. Starts `api-server` agent, passes postgres NetworkMapping -4. Sets env var: `CODEFLY__SERVICE_CONFIGURATION__STORE__POSTGRES__CONNECTION__URL=postgresql://localhost:23450/postgres` +4. Sets env var: `CODEFLY__SERVICE_CONFIGURATION__STORE__POSTGRES__CONNECTION__URL=postgresql://127.0.0.1:23450/postgres` 5. API server reads env var, connects to postgres If the API server runs in Docker instead of natively, the CLI detects `CODEFLY__RUNTIME_CONTEXT=container` and the env var becomes: diff --git a/network/doc.go b/network/doc.go index 315a946bc..db28ee149 100644 --- a/network/doc.go +++ b/network/doc.go @@ -12,7 +12,7 @@ // the CLI's remotenetwork package. // // Three NetworkAccess types describe how a peer reaches an instance: -// Native (localhost), Container (host.docker.internal), and Public +// Native (127.0.0.1), Container (host.docker.internal), and Public // (load balancer / ingress). Callers filter mappings by access type // to pick the right address for their context. // diff --git a/network/native_loopback_test.go b/network/native_loopback_test.go new file mode 100644 index 000000000..2817e86a7 --- /dev/null +++ b/network/native_loopback_test.go @@ -0,0 +1,33 @@ +package network_test + +import ( + "net" + "testing" + + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "github.com/codefly-dev/core/network" + "github.com/codefly-dev/core/standards" + "github.com/stretchr/testify/require" +) + +// Native mappings must use the same address family as the IPv4 loopback +// publication used by local containers. A hostname can resolve to IPv6, where +// Docker Desktop can reset the connection instead of allowing IPv4 fallback. +func TestNativeMappingMatchesIPv4LoopbackPublication(t *testing.T) { + listener, err := net.Listen("tcp4", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, listener.Close()) }) + port := uint16(listener.Addr().(*net.TCPAddr).Port) + for _, api := range []string{standards.TCP, standards.GRPC, standards.HTTP, standards.CONNECT} { + t.Run(api, func(t *testing.T) { + instance := network.Native(&basev0.Endpoint{Api: api}, port) + ip := net.ParseIP(instance.Hostname) + require.NotNil(t, ip, "native publication must not depend on hostname address-family selection") + require.NotNil(t, ip.To4()) + require.True(t, ip.IsLoopback()) + connection, err := net.Dial("tcp", instance.Host) + require.NoError(t, err) + require.NoError(t, connection.Close()) + }) + } +} diff --git a/network/runtime_manager.go b/network/runtime_manager.go index c7e1367e3..31cd4b2a3 100644 --- a/network/runtime_manager.go +++ b/network/runtime_manager.go @@ -18,6 +18,10 @@ import ( const Localhost = "localhost" +// NativeLoopbackHost matches IPv4-only local container publication. Using a +// hostname can select IPv6 and reach a different listener or a resetting proxy. +const NativeLoopbackHost = "127.0.0.1" + // RuntimeManager tracks per-port allocation across the entire // service graph. The CLI fan-allocates ports for parallel services, // so concurrent GenerateNetworkMappings / AllocateTemporaryPort calls share @@ -51,7 +55,7 @@ func Container(endpoint *basev0.Endpoint, port uint16) *basev0.NetworkInstance { } func Native(endpoint *basev0.Endpoint, port uint16) *basev0.NetworkInstance { - host := Localhost + host := NativeLoopbackHost var instance *basev0.NetworkInstance if standards.IsHTTPBasedAPI(endpoint.Api) { instance = resources.NewHTTPNetworkInstance(host, port, resources.EndpointSecured(endpoint)) @@ -62,7 +66,7 @@ func Native(endpoint *basev0.Endpoint, port uint16) *basev0.NetworkInstance { return instance } -// NativeFor computes the deterministic native (localhost) NetworkInstance an +// NativeFor computes the deterministic native loopback NetworkInstance an // endpoint binds to under a normal `codefly run` — WITHOUT a running flow. // // It mirrors RuntimeManager.GenerateNetworkMappings' Native() path exactly diff --git a/network/runtime_manager_test.go b/network/runtime_manager_test.go index a589e6dd8..c36559377 100644 --- a/network/runtime_manager_test.go +++ b/network/runtime_manager_test.go @@ -30,7 +30,7 @@ func TestConnectNetworkInstancesUseHTTPAddresses(t *testing.T) { } { require.Contains(t, instance.Address, "://", name) } - require.Equal(t, "http://localhost:8081", network.Native(endpoint, 8081).Address) + require.Equal(t, "http://127.0.0.1:8081", network.Native(endpoint, 8081).Address) require.Equal( t, "https://accounts.example.test:443", @@ -50,7 +50,7 @@ func TestDeclaredTLSEndpointsUseHTTPSMappings(t *testing.T) { }}, } - require.Equal(t, "https://localhost:8443", network.Native(endpoint, 8443).Address) + require.Equal(t, "https://127.0.0.1:8443", network.Native(endpoint, 8443).Address) require.Equal(t, "https://host.docker.internal:8443", network.Container(endpoint, 8443).Address) require.Equal(t, "https://localhost:8443", network.PublicDefault(endpoint, 8443).Address) }