Skip to content

Commit 1b335de

Browse files
committed
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:<name>` - **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.
1 parent 29935d6 commit 1b335de

File tree

4 files changed

+109
-10
lines changed

4 files changed

+109
-10
lines changed

config/config_docker.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package config
33
import (
44
"encoding/base64"
55
"sort"
6+
"strings"
67

78
"github.com/docker/docker/api/types/container"
89
"github.com/docker/docker/api/types/registry"
@@ -42,6 +43,13 @@ type DockerNetworkConfiguration struct {
4243
Interfaces dockerNetworkInterfaces `yaml:"interfaces"`
4344
}
4445

46+
// IsContainerNetworkMode returns true if the network mode shares another container's network namespace.
47+
// When using "container:<name>" mode, the container inherits the target container's network stack,
48+
// including hostname, DNS, and network interfaces.
49+
func (c DockerNetworkConfiguration) IsContainerNetworkMode() bool {
50+
return strings.HasPrefix(c.Mode, "container:")
51+
}
52+
4553
// DockerConfiguration defines the docker configuration used by the daemon when
4654
// interacting with containers and networks on the system.
4755
type DockerConfiguration struct {

config/config_docker_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package config
2+
3+
import "testing"
4+
5+
// TestDockerNetworkConfiguration_IsContainerNetworkMode tests the IsContainerNetworkMode
6+
// method to ensure it correctly identifies when the network mode is set to share another
7+
// container's network namespace (i.e., "container:<name>" format).
8+
func TestDockerNetworkConfiguration_IsContainerNetworkMode(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
mode string
12+
expected bool
13+
}{
14+
{"container mode with name", "container:caddy", true},
15+
{"container mode with different name", "container:some-vpn-container", true},
16+
{"container mode empty name", "container:", true}, // Edge case: technically valid prefix
17+
{"default pelican network", "pelican_nw", false},
18+
{"bridge network", "bridge", false},
19+
{"host network", "host", false},
20+
{"empty string", "", false},
21+
{"partial match", "containers", false}, // Should not match without colon
22+
}
23+
24+
for _, tt := range tests {
25+
t.Run(tt.name, func(t *testing.T) {
26+
c := DockerNetworkConfiguration{Mode: tt.mode}
27+
if got := c.IsContainerNetworkMode(); got != tt.expected {
28+
t.Errorf("IsContainerNetworkMode() = %v, want %v for mode %q", got, tt.expected, tt.mode)
29+
}
30+
})
31+
}
32+
}

environment/docker/container.go

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/docker/docker/api/types/mount"
1818
"github.com/docker/docker/api/types/network"
1919
"github.com/docker/docker/client"
20+
"github.com/docker/go-connections/nat"
2021

2122
"github.com/pelican-dev/wings/config"
2223
"github.com/pelican-dev/wings/environment"
@@ -177,15 +178,34 @@ func (e *Environment) Create() error {
177178
labels["Service"] = "Pelican"
178179
labels["ContainerType"] = "server_process"
179180

181+
// Only set hostname/domainname if not using container network mode.
182+
// Containers sharing another container's network namespace inherit that container's
183+
// hostname and domainname, so setting them would cause a Docker API error.
184+
var hostname, domainname string
185+
if !cfg.Docker.Network.IsContainerNetworkMode() {
186+
hostname = e.Id
187+
domainname = cfg.Docker.Domainname
188+
} else {
189+
e.log().WithField("network_mode", cfg.Docker.Network.Mode).
190+
Debug("environment/docker: using container network mode, skipping hostname/domainname configuration")
191+
}
192+
193+
// Port exposure is not allowed when using container network mode since the network
194+
// stack is inherited from the target container. Ports must be exposed on that container instead.
195+
var exposedPorts nat.PortSet
196+
if !cfg.Docker.Network.IsContainerNetworkMode() {
197+
exposedPorts = a.Exposed()
198+
}
199+
180200
conf := &container.Config{
181-
Hostname: e.Id,
182-
Domainname: cfg.Docker.Domainname,
201+
Hostname: hostname,
202+
Domainname: domainname,
183203
AttachStdin: true,
184204
AttachStdout: true,
185205
AttachStderr: true,
186206
OpenStdin: true,
187207
Tty: true,
188-
ExposedPorts: a.Exposed(),
208+
ExposedPorts: exposedPorts,
189209
Image: strings.TrimPrefix(e.meta.Image, "~"),
190210
Env: e.Configuration.EnvironmentVariables(),
191211
Labels: labels,
@@ -199,9 +219,14 @@ func (e *Environment) Create() error {
199219
}
200220

201221
networkMode := container.NetworkMode(cfg.Docker.Network.Mode)
222+
223+
// ForceOutgoingIP is incompatible with container network mode since the network
224+
// stack is inherited from the target container. Skip this logic entirely.
202225
if a.ForceOutgoingIP {
203-
// We can't use ForceOutgoingIP if we made a server with no allocation
204-
if a.DefaultMapping.Port != 0 {
226+
if cfg.Docker.Network.IsContainerNetworkMode() {
227+
e.log().WithField("network_mode", cfg.Docker.Network.Mode).
228+
Warn("environment/docker: ForceOutgoingIP is enabled but will be ignored when using container network mode")
229+
} else if a.DefaultMapping.Port != 0 {
205230
enableIPv6 := false
206231
e.log().Debug("environment/docker: forcing outgoing IP address")
207232
networkName := "ip-" + strings.ReplaceAll(strings.ReplaceAll(a.DefaultMapping.Ip, ".", "-"), ":", "-")
@@ -233,8 +258,24 @@ func (e *Environment) Create() error {
233258
}
234259
}
235260

261+
// DNS settings are inherited when using container network mode.
262+
var dns []string
263+
if !cfg.Docker.Network.IsContainerNetworkMode() {
264+
dns = cfg.Docker.Network.Dns
265+
}
266+
267+
// Port bindings are not allowed when using container network mode since the network
268+
// stack is inherited from the target container. Ports must be published on that container instead.
269+
var portBindings nat.PortMap
270+
if !cfg.Docker.Network.IsContainerNetworkMode() {
271+
portBindings = a.DockerBindings()
272+
} else {
273+
e.log().WithField("network_mode", cfg.Docker.Network.Mode).
274+
Debug("environment/docker: using container network mode, skipping port bindings configuration")
275+
}
276+
236277
hostConf := &container.HostConfig{
237-
PortBindings: a.DockerBindings(),
278+
PortBindings: portBindings,
238279

239280
// Configure the mounts for this container. First mount the server data directory
240281
// into the container as an r/w bind.
@@ -250,7 +291,7 @@ func (e *Environment) Create() error {
250291
// from the Panel.
251292
Resources: e.Configuration.Limits().AsContainerResources(),
252293

253-
DNS: cfg.Docker.Network.Dns,
294+
DNS: dns,
254295

255296
// Configure logging for the container to make it easier on the Daemon to grab
256297
// the server output. Ensure that we don't use too much space on the host machine

server/install.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -415,8 +415,21 @@ func (ip *InstallationProcess) Execute() (string, error) {
415415
ctx, cancel := context.WithCancel(ip.Server.Context())
416416
defer cancel()
417417

418+
// Get config first - must be available before container.Config struct
419+
cfg := config.Get()
420+
421+
// Only set hostname if not using container network mode.
422+
// Containers sharing another container's network namespace inherit that container's hostname.
423+
var hostname string
424+
if !cfg.Docker.Network.IsContainerNetworkMode() {
425+
hostname = "installer"
426+
} else {
427+
ip.Server.Log().WithField("network_mode", cfg.Docker.Network.Mode).
428+
Debug("server/install: using container network mode, skipping hostname configuration")
429+
}
430+
418431
conf := &container.Config{
419-
Hostname: "installer",
432+
Hostname: hostname,
420433
AttachStdout: true,
421434
AttachStderr: true,
422435
AttachStdin: true,
@@ -431,7 +444,12 @@ func (ip *InstallationProcess) Execute() (string, error) {
431444
},
432445
}
433446

434-
cfg := config.Get()
447+
// DNS settings are inherited when using container network mode.
448+
var dns []string
449+
if !cfg.Docker.Network.IsContainerNetworkMode() {
450+
dns = cfg.Docker.Network.Dns
451+
}
452+
435453
tmpfsSize := strconv.Itoa(int(cfg.Docker.TmpfsSize))
436454
hostConf := &container.HostConfig{
437455
Mounts: []mount.Mount{
@@ -452,7 +470,7 @@ func (ip *InstallationProcess) Execute() (string, error) {
452470
Tmpfs: map[string]string{
453471
"/tmp": "rw,exec,nosuid,size=" + tmpfsSize + "M",
454472
},
455-
DNS: cfg.Docker.Network.Dns,
473+
DNS: dns,
456474
LogConfig: cfg.Docker.ContainerLogConfig(),
457475
NetworkMode: container.NetworkMode(cfg.Docker.Network.Mode),
458476
UsernsMode: container.UsernsMode(cfg.Docker.UsernsMode),

0 commit comments

Comments
 (0)