Skip to content

Commit b87f8e6

Browse files
committed
feat(docker): support container network mode
Enable Docker's container:<name> network mode by conditionally handling settings that are inherited from the target container's network namespace: - Add IsContainerNetworkMode() helper to detect container network mode - Skip hostname/domainname when using container network mode - Skip DNS settings (inherited from target container) - Skip port exposure and bindings (must be on target container) - Warn when ForceOutgoingIP is enabled but ignored - Apply same logic to installer containers This allows routing game server traffic through VPN containers by sharing their network namespace.
1 parent eb6db92 commit b87f8e6

4 files changed

Lines changed: 111 additions & 10 deletions

File tree

config/config_docker.go

Lines changed: 10 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,15 @@ 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+
// Must have "container:" prefix and at least one character for the container name.
51+
// Docker rejects "container:" without a name with "invalid container format container:".
52+
return strings.HasPrefix(c.Mode, "container:") && len(c.Mode) > len("container:")
53+
}
54+
4555
// DockerConfiguration defines the docker configuration used by the daemon when
4656
// interacting with containers and networks on the system.
4757
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:", false}, // Docker rejects "container:" without a name
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)