diff --git a/internal/runtime/proxied.go b/internal/runtime/proxied.go index 2b5eb6f..4abd927 100644 --- a/internal/runtime/proxied.go +++ b/internal/runtime/proxied.go @@ -407,10 +407,6 @@ func sidecarCreate(name, slug string, proxyID *int64, image, network string) cli } init := true pids := tunnelPidsLimit - mode := container.NetworkMode("bridge") - if network != "" { - mode = container.NetworkMode(network) - } restart := container.RestartPolicy{Name: "unless-stopped"} if proxyID == nil { restart = container.RestartPolicy{Name: "no"} @@ -420,6 +416,34 @@ func sidecarCreate(name, slug string, proxyID *int64, image, network string) cli if len(hostname) > 63 { hostname = hostname[:63] } + host := &container.HostConfig{ + NetworkMode: "bridge", + CapDrop: []string{"ALL"}, + CapAdd: []string{"NET_ADMIN"}, + Privileged: false, + SecurityOpt: []string{noNewPrivileges}, + ReadonlyRootfs: true, + Tmpfs: map[string]string{"/run": "size=8m,mode=0755"}, + Init: &init, + Sysctls: map[string]string{ + "net.ipv6.conf.all.disable_ipv6": "1", + "net.ipv6.conf.default.disable_ipv6": "1", + // DNS sent to Docker's resolver is redirected into the tunnel and keeps a + // loopback source until it is rewritten; see cp-tunnel. + "net.ipv4.conf.all.route_localnet": "1", + }, + RestartPolicy: restart, + // tun2proxy runs at warn, so this log holds errors, never the sites the + // earner reaches; the bound keeps even a noisy failure off the disk. + LogConfig: container.LogConfig{Type: "json-file", Config: map[string]string{"max-size": "1m", "max-file": "2"}}, + Resources: container.Resources{ + PidsLimit: &pids, + Devices: []container.DeviceMapping{{PathOnHost: "/dev/net/tun", PathInContainer: "/dev/net/tun", CgroupPermissions: "rwm"}}, + }, + } + // The fenced network and its resolvers, as every bridge earner gets them: the + // fence drops the daemon's LAN resolver, so the tunnel needs the public ones. + isolate(host, network) return client.ContainerCreateOptions{ Name: name, Config: &container.Config{ @@ -428,31 +452,7 @@ func sidecarCreate(name, slug string, proxyID *int64, image, network string) cli Hostname: hostname, Volumes: map[string]struct{}{tunnelSecretDir: {}}, }, - HostConfig: &container.HostConfig{ - NetworkMode: mode, - CapDrop: []string{"ALL"}, - CapAdd: []string{"NET_ADMIN"}, - Privileged: false, - SecurityOpt: []string{noNewPrivileges}, - ReadonlyRootfs: true, - Tmpfs: map[string]string{"/run": "size=8m,mode=0755"}, - Init: &init, - Sysctls: map[string]string{ - "net.ipv6.conf.all.disable_ipv6": "1", - "net.ipv6.conf.default.disable_ipv6": "1", - // DNS sent to Docker's resolver is redirected into the tunnel and keeps a - // loopback source until it is rewritten; see cp-tunnel. - "net.ipv4.conf.all.route_localnet": "1", - }, - RestartPolicy: restart, - // tun2proxy runs at warn, so this log holds errors, never the sites the - // earner reaches; the bound keeps even a noisy failure off the disk. - LogConfig: container.LogConfig{Type: "json-file", Config: map[string]string{"max-size": "1m", "max-file": "2"}}, - Resources: container.Resources{ - PidsLimit: &pids, - Devices: []container.DeviceMapping{{PathOnHost: "/dev/net/tun", PathInContainer: "/dev/net/tun", CgroupPermissions: "rwm"}}, - }, - }, + HostConfig: host, } } diff --git a/internal/runtime/proxied_test.go b/internal/runtime/proxied_test.go index eb31bd5..d3f09f4 100644 --- a/internal/runtime/proxied_test.go +++ b/internal/runtime/proxied_test.go @@ -280,6 +280,23 @@ func deployRequest() ProxiedDeploy { return ProxiedDeploy{Slug: "earner", ProxyID: 3, Service: proxiedService, Env: map[string]string{"TOKEN": "tok"}, Proxy: testSpec(), OwnIP: "192.0.2.1"} } +// A fenced tunnel gets the fence's resolvers, as every fenced earner does: the fence +// drops the daemon's LAN resolver, so without them the tunnel resolves nothing. +func TestFencedSidecarGetsTheFencesDNS(t *testing.T) { + id := int64(3) + fenced := sidecarCreate(tunnelName("earner", id), "earner", &id, "example/cashpilot-tunnel:1.66.0", FenceNetwork).HostConfig + if fenced.NetworkMode != FenceNetwork { + t.Fatalf("network mode %q", fenced.NetworkMode) + } + if !slices.Equal(fenced.DNS, fenceDNS) { + t.Fatalf("DNS %v, want the fence's %v", fenced.DNS, fenceDNS) + } + plain := sidecarCreate(tunnelName("earner", id), "earner", &id, "example/cashpilot-tunnel:1.66.0", "").HostConfig + if plain.NetworkMode != "bridge" || plain.DNS != nil { + t.Fatalf("the default bridge tunnel was changed: net=%q dns=%v", plain.NetworkMode, plain.DNS) + } +} + // The sidecar is the only container with any network power, so its create options // are the security boundary: NET_ADMIN and /dev/net/tun and nothing else, never // privileged, read-only, no Docker socket, no published ports.