From 1b335de07dcecf6ebbc6669232fe1345d3513f61 Mon Sep 17 00:00:00 2001 From: engels74 <141435164+engels74@users.noreply.github.com> Date: Sun, 21 Dec 2025 13:41:31 +0100 Subject: [PATCH 1/2] feat(docker): improve container network mode handling This commit enhances the handling of Docker's `container:` network mode by ensuring compatibility with shared network namespaces: - **Adding `IsContainerNetworkMode` method:** A new boolean method to easily check if the network mode is `container:` - **Conditional hostname/domainname setting:** When `container:` network mode is detected, hostname and domainname are not explicitly set, preventing Docker API errors since these settings are inherited from the target container - **Conditional DNS setting:** DNS settings are only applied if not in `container:` network mode, as they are inherited in this configuration - **Conditional port configuration:** `ExposedPorts` and `PortBindings` are only configured when not in `container:` network mode, since port exposure should be handled on the target container - **ForceOutgoingIP warning:** A warning is logged if `ForceOutgoingIP` is enabled while in `container:` network mode, as this setting is incompatible and will be ignored These changes ensure robust and correct behavior when using shared network namespaces with Docker containers, preventing API errors and configuration conflicts. --- config/config_docker.go | 8 +++++ config/config_docker_test.go | 32 +++++++++++++++++++ environment/docker/container.go | 55 ++++++++++++++++++++++++++++----- server/install.go | 24 ++++++++++++-- 4 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 config/config_docker_test.go diff --git a/config/config_docker.go b/config/config_docker.go index f3e846b0..52f33c33 100644 --- a/config/config_docker.go +++ b/config/config_docker.go @@ -3,6 +3,7 @@ package config import ( "encoding/base64" "sort" + "strings" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/registry" @@ -42,6 +43,13 @@ type DockerNetworkConfiguration struct { Interfaces dockerNetworkInterfaces `yaml:"interfaces"` } +// IsContainerNetworkMode returns true if the network mode shares another container's network namespace. +// When using "container:" mode, the container inherits the target container's network stack, +// including hostname, DNS, and network interfaces. +func (c DockerNetworkConfiguration) IsContainerNetworkMode() bool { + return strings.HasPrefix(c.Mode, "container:") +} + // DockerConfiguration defines the docker configuration used by the daemon when // interacting with containers and networks on the system. type DockerConfiguration struct { diff --git a/config/config_docker_test.go b/config/config_docker_test.go new file mode 100644 index 00000000..d6fc618c --- /dev/null +++ b/config/config_docker_test.go @@ -0,0 +1,32 @@ +package config + +import "testing" + +// TestDockerNetworkConfiguration_IsContainerNetworkMode tests the IsContainerNetworkMode +// method to ensure it correctly identifies when the network mode is set to share another +// container's network namespace (i.e., "container:" format). +func TestDockerNetworkConfiguration_IsContainerNetworkMode(t *testing.T) { + tests := []struct { + name string + mode string + expected bool + }{ + {"container mode with name", "container:caddy", true}, + {"container mode with different name", "container:some-vpn-container", true}, + {"container mode empty name", "container:", true}, // Edge case: technically valid prefix + {"default pelican network", "pelican_nw", false}, + {"bridge network", "bridge", false}, + {"host network", "host", false}, + {"empty string", "", false}, + {"partial match", "containers", false}, // Should not match without colon + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := DockerNetworkConfiguration{Mode: tt.mode} + if got := c.IsContainerNetworkMode(); got != tt.expected { + t.Errorf("IsContainerNetworkMode() = %v, want %v for mode %q", got, tt.expected, tt.mode) + } + }) + } +} diff --git a/environment/docker/container.go b/environment/docker/container.go index 8284dc5b..8259f476 100644 --- a/environment/docker/container.go +++ b/environment/docker/container.go @@ -17,6 +17,7 @@ import ( "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" "github.com/pelican-dev/wings/config" "github.com/pelican-dev/wings/environment" @@ -177,15 +178,34 @@ func (e *Environment) Create() error { labels["Service"] = "Pelican" labels["ContainerType"] = "server_process" + // Only set hostname/domainname if not using container network mode. + // Containers sharing another container's network namespace inherit that container's + // hostname and domainname, so setting them would cause a Docker API error. + var hostname, domainname string + if !cfg.Docker.Network.IsContainerNetworkMode() { + hostname = e.Id + domainname = cfg.Docker.Domainname + } else { + e.log().WithField("network_mode", cfg.Docker.Network.Mode). + Debug("environment/docker: using container network mode, skipping hostname/domainname configuration") + } + + // Port exposure is not allowed when using container network mode since the network + // stack is inherited from the target container. Ports must be exposed on that container instead. + var exposedPorts nat.PortSet + if !cfg.Docker.Network.IsContainerNetworkMode() { + exposedPorts = a.Exposed() + } + conf := &container.Config{ - Hostname: e.Id, - Domainname: cfg.Docker.Domainname, + Hostname: hostname, + Domainname: domainname, AttachStdin: true, AttachStdout: true, AttachStderr: true, OpenStdin: true, Tty: true, - ExposedPorts: a.Exposed(), + ExposedPorts: exposedPorts, Image: strings.TrimPrefix(e.meta.Image, "~"), Env: e.Configuration.EnvironmentVariables(), Labels: labels, @@ -199,9 +219,14 @@ func (e *Environment) Create() error { } networkMode := container.NetworkMode(cfg.Docker.Network.Mode) + + // ForceOutgoingIP is incompatible with container network mode since the network + // stack is inherited from the target container. Skip this logic entirely. if a.ForceOutgoingIP { - // We can't use ForceOutgoingIP if we made a server with no allocation - if a.DefaultMapping.Port != 0 { + if cfg.Docker.Network.IsContainerNetworkMode() { + e.log().WithField("network_mode", cfg.Docker.Network.Mode). + Warn("environment/docker: ForceOutgoingIP is enabled but will be ignored when using container network mode") + } else if a.DefaultMapping.Port != 0 { enableIPv6 := false e.log().Debug("environment/docker: forcing outgoing IP address") networkName := "ip-" + strings.ReplaceAll(strings.ReplaceAll(a.DefaultMapping.Ip, ".", "-"), ":", "-") @@ -233,8 +258,24 @@ func (e *Environment) Create() error { } } + // DNS settings are inherited when using container network mode. + var dns []string + if !cfg.Docker.Network.IsContainerNetworkMode() { + dns = cfg.Docker.Network.Dns + } + + // Port bindings are not allowed when using container network mode since the network + // stack is inherited from the target container. Ports must be published on that container instead. + var portBindings nat.PortMap + if !cfg.Docker.Network.IsContainerNetworkMode() { + portBindings = a.DockerBindings() + } else { + e.log().WithField("network_mode", cfg.Docker.Network.Mode). + Debug("environment/docker: using container network mode, skipping port bindings configuration") + } + hostConf := &container.HostConfig{ - PortBindings: a.DockerBindings(), + PortBindings: portBindings, // Configure the mounts for this container. First mount the server data directory // into the container as an r/w bind. @@ -250,7 +291,7 @@ func (e *Environment) Create() error { // from the Panel. Resources: e.Configuration.Limits().AsContainerResources(), - DNS: cfg.Docker.Network.Dns, + DNS: dns, // Configure logging for the container to make it easier on the Daemon to grab // the server output. Ensure that we don't use too much space on the host machine diff --git a/server/install.go b/server/install.go index fc0a29de..3a56f166 100644 --- a/server/install.go +++ b/server/install.go @@ -415,8 +415,21 @@ func (ip *InstallationProcess) Execute() (string, error) { ctx, cancel := context.WithCancel(ip.Server.Context()) defer cancel() + // Get config first - must be available before container.Config struct + cfg := config.Get() + + // Only set hostname if not using container network mode. + // Containers sharing another container's network namespace inherit that container's hostname. + var hostname string + if !cfg.Docker.Network.IsContainerNetworkMode() { + hostname = "installer" + } else { + ip.Server.Log().WithField("network_mode", cfg.Docker.Network.Mode). + Debug("server/install: using container network mode, skipping hostname configuration") + } + conf := &container.Config{ - Hostname: "installer", + Hostname: hostname, AttachStdout: true, AttachStderr: true, AttachStdin: true, @@ -431,7 +444,12 @@ func (ip *InstallationProcess) Execute() (string, error) { }, } - cfg := config.Get() + // DNS settings are inherited when using container network mode. + var dns []string + if !cfg.Docker.Network.IsContainerNetworkMode() { + dns = cfg.Docker.Network.Dns + } + tmpfsSize := strconv.Itoa(int(cfg.Docker.TmpfsSize)) hostConf := &container.HostConfig{ Mounts: []mount.Mount{ @@ -452,7 +470,7 @@ func (ip *InstallationProcess) Execute() (string, error) { Tmpfs: map[string]string{ "/tmp": "rw,exec,nosuid,size=" + tmpfsSize + "M", }, - DNS: cfg.Docker.Network.Dns, + DNS: dns, LogConfig: cfg.Docker.ContainerLogConfig(), NetworkMode: container.NetworkMode(cfg.Docker.Network.Mode), UsernsMode: container.UsernsMode(cfg.Docker.UsernsMode), From 17ece6ecdd02983673392d2081a867e0e742ff04 Mon Sep 17 00:00:00 2001 From: engels74 <141435164+engels74@users.noreply.github.com> Date: Sun, 21 Dec 2025 14:29:38 +0100 Subject: [PATCH 2/2] fix(docker): correctly validate container network mode The previous implementation of `IsContainerNetworkMode` considered `container:` an empty but valid container network mode. However, Docker rejects "container:" without a name with "invalid container format container:". This commit updates the `IsContainerNetworkMode` function to ensure that there is at least one character for the container name after "container:", aligning the validation with Docker's behavior. --- config/config_docker.go | 4 +++- config/config_docker_test.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config/config_docker.go b/config/config_docker.go index 52f33c33..f0184985 100644 --- a/config/config_docker.go +++ b/config/config_docker.go @@ -47,7 +47,9 @@ type DockerNetworkConfiguration struct { // When using "container:" mode, the container inherits the target container's network stack, // including hostname, DNS, and network interfaces. func (c DockerNetworkConfiguration) IsContainerNetworkMode() bool { - return strings.HasPrefix(c.Mode, "container:") + // Must have "container:" prefix and at least one character for the container name. + // Docker rejects "container:" without a name with "invalid container format container:". + return strings.HasPrefix(c.Mode, "container:") && len(c.Mode) > len("container:") } // DockerConfiguration defines the docker configuration used by the daemon when diff --git a/config/config_docker_test.go b/config/config_docker_test.go index d6fc618c..c6e0040f 100644 --- a/config/config_docker_test.go +++ b/config/config_docker_test.go @@ -13,7 +13,7 @@ func TestDockerNetworkConfiguration_IsContainerNetworkMode(t *testing.T) { }{ {"container mode with name", "container:caddy", true}, {"container mode with different name", "container:some-vpn-container", true}, - {"container mode empty name", "container:", true}, // Edge case: technically valid prefix + {"container mode empty name", "container:", false}, // Docker rejects "container:" without a name {"default pelican network", "pelican_nw", false}, {"bridge network", "bridge", false}, {"host network", "host", false},