diff --git a/daemon/libnetwork/drivers/bridge/internal/firewaller/firewaller.go b/daemon/libnetwork/drivers/bridge/internal/firewaller/firewaller.go index 9ab68aaa58..31320fff7d 100644 --- a/daemon/libnetwork/drivers/bridge/internal/firewaller/firewaller.go +++ b/daemon/libnetwork/drivers/bridge/internal/firewaller/firewaller.go @@ -71,6 +71,22 @@ type NetworkConfigFam struct { Unprotected bool } +// DNATFromOwnBridge returns true if packets that arrive from the network's own +// bridge should be DNATed to pb's container address. If it returns false, +// they're left for the userland proxy. +// +// Without the proxy (config.Hairpin), they're always DNATed. With the proxy, +// DNATing them needs two things the proxy avoids. First, br_netfilter: the +// container's replies are bridged straight back to the client, so conntrack +// only sees them, and restores their source address, if bridged packets go +// through netfilter. The network has that when ICC is disabled. Second, for a +// container that connects to its own published port, hairpin mode on its +// bridge port and a MASQUERADE rule. A binding marked NoSelfConnect doesn't +// need those. +func (nc NetworkConfig) DNATFromOwnBridge(config Config, pb types.PortBinding) bool { + return config.Hairpin || (!nc.ICC && pb.NoSelfConnect) +} + // Firewaller implements firewall rules for bridge networks. type Firewaller interface { // NewNetwork returns an object that can be used to add published ports and legacy diff --git a/daemon/libnetwork/drivers/bridge/internal/iptabler/iptabler_test.go b/daemon/libnetwork/drivers/bridge/internal/iptabler/iptabler_test.go index fd88396a08..94537439fe 100644 --- a/daemon/libnetwork/drivers/bridge/internal/iptabler/iptabler_test.go +++ b/daemon/libnetwork/drivers/bridge/internal/iptabler/iptabler_test.go @@ -15,6 +15,7 @@ import ( "github.com/moby/moby/v2/daemon/libnetwork/types" "github.com/moby/moby/v2/internal/testutil/netnsutils" "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" "gotest.tools/v3/icmd" "gotest.tools/v3/skip" @@ -265,3 +266,63 @@ func testIptabler(t *testing.T, tn string, config firewaller.Config, netConfig f checkResults("iptables", rnWSL2Mirrored(fmt.Sprintf("%s/cleaned,hairpin=%v", tn, config.Hairpin)), config.IPv4) checkResults("ip6tables", fmt.Sprintf("%s/cleaned,hairpin=%v", tn, config.Hairpin), config.IPv6) } + +// TestDNATFromOwnBridge checks when a port binding's DNAT rule leaves packets +// arriving from the network's own bridge to the userland proxy, and that the +// rule is deleted with the binding. +func TestDNATFromOwnBridge(t *testing.T) { + skip.If(t, iptables.UsingFirewalld(), "firewalld is running in the host netns, it can't modify rules in the test's netns") + + testcases := []struct { + hairpin, icc, noSelfConnect bool + expProxySkip bool + }{ + {hairpin: true, icc: true}, + {hairpin: true, icc: false, noSelfConnect: true}, + {icc: true, expProxySkip: true}, + {icc: true, noSelfConnect: true, expProxySkip: true}, + {icc: false, expProxySkip: true}, + {icc: false, noSelfConnect: true}, + } + for _, tc := range testcases { + t.Run(fmt.Sprintf("hairpin=%v/icc=%v/noSelfConnect=%v", tc.hairpin, tc.icc, tc.noSelfConnect), func(t *testing.T) { + defer netnsutils.SetupTestOSContext(t, netnsutils.WithSetNsHandles(false))() + + fw, err := NewIptabler(t.Context(), firewaller.Config{IPv4: true, Hairpin: tc.hairpin}) + assert.NilError(t, err) + nw, err := fw.NewNetwork(t.Context(), firewaller.NetworkConfig{ + IfName: "br-dummy", + ICC: tc.icc, + Masquerade: true, + Config4: firewaller.NetworkConfigFam{Prefix: netip.MustParsePrefix("192.168.0.0/24")}, + }) + assert.NilError(t, err) + pb := types.PortBinding{ + Proto: types.TCP, + IP: net.ParseIP("192.168.0.2"), + Port: 80, + HostIP: net.IPv4zero, + HostPort: 8080, + HostPortEnd: 8080, + NoSelfConnect: tc.noSelfConnect, + } + + dnatRules := func() []string { + t.Helper() + out, err := iptables.GetIptable(iptables.IPv4).Raw("-t", "nat", "-S", dockerChain) + assert.NilError(t, err) + return slices.DeleteFunc(strings.Split(string(out), "\n"), func(r string) bool { + return !strings.Contains(r, "-j DNAT") + }) + } + + assert.NilError(t, nw.AddPorts(t.Context(), []types.PortBinding{pb})) + rules := dnatRules() + assert.Assert(t, is.Len(rules, 1)) + assert.Check(t, strings.Contains(rules[0], "! -i br-dummy") == tc.expProxySkip, "DNAT rule: %s", rules[0]) + + assert.NilError(t, nw.DelPorts(t.Context(), []types.PortBinding{pb})) + assert.Check(t, is.Len(dnatRules(), 0)) + }) + } +} diff --git a/daemon/libnetwork/drivers/bridge/internal/iptabler/port.go b/daemon/libnetwork/drivers/bridge/internal/iptabler/port.go index fcaf63e757..9938e41c40 100644 --- a/daemon/libnetwork/drivers/bridge/internal/iptabler/port.go +++ b/daemon/libnetwork/drivers/bridge/internal/iptabler/port.go @@ -102,7 +102,7 @@ func (n *network) setPerPortNAT(ipv iptables.IPVersion, b types.PortBinding, ena "-j", "DNAT", "--to-destination", net.JoinHostPort(b.IP.String(), strconv.Itoa(int(b.Port))), } - if !n.ipt.config.Hairpin { + if !n.config.DNATFromOwnBridge(n.ipt.config, b) { args = append(args, "!", "-i", n.config.IfName) } if ipv == iptables.IPv6 { diff --git a/daemon/libnetwork/drivers/bridge/internal/nftabler/nftabler_test.go b/daemon/libnetwork/drivers/bridge/internal/nftabler/nftabler_test.go index 78dc1a5d92..92ca7a9d74 100644 --- a/daemon/libnetwork/drivers/bridge/internal/nftabler/nftabler_test.go +++ b/daemon/libnetwork/drivers/bridge/internal/nftabler/nftabler_test.go @@ -242,3 +242,69 @@ func TestHairpinMultipleHostPorts(t *testing.T) { assert.Check(t, is.Equal(countInChain(postRtChain, masqComment), 0)) assert.Check(t, is.Equal(countInChain(natChain, dnatComment), 0)) } + +// TestDNATFromOwnBridge checks when a port binding's DNAT rule leaves packets +// arriving from the network's own bridge to the userland proxy, and that the +// rule is deleted with the binding. +func TestDNATFromOwnBridge(t *testing.T) { + nftables.Enable() + defer nftables.Disable() + + testcases := []struct { + hairpin, icc, noSelfConnect bool + expProxySkip bool + }{ + {hairpin: true, icc: true}, + {hairpin: true, icc: false, noSelfConnect: true}, + {icc: true, expProxySkip: true}, + {icc: true, noSelfConnect: true, expProxySkip: true}, + {icc: false, expProxySkip: true}, + {icc: false, noSelfConnect: true}, + } + for _, tc := range testcases { + t.Run(fmt.Sprintf("hairpin=%v/icc=%v/noSelfConnect=%v", tc.hairpin, tc.icc, tc.noSelfConnect), func(t *testing.T) { + defer netnsutils.SetupTestOSContext(t, netnsutils.WithSetNsHandles(false))() + + fw, err := NewNftabler(t.Context(), firewaller.Config{IPv4: true, Hairpin: tc.hairpin}) + assert.NilError(t, err) + defer fw.Close() + nw, err := fw.NewNetwork(t.Context(), firewaller.NetworkConfig{ + IfName: "br-dummy", + ICC: tc.icc, + Masquerade: true, + Config4: firewaller.NetworkConfigFam{Prefix: netip.MustParsePrefix("192.168.0.0/24")}, + }) + assert.NilError(t, err) + pb := types.PortBinding{ + Proto: types.TCP, + IP: net.ParseIP("192.168.0.2"), + Port: 80, + HostIP: net.IPv4zero, + HostPort: 8080, + HostPortEnd: 8080, + NoSelfConnect: tc.noSelfConnect, + } + + dnatRules := func() []string { + t.Helper() + res := icmd.RunCommand("nft", "list", "table", "ip", dockerTable) + assert.Assert(t, res.Error, res.Combined()) + var rules []string + for _, r := range strings.Split(res.Stdout(), "\n") { + if strings.Contains(r, "dnat to") { + rules = append(rules, r) + } + } + return rules + } + + assert.NilError(t, nw.AddPorts(t.Context(), []types.PortBinding{pb})) + rules := dnatRules() + assert.Assert(t, is.Len(rules, 1)) + assert.Check(t, strings.Contains(rules[0], `iifname != "br-dummy"`) == tc.expProxySkip, "DNAT rule: %s", rules[0]) + + assert.NilError(t, nw.DelPorts(t.Context(), []types.PortBinding{pb})) + assert.Check(t, is.Len(dnatRules(), 0)) + }) + } +} diff --git a/daemon/libnetwork/drivers/bridge/internal/nftabler/port.go b/daemon/libnetwork/drivers/bridge/internal/nftabler/port.go index a186251083..6b25da226e 100644 --- a/daemon/libnetwork/drivers/bridge/internal/nftabler/port.go +++ b/daemon/libnetwork/drivers/bridge/internal/nftabler/port.go @@ -144,10 +144,6 @@ func (n *network) setPerPortHairpinForwarding(pbs []types.PortBinding, updater f } func (n *network) setPerPortDNAT(pbs []types.PortBinding, updater func(nftables.Obj), ipv nftables.Family) { - var proxySkip string - if !n.fw.config.Hairpin { - proxySkip = fmt.Sprintf("iifname != %s ", n.config.IfName) - } var v6LLSkip string if ipv == nftables.IPv6 { v6LLSkip = "ip6 saddr != fe80::/10 " @@ -162,6 +158,10 @@ func (n *network) setPerPortDNAT(pbs []types.PortBinding, updater func(nftables. if (pb.IP.To4() != nil) != (pb.HostIP.To4() != nil) { continue } + var proxySkip string + if !n.config.DNATFromOwnBridge(n.fw.config, pb) { + proxySkip = fmt.Sprintf("iifname != %s ", n.config.IfName) + } var daddrMatch string if !pb.HostIP.IsUnspecified() { daddrMatch = fmt.Sprintf("%s daddr %s ", ipv, pb.HostIP) diff --git a/daemon/libnetwork/drivers/bridge/port_mapping_ephemeral_linux_test.go b/daemon/libnetwork/drivers/bridge/port_mapping_ephemeral_linux_test.go index 00afbe487a..cbee4d5c03 100644 --- a/daemon/libnetwork/drivers/bridge/port_mapping_ephemeral_linux_test.go +++ b/daemon/libnetwork/drivers/bridge/port_mapping_ephemeral_linux_test.go @@ -7,6 +7,7 @@ import ( "os" "testing" + "github.com/moby/moby/v2/daemon/libnetwork/drivers/bridge/internal/firewaller" "github.com/moby/moby/v2/daemon/libnetwork/drvregistry" "github.com/moby/moby/v2/daemon/libnetwork/portallocator" "github.com/moby/moby/v2/daemon/libnetwork/portmappers/nat" @@ -112,6 +113,23 @@ func TestAddEphemeralPortsTracksSeparately(t *testing.T) { assert.Check(t, is.Equal(env.ep.ephemeralPortMapping[0].Proto, types.TCP)) } +// TestAddEphemeralPortsNoSelfConnect checks that a published port's +// NoSelfConnect setting reaches the firewaller, which needs it to decide +// whether packets from the bridge are DNATed or left for the userland proxy. +func TestAddEphemeralPortsNoSelfConnect(t *testing.T) { + defer netnsutils.SetupTestOSContext(t)() + env := newEphTestEnv(t, portBindingMode{ipv4: true}) + + pp := pubPort(types.TCP, 8080) + pp.NoSelfConnect = true + err := env.d.AddEphemeralPorts(t.Context(), ephTestNID, ephTestEID, []types.PublishedPort{pp}) + assert.NilError(t, err) + + fwPorts := env.n.firewallerNetwork.(*firewaller.StubFirewallerNetwork).Ports + assert.Assert(t, is.Len(fwPorts, 1)) + assert.Check(t, fwPorts[0].NoSelfConnect) +} + // TestAddEphemeralPortsNoActiveMode documents a sharp edge rather than asserting // it is desirable: with no NAT family active on the endpoint, addPortMappings // generates no bindings and returns no error, so AddEphemeralPorts reports success diff --git a/daemon/libnetwork/drivers/bridge/port_mapping_linux.go b/daemon/libnetwork/drivers/bridge/port_mapping_linux.go index ce58d5bb98..d131dd498c 100644 --- a/daemon/libnetwork/drivers/bridge/port_mapping_linux.go +++ b/daemon/libnetwork/drivers/bridge/port_mapping_linux.go @@ -471,12 +471,13 @@ func collectFirewallPorts(pbs []portmapperapi.PortBinding) []types.PortBinding { // that can be passed to firewaller.Network for setting up a NAT rule. func toNATBinding(pb portmapperapi.PortBinding) types.PortBinding { return types.PortBinding{ - IP: pb.IP, - Port: pb.Port, - Proto: pb.Proto, - HostIP: pb.NAT.Addr().AsSlice(), - HostPort: pb.NAT.Port(), - HostPortEnd: pb.NAT.Port(), + IP: pb.IP, + Port: pb.Port, + Proto: pb.Proto, + HostIP: pb.NAT.Addr().AsSlice(), + HostPort: pb.NAT.Port(), + HostPortEnd: pb.NAT.Port(), + NoSelfConnect: pb.NoSelfConnect, } } diff --git a/daemon/libnetwork/service_linux.go b/daemon/libnetwork/service_linux.go index 95d0abebc7..5af2b3d3f7 100644 --- a/daemon/libnetwork/service_linux.go +++ b/daemon/libnetwork/service_linux.go @@ -284,6 +284,10 @@ func publishedPort(pc *PortConfig) (types.PublishedPort, error) { Proto: types.ParseProtocol(strings.ToLower(pc.Protocol.String())), Port: uint16(pc.PublishedPort), HostPort: uint16(pc.PublishedPort), + // The ports are published on the load-balancer sandbox's gateway + // endpoint. The sandbox has no container, so nothing behind the + // endpoint connects to them. + NoSelfConnect: true, }, nil } diff --git a/daemon/libnetwork/service_linux_test.go b/daemon/libnetwork/service_linux_test.go index d9c6b39220..b560c2dd8e 100644 --- a/daemon/libnetwork/service_linux_test.go +++ b/daemon/libnetwork/service_linux_test.go @@ -49,7 +49,7 @@ func TestPublishedPortIgnoresTargetPort(t *testing.T) { assert.NilError(t, err) assert.Check(t, is.Equal(a, b)) - assert.Check(t, is.Equal(a, types.PublishedPort{Proto: types.TCP, Port: 8080, HostPort: 8080})) + assert.Check(t, is.Equal(a, types.PublishedPort{Proto: types.TCP, Port: 8080, HostPort: 8080, NoSelfConnect: true})) // A differing protocol is a different reservation. udp, err := publishedPort(&PortConfig{Protocol: ProtocolUDP, PublishedPort: 8080, TargetPort: 80}) @@ -84,7 +84,7 @@ func TestIngressPortRefsAcrossGenerations(t *testing.T) { ppsB, err := publishedPorts([]*PortConfig{{Protocol: ProtocolTCP, PublishedPort: 8080, TargetPort: 8080}}) assert.NilError(t, err) - want := types.PublishedPort{Proto: types.TCP, Port: 8080, HostPort: 8080} + want := types.PublishedPort{Proto: types.TCP, Port: 8080, HostPort: 8080, NoSelfConnect: true} // The first generation publishes the reservation. assert.Check(t, is.DeepEqual(refIngressPorts(ppsA), []types.PublishedPort{want})) diff --git a/daemon/libnetwork/types/types.go b/daemon/libnetwork/types/types.go index 67016ce701..b88eac21aa 100644 --- a/daemon/libnetwork/types/types.go +++ b/daemon/libnetwork/types/types.go @@ -56,6 +56,11 @@ type PortBinding struct { HostIP net.IP HostPort uint16 HostPortEnd uint16 + // NoSelfConnect is a hint that nothing behind the endpoint connects to + // its own published port through the host. Packets from other endpoints + // on the same network can then be DNATed to the port without hairpin + // mode or masquerading, instead of being left for the userland proxy. + NoSelfConnect bool `json:",omitempty"` } // PublishedPort is a request to publish a single container port on the host. @@ -67,16 +72,20 @@ type PublishedPort struct { Proto Protocol Port uint16 // container port HostPort uint16 // single host port - there is no range + // NoSelfConnect is a hint that nothing behind the endpoint connects to + // its own published port through the host. See [PortBinding.NoSelfConnect]. + NoSelfConnect bool } // PortBinding returns the single-port [PortBinding] equivalent of p, with // HostPortEnd pinned to HostPort so it can never denote a range. func (p PublishedPort) PortBinding() PortBinding { return PortBinding{ - Proto: p.Proto, - Port: p.Port, - HostPort: p.HostPort, - HostPortEnd: p.HostPort, + Proto: p.Proto, + Port: p.Port, + HostPort: p.HostPort, + HostPortEnd: p.HostPort, + NoSelfConnect: p.NoSelfConnect, } } @@ -97,12 +106,13 @@ func (p PortBinding) HostAddr() (net.Addr, error) { // Copy returns a deep copy of the PortBinding. func (p PortBinding) Copy() PortBinding { return PortBinding{ - Proto: p.Proto, - IP: slices.Clone(p.IP), - Port: p.Port, - HostIP: slices.Clone(p.HostIP), - HostPort: p.HostPort, - HostPortEnd: p.HostPortEnd, + Proto: p.Proto, + IP: slices.Clone(p.IP), + Port: p.Port, + HostIP: slices.Clone(p.HostIP), + HostPort: p.HostPort, + HostPortEnd: p.HostPortEnd, + NoSelfConnect: p.NoSelfConnect, } } @@ -113,7 +123,8 @@ func (p PortBinding) Equal(o PortBinding) bool { p.Port == o.Port && p.HostIP.Equal(o.HostIP) && p.HostPort == o.HostPort && - p.HostPortEnd == o.HostPortEnd + p.HostPortEnd == o.HostPortEnd && + p.NoSelfConnect == o.NoSelfConnect } // String returns the PortBinding structure in the form "HostIP:HostPort:IP:Port/Proto", diff --git a/integration/network/bridge/iptablesdoc/generated/swarm-portmap.md b/integration/network/bridge/iptablesdoc/generated/swarm-portmap.md index 7af5c49ea7..1b30a8eada 100644 --- a/integration/network/bridge/iptablesdoc/generated/swarm-portmap.md +++ b/integration/network/bridge/iptablesdoc/generated/swarm-portmap.md @@ -91,7 +91,11 @@ Note that: - The published port is set up as an ordinary port mapping on the ingress load-balancer sandbox's `docker_gwbridge` gateway endpoint (`172.18.0.2`), using the same rules as any other published container port: - - a DNAT rule in the nat `DOCKER` chain, and + - a DNAT rule in the nat `DOCKER` chain. Unlike a container's port mapping + when the userland proxy is running, it doesn't skip packets arriving on + `docker_gwbridge`. The load-balancer never connects to its own published + port, so a task that reaches the port via its node's address is DNATed + like any other client, and doesn't go through docker-proxy, and - ACCEPT rules in the filter `DOCKER` chain. There are two, because `docker_gwbridge` has inter-container communication disabled: one for packets from outside the network, and one for packets from the network @@ -118,7 +122,7 @@ And the corresponding nat table: Chain DOCKER (2 references) num pkts bytes target prot opt in out source destination - 1 0 0 DNAT tcp -- !docker_gwbridge any anywhere anywhere tcp dpt:http-alt to:172.18.0.2:8080 + 1 0 0 DNAT tcp -- any any anywhere anywhere tcp dpt:http-alt to:172.18.0.2:8080
@@ -133,7 +137,7 @@ And the corresponding nat table: -A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER -A POSTROUTING -s 172.18.0.0/16 ! -o docker_gwbridge -j MASQUERADE -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE - -A DOCKER ! -i docker_gwbridge -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.18.0.2:8080 + -A DOCKER -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.18.0.2:8080
diff --git a/integration/network/bridge/iptablesdoc/templates/swarm-portmap.md b/integration/network/bridge/iptablesdoc/templates/swarm-portmap.md index 9890628164..f152f1643e 100644 --- a/integration/network/bridge/iptablesdoc/templates/swarm-portmap.md +++ b/integration/network/bridge/iptablesdoc/templates/swarm-portmap.md @@ -22,7 +22,11 @@ Note that: - The published port is set up as an ordinary port mapping on the ingress load-balancer sandbox's `docker_gwbridge` gateway endpoint (`172.18.0.2`), using the same rules as any other published container port: - - a DNAT rule in the nat `DOCKER` chain, and + - a DNAT rule in the nat `DOCKER` chain. Unlike a container's port mapping + when the userland proxy is running, it doesn't skip packets arriving on + `docker_gwbridge`. The load-balancer never connects to its own published + port, so a task that reaches the port via its node's address is DNATed + like any other client, and doesn't go through docker-proxy, and - ACCEPT rules in the filter `DOCKER` chain. There are two, because `docker_gwbridge` has inter-container communication disabled: one for packets from outside the network, and one for packets from the network diff --git a/integration/service/network_linux_test.go b/integration/service/network_linux_test.go index 5f273ae976..713deb4c24 100644 --- a/integration/service/network_linux_test.go +++ b/integration/service/network_linux_test.go @@ -995,17 +995,21 @@ func probeUDPPort(t *testing.T, host networking.Host, hostAddr, port string) err // TestIngressPortFromTaskOnSameNode checks that a container on the node can // reach a service's published port via one of the node's own addresses, with -// and without the userland proxy. +// and without the userland proxy, while the node drops unsolicited input. // // The ingress load balancer's published ports are mapped onto its // docker_gwbridge endpoint, and docker_gwbridge is created with inter-container -// communication disabled. With docker-proxy, the connection is served from the -// host's netns and never crosses that bridge. Without it, the packets are -// DNATed across the bridge, so they have to get past the rules that implement -// icc=false. Reaching a published port shouldn't depend on the userland proxy, -// so both modes are checked and expected to behave the same. +// communication disabled. The container's packets are DNATed across that +// bridge, so they have to get past the rules that implement icc=false. With +// the userland proxy enabled, packets from a container's own bridge are +// normally left for docker-proxy instead, which receives them through the +// host's INPUT chain, where a host firewall such as ufw drops them. Ingress +// ports are DNATed anyway, so that reaching a published port doesn't depend +// on the userland proxy or the host's input policy. Both modes are checked +// and expected to behave the same. // -// Regression test for https://github.com/moby/moby/issues/53713 +// Regression test for https://github.com/moby/moby/issues/53713 and +// https://github.com/moby/moby/issues/53962 func TestIngressPortFromTaskOnSameNode(t *testing.T) { skip.If(t, testEnv.IsRemoteDaemon) skip.If(t, testEnv.IsRootless, "rootless mode doesn't support Swarm-mode") @@ -1031,6 +1035,12 @@ func TestIngressPortFromTaskOnSameNode(t *testing.T) { defer l3.Destroy(t) l3.AddHost(t, l3SegHost, "ns-"+l3SegHost, "eth0", netip.MustParsePrefix(hostAddr+"/24")) + // Drop unsolicited input, like ufw's default "deny incoming" policy. + host := l3.Hosts[l3SegHost] + host.MustRun(t, "iptables", "-A", "INPUT", "-i", "lo", "-j", "ACCEPT") + host.MustRun(t, "iptables", "-A", "INPUT", "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT") + host.MustRun(t, "iptables", "-P", "INPUT", "DROP") + l3.Hosts[l3SegHost].Do(t, func() { // Not swarm.NewSwarm, because it can't pass extra daemon flags. d := daemon.New(t, daemon.WithSwarmIptables(true))