diff --git a/cmd/dockerd/config_unix.go b/cmd/dockerd/config_unix.go index 2706b5f1ed..de3fa443b3 100644 --- a/cmd/dockerd/config_unix.go +++ b/cmd/dockerd/config_unix.go @@ -38,6 +38,7 @@ func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) { flags.IPVar(&conf.BridgeConfig.DefaultIP, "ip", net.IPv4zero, "Host IP for port publishing from the default bridge network") flags.BoolVar(&conf.BridgeConfig.EnableUserlandProxy, "userland-proxy", true, "Use userland proxy for loopback traffic") flags.StringVar(&conf.BridgeConfig.UserlandProxyPath, "userland-proxy-path", conf.BridgeConfig.UserlandProxyPath, "Path to the userland proxy binary") + flags.BoolVar(&conf.BridgeConfig.AllowDirectRouting, "allow-direct-routing", false, "Allow remote access to published ports on container IP addresses") flags.StringVar(&conf.CgroupParent, "cgroup-parent", "", "Set parent cgroup for all containers") flags.StringVar(&conf.RemappedRoot, "userns-remap", "", "User/Group setting for user namespaces") flags.BoolVar(&conf.LiveRestoreEnabled, "live-restore", false, "Enable live restore of docker when containers are still running") diff --git a/daemon/config/config_linux.go b/daemon/config/config_linux.go index 99c2e3d910..fb76204609 100644 --- a/daemon/config/config_linux.go +++ b/daemon/config/config_linux.go @@ -48,6 +48,7 @@ type BridgeConfig struct { EnableIPMasq bool `json:"ip-masq,omitempty"` EnableUserlandProxy bool `json:"userland-proxy,omitempty"` UserlandProxyPath string `json:"userland-proxy-path,omitempty"` + AllowDirectRouting bool `json:"allow-direct-routing,omitempty"` } // DefaultBridgeConfig stores all the parameters for the default bridge network. diff --git a/daemon/daemon_unix.go b/daemon/daemon_unix.go index 6cf97e1159..fd62b6f2a1 100644 --- a/daemon/daemon_unix.go +++ b/daemon/daemon_unix.go @@ -933,6 +933,7 @@ func driverOptions(config *config.Config) nwconfig.Option { "EnableIP6Tables": config.BridgeConfig.EnableIP6Tables, "EnableUserlandProxy": config.BridgeConfig.EnableUserlandProxy, "UserlandProxyPath": config.BridgeConfig.UserlandProxyPath, + "AllowDirectRouting": config.BridgeConfig.AllowDirectRouting, "Rootless": config.Rootless, }, }) diff --git a/integration/networking/port_mapping_linux_test.go b/integration/networking/port_mapping_linux_test.go index 3025572ea3..f17955d37e 100644 --- a/integration/networking/port_mapping_linux_test.go +++ b/integration/networking/port_mapping_linux_test.go @@ -917,7 +917,30 @@ func TestDirectRemoteAccessOnExposedPort(t *testing.T) { // skip.If(t, testEnv.IsRootless, "rootlesskit has its own netns") ctx := setupTest(t) + d := daemon.New(t) + d.StartWithBusybox(ctx, t) + defer d.Stop(t) + testDirectRemoteAccessOnExposedPort(t, ctx, d, false) +} +// TestAllowDirectRemoteAccessOnExposedPort checks that remote hosts can directly +// reach a container on one of its exposed ports - if the daemon is running with +// option --allow-direct-routing. +func TestAllowDirectRemoteAccessOnExposedPort(t *testing.T) { + // This test checks iptables rules that live in dockerd's netns. In the case + // of rootlesskit, this is not the same netns as the host, so they don't + // have any effect. + // TODO(aker): we need to figure out what we want to do for rootlesskit. + // skip.If(t, testEnv.IsRootless, "rootlesskit has its own netns") + + ctx := setupTest(t) + d := daemon.New(t) + d.StartWithBusybox(ctx, t, "--allow-direct-routing") + defer d.Stop(t) + testDirectRemoteAccessOnExposedPort(t, ctx, d, true) +} + +func testDirectRemoteAccessOnExposedPort(t *testing.T, ctx context.Context, d *daemon.Daemon, allowDirectRouting bool) { const ( hostIPv4 = "192.168.120.2" hostIPv6 = "fdbc:277b:d40b::2" @@ -936,10 +959,6 @@ func TestDirectRemoteAccessOnExposedPort(t *testing.T) { netip.MustParsePrefix("192.168.120.3/24"), netip.MustParsePrefix("fdbc:277b:d40b::3/64")) - d := daemon.New(t) - d.StartWithBusybox(ctx, t) - defer d.Stop(t) - c := d.NewClientT(t) defer c.Close() for _, tc := range []struct { @@ -1006,7 +1025,7 @@ func TestDirectRemoteAccessOnExposedPort(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - expDirectAccess := tc.gwMode == "routed" || tc.gwMode == "nat-unprotected" || tc.trusted + expDirectAccess := tc.gwMode == "routed" || tc.gwMode == "nat-unprotected" || tc.trusted || allowDirectRouting skip.If(t, expDirectAccess && testEnv.IsRootless(), "rootlesskit doesn't support routed mode as it's running in a separate netns") testutil.StartSpan(ctx, t) diff --git a/libnetwork/drivers/bridge/bridge_linux.go b/libnetwork/drivers/bridge/bridge_linux.go index c2a745c52f..83879fdef3 100644 --- a/libnetwork/drivers/bridge/bridge_linux.go +++ b/libnetwork/drivers/bridge/bridge_linux.go @@ -66,6 +66,7 @@ type configuration struct { EnableUserlandProxy bool UserlandProxyPath string Rootless bool + AllowDirectRouting bool } // networkConfiguration for network specific configuration @@ -508,9 +509,10 @@ func (d *driver) configure(option map[string]interface{}) error { var err error d.firewaller, err = iptabler.NewIptabler(iptabler.FirewallConfig{ - IPv4: config.EnableIPTables, - IPv6: config.EnableIP6Tables, - Hairpin: !config.EnableUserlandProxy || config.UserlandProxyPath == "", + IPv4: config.EnableIPTables, + IPv6: config.EnableIP6Tables, + Hairpin: !config.EnableUserlandProxy || config.UserlandProxyPath == "", + AllowDirectRouting: config.AllowDirectRouting, }) if err != nil { return err diff --git a/libnetwork/drivers/bridge/internal/iptabler/endpoint.go b/libnetwork/drivers/bridge/internal/iptabler/endpoint.go index 1feaa74e16..aeea4504ca 100644 --- a/libnetwork/drivers/bridge/internal/iptabler/endpoint.go +++ b/libnetwork/drivers/bridge/internal/iptabler/endpoint.go @@ -37,6 +37,7 @@ func (n *Network) modEndpoint(ctx context.Context, epIPv4, epIPv6 netip.Addr, en // It is a no-op if: // - the network is internal // - gateway mode is "nat-unprotected" or "routed". +// - direct routing is enabled at the daemon level. // - "raw" rules are disabled (possibly because the host doesn't have the necessary // kernel support). // @@ -54,7 +55,7 @@ func (n *Network) filterDirectAccess(ctx context.Context, ipv iptables.IPVersion // direct routing has since been disabled, the rules need to be deleted when // cleanup happens on restart. This also means a change in config over a // live-restore restart will take effect. - if rawRulesDisabled(ctx) { + if n.ipt.AllowDirectRouting || rawRulesDisabled(ctx) { enable = false } for _, ifName := range n.TrustedHostInterfaces { diff --git a/libnetwork/drivers/bridge/internal/iptabler/iptabler.go b/libnetwork/drivers/bridge/internal/iptabler/iptabler.go index b88e2739a6..77200037be 100644 --- a/libnetwork/drivers/bridge/internal/iptabler/iptabler.go +++ b/libnetwork/drivers/bridge/internal/iptabler/iptabler.go @@ -35,9 +35,10 @@ const ( ) type FirewallConfig struct { - IPv4 bool - IPv6 bool - Hairpin bool + IPv4 bool + IPv6 bool + Hairpin bool + AllowDirectRouting bool } type Iptabler struct { diff --git a/man/dockerd.8.md b/man/dockerd.8.md index 276ee90f5f..042f6ed0b7 100644 --- a/man/dockerd.8.md +++ b/man/dockerd.8.md @@ -415,6 +415,10 @@ unix://[/path/to/socket] to use. Use TLS and verify the remote (daemon: verify client, client: verify daemon). Default is **false**. +**--allow-direct-routing**=**true**|**false** + Allow remote access to published ports on container IP addresses. + Default is **false**. + **--userland-proxy**=**true**|**false** Rely on a userland proxy implementation for inter-container and outside-to-container loopback communications. Default is **true**.