From 44a3453d73f5366fc368ed5ef6957ec03495df1c Mon Sep 17 00:00:00 2001 From: Rob Murray Date: Mon, 14 Apr 2025 17:59:25 +0100 Subject: [PATCH] Add daemon option --allow-direct-routing Per-network option com.docker.network.bridge.trusted-host-interfaces accepts a list of interfaces that are allowed to route directly to a container's published ports in a bridge network with nat enabled. This daemon level option disables direct access filtering, enabling direct access to published ports on container addresses in all bridge networks, via all host interfaces. It overlaps with short-term env-var workaround: DOCKER_INSECURE_NO_IPTABLES_RAW=1 - it does not allow packets sent from outside the host to reach ports published only to 127.0.0.1 - it will outlive iptables (the workaround was initially intended for hosts that do not have kernel support for the "raw" iptables table). Signed-off-by: Rob Murray --- cmd/dockerd/config_unix.go | 1 + daemon/config/config_linux.go | 1 + daemon/daemon_unix.go | 1 + .../networking/port_mapping_linux_test.go | 29 +++++++++++++++---- libnetwork/drivers/bridge/bridge_linux.go | 8 +++-- .../bridge/internal/iptabler/endpoint.go | 3 +- .../bridge/internal/iptabler/iptabler.go | 7 +++-- man/dockerd.8.md | 4 +++ 8 files changed, 42 insertions(+), 12 deletions(-) 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**.