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 <rob.murray@docker.com>
This commit is contained in:
Rob Murray
2025-04-14 17:59:25 +01:00
parent c16caabe36
commit 44a3453d73
8 changed files with 42 additions and 12 deletions

View File

@@ -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")

View File

@@ -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.

View File

@@ -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,
},
})

View File

@@ -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)

View File

@@ -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

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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**.