From 6486a24a9d4c1fd15e3c2ac97de185169b9c7dc8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 13:12:28 +0000 Subject: [PATCH 1/2] fix(config): skip port-mapping check for host-networked containers in --validate-env Closes #14053 Root cause: run_containerized.sh's validate_port_mapping already skips port-mapping validation when a container's HostConfig.NetworkMode is "host" (fixed in #7684 for #7647), but the Go validator that run_containerized.sh now delegates to via --validate-env (ValidateContainerizedEnvironmentForRuntime) never got the same exception. Docker discards published port mappings for --network host containers, so NetworkSettings.Ports is always empty and CheckPortMapping always fails, regressing #7647 for anyone running the gateway with --validate-env under host networking. Fix: add sys.IsHostNetworkMode(containerID), which inspects HostConfig.NetworkMode the same way run_containerized.sh's bash fix does, and skip the NetworkSettings.Ports check in ValidateContainerizedEnvironmentForRuntime when host networking is detected, treating the port as satisfied. Bridge-networked containers are unaffected. Trade-offs: matches the existing bash-side exception exactly, so no new security surface; a failure to determine the network mode falls back to running the original port-mapping check with a warning instead of erroring outright. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- internal/config/validation_env.go | 33 +++++++++++++++++------- internal/sys/docker.go | 15 +++++++++++ internal/sys/docker_test.go | 43 +++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 10 deletions(-) diff --git a/internal/config/validation_env.go b/internal/config/validation_env.go index 49ac3dcca..c6f8b6f01 100644 --- a/internal/config/validation_env.go +++ b/internal/config/validation_env.go @@ -129,20 +129,33 @@ func ValidateContainerizedEnvironmentForRuntime(containerID, runtimeCommand stri return result } - // Validate port mapping + // Validate port mapping. Host-networked containers (--network host) do not have + // published port mappings in NetworkSettings.Ports -- Docker discards them -- + // so the check is skipped for them, matching run_containerized.sh's handling + // of the same case (see #7647). port := os.Getenv("MCP_GATEWAY_PORT") if port != "" { - logEnv.Printf("Checking port mapping: port=%s", port) - portMapped, err := sys.CheckPortMapping(containerID, port) - if err != nil { + hostNetwork, netErr := sys.IsHostNetworkMode(containerID) + if netErr != nil { result.ValidationWarnings = append(result.ValidationWarnings, - fmt.Sprintf("Could not verify port mapping: %v", err)) - } else if !portMapped { - result.ValidationErrors = append(result.ValidationErrors, - fmt.Sprintf("MCP_GATEWAY_PORT (%s) is not mapped to a host port. Use: -p :%s", port, port)) + fmt.Sprintf("Could not verify network mode: %v", netErr)) + } + if hostNetwork { + logEnv.Printf("Host network mode detected: skipping port mapping check for port=%s", port) + result.PortMapped = true + } else { + logEnv.Printf("Checking port mapping: port=%s", port) + portMapped, err := sys.CheckPortMapping(containerID, port) + if err != nil { + result.ValidationWarnings = append(result.ValidationWarnings, + fmt.Sprintf("Could not verify port mapping: %v", err)) + } else if !portMapped { + result.ValidationErrors = append(result.ValidationErrors, + fmt.Sprintf("MCP_GATEWAY_PORT (%s) is not mapped to a host port. Use: -p :%s", port, port)) + } + result.PortMapped = portMapped + logEnv.Printf("Port mapping result: mapped=%v", portMapped) } - result.PortMapped = portMapped - logEnv.Printf("Port mapping result: mapped=%v", portMapped) } // Check if stdin is interactive (requires -i flag) diff --git a/internal/sys/docker.go b/internal/sys/docker.go index fcdd00455..1f9dbb25c 100644 --- a/internal/sys/docker.go +++ b/internal/sys/docker.go @@ -102,6 +102,21 @@ func runDockerInspect(containerID, formatTemplate string) (string, error) { return result, nil } +// IsHostNetworkMode uses docker inspect to determine whether the container was +// started with `--network host`. Host-networked containers do not have published +// port mappings in NetworkSettings.Ports (Docker discards them), so port-mapping +// checks must be skipped for them. +func IsHostNetworkMode(containerID string) (bool, error) { + output, err := runDockerInspect(containerID, "{{.HostConfig.NetworkMode}}") + if err != nil { + return false, err + } + + hostNetwork := output == "host" + logDocker.Printf("Network mode check: containerID=%s, networkMode=%s, host=%v", containerID, output, hostNetwork) + return hostNetwork, nil +} + // CheckPortMapping uses docker inspect to verify that the specified port is mapped. func CheckPortMapping(containerID, port string) (bool, error) { logDocker.Printf("Checking port mapping: containerID=%s, port=%s", containerID, port) diff --git a/internal/sys/docker_test.go b/internal/sys/docker_test.go index 969bc746a..04339042e 100644 --- a/internal/sys/docker_test.go +++ b/internal/sys/docker_test.go @@ -291,6 +291,49 @@ func TestCheckPortMapping(t *testing.T) { } } +func TestIsHostNetworkMode(t *testing.T) { + tests := []struct { + name string + containerID string + port string + shouldError bool + dockerHost string // if set, DOCKER_HOST is overridden for this subtest + }{ + { + name: "empty container ID", + containerID: "", + shouldError: true, + }, + { + name: "invalid container ID", + containerID: "invalid;id", + shouldError: true, + }, + { + name: "valid container ID format - container absent", + containerID: "abc123def4567890", + shouldError: true, // docker inspect will fail: socket doesn't exist + dockerHost: "unix:///nonexistent/docker.sock", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.dockerHost != "" { + t.Setenv("DOCKER_HOST", tt.dockerHost) + } + hostNetwork, err := IsHostNetworkMode(tt.containerID) + + if tt.shouldError { + require.Error(t, err, "Expected error for %s", tt.name) + assert.False(t, hostNetwork, "Should not be reported as host network on error") + } else { + require.NoError(t, err, "Unexpected error") + } + }) + } +} + func TestCheckStdinInteractive(t *testing.T) { tests := []struct { name string From 0440e98767378531f6758b712227ce2adbc9f302 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 28 Sep 2026 20:11:24 +0000 Subject: [PATCH 2/2] test(config): cover host-network port validation Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- internal/config/validation_env_test.go | 85 ++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/internal/config/validation_env_test.go b/internal/config/validation_env_test.go index 6b484f542..272cc4f11 100644 --- a/internal/config/validation_env_test.go +++ b/internal/config/validation_env_test.go @@ -591,3 +591,88 @@ func TestValidateContainerizedEnvironmentForRuntime_PodmanSkipsDockerInspection( assert.False(t, result.StdinInteractive) assert.False(t, result.LogDirMounted) } + +func TestValidateContainerizedEnvironmentForRuntime_NetworkModePortMapping(t *testing.T) { + tests := []struct { + name string + networkMode string + failNetworkModeCheck bool + wantPortMapped bool + wantPortMappingError bool + wantNetworkWarning bool + wantValid bool + }{ + { + name: "host network skips port mapping check", + networkMode: "host", + wantPortMapped: true, + wantValid: true, + }, + { + name: "bridge network still requires port mapping", + networkMode: "bridge", + wantPortMappingError: true, + }, + { + name: "network mode inspection failure falls back to port mapping", + failNetworkModeCheck: true, + wantPortMappingError: true, + wantNetworkWarning: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + dockerPath := filepath.Join(tempDir, "docker") + dockerScript := `#!/bin/sh +if [ "$1" = "info" ]; then + exit 0 +fi +if [ "$1" = "inspect" ]; then + case "$3" in + "{{.HostConfig.NetworkMode}}") + if [ "$MOCK_FAIL_NETWORK_MODE_CHECK" = "true" ]; then + exit 1 + fi + printf '%s\n' "$MOCK_NETWORK_MODE" + ;; + "{{json .NetworkSettings.Ports}}") + printf '{}\n' + ;; + "{{.Config.OpenStdin}}") + printf 'true\n' + ;; + "{{json .Mounts}}") + printf '[]\n' + ;; + esac + exit 0 +fi +exit 1 +` + require.NoError(t, os.WriteFile(dockerPath, []byte(dockerScript), 0o755)) + dockerHost := filepath.Join(tempDir, "docker.sock") + require.NoError(t, os.WriteFile(dockerHost, nil, 0o600)) + + t.Setenv("PATH", tempDir) + t.Setenv("DOCKER_HOST", dockerHost) + t.Setenv("MCP_GATEWAY_PORT", "8080") + t.Setenv("MCP_GATEWAY_DOMAIN", "localhost") + t.Setenv("MCP_GATEWAY_AGENT_ID", "test-agent") + t.Setenv("MOCK_NETWORK_MODE", tt.networkMode) + if tt.failNetworkModeCheck { + t.Setenv("MOCK_FAIL_NETWORK_MODE_CHECK", "true") + } else { + t.Setenv("MOCK_FAIL_NETWORK_MODE_CHECK", "false") + } + + result := ValidateContainerizedEnvironmentForRuntime("abcdef123456", "docker") + + assert.Equal(t, tt.wantPortMapped, result.PortMapped) + assert.Equal(t, tt.wantPortMappingError, strings.Contains(strings.Join(result.ValidationErrors, "\n"), "is not mapped to a host port")) + assert.Equal(t, tt.wantNetworkWarning, strings.Contains(strings.Join(result.ValidationWarnings, "\n"), "Could not verify network mode")) + assert.Equal(t, tt.wantValid, result.IsValid()) + }) + } +}