mirror of
https://github.com/moby/moby.git
synced 2026-01-15 18:02:03 +00:00
libnet/d/bridge: drop remote connections to port mapped on lo
Traditionally when Linux receives remote packets with daddr set to a loopback address, it reject them as 'martians'. However, when a NAT rule is applied through iptables this doesn't happen. Our current DNAT rule used to map host ports to containers is applied unconditionally, even for such 'martian' packets. This means a neighbor host (ie. a host connected to the same L2 segment) can send packets to a port mapped on a loopback address. The purpose of publishing on a loopback address is to make ports inaccessible to remote hosts -- lack of proper filtering defeats that. This commit adds an iptables rule to the raw-PREROUTING chain to drop packets with a loopback dest address and coming from any interface other than lo. To accomodate WSL2 mirrored mode, another rule is inserted beforehand to specifically accept packets coming from the loopback0 interface. Signed-off-by: Albin Kerouanton <albinker@gmail.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
## Container on a user-defined network, with a port published on a loopback address
|
||||
|
||||
Adding a network running a container with a port mapped on a loopback address, equivalent to:
|
||||
|
||||
docker network create \
|
||||
-o com.docker.network.bridge.name=bridge1 \
|
||||
--subnet 192.0.2.0/24 --gateway 192.0.2.1 bridge1
|
||||
docker run --network bridge1 -p 127.0.0.1:8080:80 --name c1 busybox
|
||||
|
||||
The filter and nat tables are identical to [nat mode][0]:
|
||||
|
||||
<details>
|
||||
<summary>filter table</summary>
|
||||
|
||||
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
|
||||
num pkts bytes target prot opt in out source destination
|
||||
|
||||
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
|
||||
num pkts bytes target prot opt in out source destination
|
||||
1 0 0 DOCKER-USER 0 -- * * 0.0.0.0/0 0.0.0.0/0
|
||||
2 0 0 ACCEPT 0 -- * * 0.0.0.0/0 0.0.0.0/0 match-set docker-ext-bridges-v4 dst ctstate RELATED,ESTABLISHED
|
||||
3 0 0 DOCKER-ISOLATION-STAGE-1 0 -- * * 0.0.0.0/0 0.0.0.0/0
|
||||
4 0 0 DOCKER 0 -- * * 0.0.0.0/0 0.0.0.0/0 match-set docker-ext-bridges-v4 dst
|
||||
5 0 0 ACCEPT 0 -- docker0 * 0.0.0.0/0 0.0.0.0/0
|
||||
6 0 0 ACCEPT 0 -- bridge1 * 0.0.0.0/0 0.0.0.0/0
|
||||
|
||||
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
|
||||
num pkts bytes target prot opt in out source destination
|
||||
|
||||
Chain DOCKER (1 references)
|
||||
num pkts bytes target prot opt in out source destination
|
||||
1 0 0 ACCEPT 6 -- !bridge1 bridge1 0.0.0.0/0 192.0.2.2 tcp dpt:80
|
||||
2 0 0 DROP 0 -- !docker0 docker0 0.0.0.0/0 0.0.0.0/0
|
||||
3 0 0 DROP 0 -- !bridge1 bridge1 0.0.0.0/0 0.0.0.0/0
|
||||
|
||||
Chain DOCKER-ISOLATION-STAGE-1 (1 references)
|
||||
num pkts bytes target prot opt in out source destination
|
||||
1 0 0 DOCKER-ISOLATION-STAGE-2 0 -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
|
||||
2 0 0 DOCKER-ISOLATION-STAGE-2 0 -- bridge1 !bridge1 0.0.0.0/0 0.0.0.0/0
|
||||
|
||||
Chain DOCKER-ISOLATION-STAGE-2 (2 references)
|
||||
num pkts bytes target prot opt in out source destination
|
||||
1 0 0 DROP 0 -- * bridge1 0.0.0.0/0 0.0.0.0/0
|
||||
2 0 0 DROP 0 -- * docker0 0.0.0.0/0 0.0.0.0/0
|
||||
|
||||
Chain DOCKER-USER (1 references)
|
||||
num pkts bytes target prot opt in out source destination
|
||||
1 0 0 RETURN 0 -- * * 0.0.0.0/0 0.0.0.0/0
|
||||
|
||||
|
||||
-P INPUT ACCEPT
|
||||
-P FORWARD ACCEPT
|
||||
-P OUTPUT ACCEPT
|
||||
-N DOCKER
|
||||
-N DOCKER-ISOLATION-STAGE-1
|
||||
-N DOCKER-ISOLATION-STAGE-2
|
||||
-N DOCKER-USER
|
||||
-A FORWARD -j DOCKER-USER
|
||||
-A FORWARD -m set --match-set docker-ext-bridges-v4 dst -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
|
||||
-A FORWARD -m set --match-set docker-ext-bridges-v4 dst -j DOCKER
|
||||
-A FORWARD -i docker0 -j ACCEPT
|
||||
-A FORWARD -i bridge1 -j ACCEPT
|
||||
-A DOCKER -d 192.0.2.2/32 ! -i bridge1 -o bridge1 -p tcp -m tcp --dport 80 -j ACCEPT
|
||||
-A DOCKER ! -i docker0 -o docker0 -j DROP
|
||||
-A DOCKER ! -i bridge1 -o bridge1 -j DROP
|
||||
-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
|
||||
-A DOCKER-ISOLATION-STAGE-1 -i bridge1 ! -o bridge1 -j DOCKER-ISOLATION-STAGE-2
|
||||
-A DOCKER-ISOLATION-STAGE-2 -o bridge1 -j DROP
|
||||
-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
|
||||
-A DOCKER-USER -j RETURN
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>nat table</summary>
|
||||
|
||||
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
|
||||
num pkts bytes target prot opt in out source destination
|
||||
1 0 0 DOCKER 0 -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
|
||||
|
||||
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
|
||||
num pkts bytes target prot opt in out source destination
|
||||
|
||||
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
|
||||
num pkts bytes target prot opt in out source destination
|
||||
1 0 0 DOCKER 0 -- * * 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL
|
||||
|
||||
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
|
||||
num pkts bytes target prot opt in out source destination
|
||||
1 0 0 MASQUERADE 0 -- * !bridge1 192.0.2.0/24 0.0.0.0/0
|
||||
2 0 0 MASQUERADE 0 -- * !docker0 172.17.0.0/16 0.0.0.0/0
|
||||
|
||||
Chain DOCKER (2 references)
|
||||
num pkts bytes target prot opt in out source destination
|
||||
1 0 0 RETURN 0 -- bridge1 * 0.0.0.0/0 0.0.0.0/0
|
||||
2 0 0 RETURN 0 -- docker0 * 0.0.0.0/0 0.0.0.0/0
|
||||
3 0 0 DNAT 6 -- !bridge1 * 0.0.0.0/0 127.0.0.1 tcp dpt:8080 to:192.0.2.2:80
|
||||
|
||||
|
||||
-P PREROUTING ACCEPT
|
||||
-P INPUT ACCEPT
|
||||
-P OUTPUT ACCEPT
|
||||
-P POSTROUTING ACCEPT
|
||||
-N DOCKER
|
||||
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
|
||||
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
|
||||
-A POSTROUTING -s 192.0.2.0/24 ! -o bridge1 -j MASQUERADE
|
||||
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
|
||||
-A DOCKER -i bridge1 -j RETURN
|
||||
-A DOCKER -i docker0 -j RETURN
|
||||
-A DOCKER -d 127.0.0.1/32 ! -i bridge1 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 192.0.2.2:80
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
|
||||
num pkts bytes target prot opt in out source destination
|
||||
1 0 0 DROP 6 -- !lo * 0.0.0.0/0 127.0.0.1 tcp dpt:8080
|
||||
2 0 0 DROP 6 -- !bridge1 * 0.0.0.0/0 192.0.2.2 tcp dpt:80
|
||||
|
||||
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
|
||||
num pkts bytes target prot opt in out source destination
|
||||
|
||||
|
||||
<details>
|
||||
<summary>iptables commands</summary>
|
||||
|
||||
-P PREROUTING ACCEPT
|
||||
-P OUTPUT ACCEPT
|
||||
-A PREROUTING -d 127.0.0.1/32 ! -i lo -p tcp -m tcp --dport 8080 -j DROP
|
||||
-A PREROUTING -d 192.0.2.2/32 ! -i bridge1 -p tcp -m tcp --dport 80 -j DROP
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
[filterPortMappedOnLoopback][1] adds an extra rule in the raw-PREROUTING chain to DROP remote traffic destined to the
|
||||
port mapped on the loopback address.
|
||||
|
||||
[0]: usernet-portmap.md
|
||||
[1]: https://github.com/search?q=repo%3Amoby%2Fmoby%20filterPortMappedOnLoopback&type=code
|
||||
@@ -40,6 +40,7 @@ Scenarios:
|
||||
|
||||
- [New daemon](generated/new-daemon.md)
|
||||
- [Container on a user-defined network, with a published port](generated/usernet-portmap.md)
|
||||
- [Container on a user-defined network, with a port published on a loopback address](generated/usernet-portmap-lo.md)
|
||||
- [Container on a user-defined network, with a published port, no userland proxy](generated/usernet-portmap-noproxy.md)
|
||||
- [Container on a user-defined network with inter-container communication disabled, with a published port](generated/usernet-portmap-noicc.md)
|
||||
- [Container on a user-defined --internal network](generated/usernet-internal.md)
|
||||
|
||||
@@ -174,6 +174,18 @@ var index = []section{
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "usernet-portmap-lo.md",
|
||||
networks: []networkDesc{{
|
||||
name: "bridge1",
|
||||
containers: []ctrDesc{
|
||||
{
|
||||
name: "c1",
|
||||
portMappings: nat.PortMap{"80/tcp": {{HostIP: "127.0.0.1", HostPort: "8080"}}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
// iptCmdType is used to look up iptCmds in the markdown (can't use an int
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
## Container on a user-defined network, with a port published on a loopback address
|
||||
|
||||
Adding a network running a container with a port mapped on a loopback address, equivalent to:
|
||||
|
||||
docker network create \
|
||||
-o com.docker.network.bridge.name=bridge1 \
|
||||
--subnet 192.0.2.0/24 --gateway 192.0.2.1 bridge1
|
||||
docker run --network bridge1 -p 127.0.0.1:8080:80 --name c1 busybox
|
||||
|
||||
The filter and nat tables are identical to [nat mode][0]:
|
||||
|
||||
<details>
|
||||
<summary>filter table</summary>
|
||||
|
||||
{{index . "LFilter4"}}
|
||||
|
||||
{{index . "SFilter4"}}
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>nat table</summary>
|
||||
|
||||
{{index . "LNat4"}}
|
||||
|
||||
{{index . "SNat4"}}
|
||||
|
||||
</details>
|
||||
|
||||
{{index . "LRaw4"}}
|
||||
|
||||
<details>
|
||||
<summary>iptables commands</summary>
|
||||
|
||||
{{index . "SRaw4"}}
|
||||
|
||||
</details>
|
||||
|
||||
[filterPortMappedOnLoopback][1] adds an extra rule in the raw-PREROUTING chain to DROP remote traffic destined to the
|
||||
port mapped on the loopback address.
|
||||
|
||||
[0]: usernet-portmap.md
|
||||
[1]: https://github.com/search?q=repo%3Amoby%2Fmoby%20filterPortMappedOnLoopback&type=code
|
||||
@@ -1045,6 +1045,81 @@ func TestDirectRemoteAccessOnExposedPort(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestAccessPortPublishedOnLoopbackAddress checks that ports published on
|
||||
// loopback addresses can't be accessed by remote hosts.
|
||||
//
|
||||
// Regression test for https://github.com/moby/moby/issues/45610.
|
||||
func TestAccessPortPublishedOnLoopbackAddress(t *testing.T) {
|
||||
// rootlesskit uses a proxy to forward ports from the host netns to its own
|
||||
// netns, so it's not affected by the original issue.
|
||||
skip.If(t, testEnv.IsRootless, "rootlesskit has its own netns")
|
||||
|
||||
ctx := setupTest(t)
|
||||
|
||||
l3 := networking.NewL3Segment(t, "test-access-loopback",
|
||||
netip.MustParsePrefix("192.168.121.1/24"))
|
||||
defer l3.Destroy(t)
|
||||
// "docker" is the host where dockerd is running.
|
||||
l3.AddHost(t, "docker", networking.CurrentNetns, "eth-test",
|
||||
netip.MustParsePrefix("192.168.121.2/24"))
|
||||
l3.AddHost(t, "attacker", "test-access-loopback-attacker", "eth0",
|
||||
netip.MustParsePrefix("192.168.121.3/24"))
|
||||
|
||||
d := daemon.New(t)
|
||||
d.StartWithBusybox(ctx, t)
|
||||
defer d.Stop(t)
|
||||
|
||||
c := d.NewClientT(t)
|
||||
defer c.Close()
|
||||
|
||||
const bridgeName = "brtest"
|
||||
network.CreateNoError(ctx, t, c, bridgeName,
|
||||
network.WithDriver("bridge"),
|
||||
network.WithOption(bridge.BridgeName, bridgeName),
|
||||
)
|
||||
defer network.RemoveNoError(ctx, t, c, bridgeName)
|
||||
|
||||
const (
|
||||
loIP = "127.0.0.2"
|
||||
hostPort = "5000"
|
||||
)
|
||||
|
||||
// The busybox version of netcat doesn't handle properly the `-k` flag,
|
||||
// which should allow it to print the payload of multiple sequential
|
||||
// connections. To overcome that limitation, start a new container every
|
||||
// time we want to test if a payload is received.
|
||||
test := func(t *testing.T, host networking.Host, payload string) bool {
|
||||
t.Helper()
|
||||
|
||||
serverID := container.Run(ctx, t, c,
|
||||
container.WithName("server"),
|
||||
container.WithCmd("nc", "-lup", "5000"),
|
||||
container.WithExposedPorts("5000/udp"),
|
||||
// This port is mapped on 127.0.0.2, so it should not be remotely accessible.
|
||||
container.WithPortMap(nat.PortMap{"5000/udp": {{HostIP: loIP, HostPort: hostPort}}}),
|
||||
container.WithNetworkMode(bridgeName))
|
||||
defer c.ContainerRemove(ctx, serverID, containertypes.RemoveOptions{Force: true})
|
||||
|
||||
return sendPayloadFromHost(t, host, loIP, hostPort, payload, func() bool {
|
||||
logs := getContainerStdout(t, ctx, c, serverID)
|
||||
return strings.Contains(logs, payload)
|
||||
})
|
||||
}
|
||||
|
||||
// Check if the local host has access to the published port.
|
||||
res := test(t, l3.Hosts["docker"], "foobar")
|
||||
assert.Assert(t, res, "Local host should have access to the published port, but no payload was received by the container")
|
||||
|
||||
// Add a route to the loopback address on the attacker host in order to
|
||||
// conduct the attack scenario.
|
||||
l3.Hosts["attacker"].Run(t, "ip", "route", "add", loIP+"/32", "via", "192.168.121.2", "dev", "eth0")
|
||||
defer l3.Hosts["attacker"].Run(t, "ip", "route", "delete", loIP+"/32", "via", "192.168.121.2", "dev", "eth0")
|
||||
|
||||
// Check that remote access to the loopback address is correctly blocked.
|
||||
res = test(t, l3.Hosts["attacker"], "bar baz")
|
||||
assert.Assert(t, !res, "Remote host should not have access to the published port, but the payload was received by the container")
|
||||
}
|
||||
|
||||
// Send a payload to daddr:dport a few times from the 'host' netns. Stop
|
||||
// sending payloads when 'check' returns true. Return the result of 'check'.
|
||||
//
|
||||
|
||||
@@ -76,6 +76,7 @@ func (l3 *L3Segment) AddHost(t *testing.T, hostname, nsName, ifname string, addr
|
||||
host.MustRun(t, "ip", "link", "add", hostname, "netns", l3.bridge.ns, "type", "veth", "peer", "name", host.Iface)
|
||||
l3.bridge.MustRun(t, "ip", "link", "set", hostname, "up", "master", l3.bridge.Iface)
|
||||
host.MustRun(t, "ip", "link", "set", host.Iface, "up")
|
||||
host.MustRun(t, "ip", "link", "set", "lo", "up")
|
||||
|
||||
for _, addr := range addrs {
|
||||
host.MustRun(t, "ip", "addr", "add", addr.String(), "dev", host.Iface, "nodad")
|
||||
@@ -145,25 +146,27 @@ func (h Host) MustRun(t *testing.T, cmd string, args ...string) string {
|
||||
func (h Host) Do(t *testing.T, fn func()) {
|
||||
t.Helper()
|
||||
|
||||
targetNs, err := netns.GetFromName(h.ns)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get netns handle: %v", err)
|
||||
}
|
||||
defer targetNs.Close()
|
||||
if h.ns != CurrentNetns {
|
||||
targetNs, err := netns.GetFromName(h.ns)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get netns handle: %v", err)
|
||||
}
|
||||
defer targetNs.Close()
|
||||
|
||||
origNs, err := netns.Get()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get current netns: %v", err)
|
||||
}
|
||||
defer origNs.Close()
|
||||
origNs, err := netns.Get()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get current netns: %v", err)
|
||||
}
|
||||
defer origNs.Close()
|
||||
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
if err := netns.Set(targetNs); err != nil {
|
||||
t.Fatalf("failed to enter netns: %v", err)
|
||||
if err := netns.Set(targetNs); err != nil {
|
||||
t.Fatalf("failed to enter netns: %v", err)
|
||||
}
|
||||
defer netns.Set(origNs)
|
||||
}
|
||||
defer netns.Set(origNs)
|
||||
|
||||
fn()
|
||||
}
|
||||
|
||||
@@ -195,6 +195,9 @@ func (n *bridgeNetwork) addPortMappings(
|
||||
if err := n.setPerPortIptables(b, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := n.filterPortMappedOnLoopback(b, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := n.filterDirectAccess(b, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -747,6 +750,9 @@ func (n *bridgeNetwork) releasePortBindings(pbs []portBinding) error {
|
||||
if err := n.setPerPortIptables(pb, false); err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to remove iptables rules for port mapping %s: %w", pb, err))
|
||||
}
|
||||
if err := n.filterPortMappedOnLoopback(pb, false); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := n.filterDirectAccess(pb, false); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
@@ -872,6 +878,44 @@ func setPerPortForwarding(b portBinding, ipv iptables.IPVersion, bridgeName stri
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterPortMappedOnLoopback adds an iptables rule that drops remote
|
||||
// connections to ports mapped on loopback addresses.
|
||||
//
|
||||
// This is a no-ip if the portBinding is for IPv6 (IPv6 loopback address is
|
||||
// non-routable), or over a network with gw_mode=routed (PBs in routed mode
|
||||
// don't map ports on the host).
|
||||
func (n *bridgeNetwork) filterPortMappedOnLoopback(b portBinding, enable bool) error {
|
||||
hostIP := b.childHostIP
|
||||
if b.HostPort == 0 || !hostIP.IsLoopback() || b.childHostIP.To4() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
acceptMirrored := iptables.Rule{IPVer: iptables.IPv4, Table: iptables.Raw, Chain: "PREROUTING", Args: []string{
|
||||
"-p", b.Proto.String(),
|
||||
"-d", hostIP.String(),
|
||||
"--dport", strconv.Itoa(int(b.HostPort)),
|
||||
"-i", "loopback0",
|
||||
"-j", "ACCEPT",
|
||||
}}
|
||||
enableMirrored := enable && isRunningUnderWSL2MirroredMode()
|
||||
if err := appendOrDelChainRule(acceptMirrored, "LOOPBACK FILTERING - ACCEPT MIRRORED", enableMirrored); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
drop := iptables.Rule{IPVer: iptables.IPv4, Table: iptables.Raw, Chain: "PREROUTING", Args: []string{
|
||||
"-p", b.Proto.String(),
|
||||
"-d", hostIP.String(),
|
||||
"--dport", strconv.Itoa(int(b.HostPort)),
|
||||
"!", "-i", "lo",
|
||||
"-j", "DROP",
|
||||
}}
|
||||
if err := appendOrDelChainRule(drop, "LOOPBACK FILTERING - DROP", enable); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterDirectAccess adds an iptables rule that drops 'direct' remote
|
||||
// connections made to the container's IP address, when the network gateway
|
||||
// mode is "nat".
|
||||
|
||||
@@ -762,15 +762,22 @@ func mirroredWSL2Workaround(config configuration, ipv iptables.IPVersion) error
|
||||
// the workaround, with improvements in WSL2 v2.3.11, and without userland proxy
|
||||
// running - no workaround is needed, the normal DNAT/masquerading works.
|
||||
// - and, the host Linux appears to be running under Windows WSL2 with mirrored
|
||||
// mode networking. If a loopback0 device exists, and there's an executable at
|
||||
// /usr/bin/wslinfo, infer that this is WSL2 with mirrored networking. ("wslinfo
|
||||
// --networking-mode" reports "mirrored", but applying the workaround for WSL2's
|
||||
// loopback device when it's not needed is low risk, compared with executing
|
||||
// wslinfo with dockerd's elevated permissions.)
|
||||
// mode networking.
|
||||
func insertMirroredWSL2Rule(config configuration) bool {
|
||||
if !config.EnableUserlandProxy || config.UserlandProxyPath == "" {
|
||||
return false
|
||||
}
|
||||
return isRunningUnderWSL2MirroredMode()
|
||||
}
|
||||
|
||||
// isRunningUnderWSL2MirroredMode returns true if the host Linux appears to be
|
||||
// running under Windows WSL2 with mirrored mode networking. If a loopback0
|
||||
// device exists, and there's an executable at /usr/bin/wslinfo, infer that
|
||||
// this is WSL2 with mirrored networking. ("wslinfo --networking-mode" reports
|
||||
// "mirrored", but applying the workaround for WSL2's loopback device when it's
|
||||
// not needed is low risk, compared with executing wslinfo with dockerd's
|
||||
// elevated permissions.)
|
||||
func isRunningUnderWSL2MirroredMode() bool {
|
||||
if _, err := nlwrap.LinkByName("loopback0"); err != nil {
|
||||
if !errors.As(err, &netlink.LinkNotFoundError{}) {
|
||||
log.G(context.TODO()).WithError(err).Warn("Failed to check for WSL interface")
|
||||
|
||||
@@ -3,7 +3,9 @@ package bridge
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/internal/nlwrap"
|
||||
@@ -12,6 +14,7 @@ import (
|
||||
"github.com/docker/docker/libnetwork/driverapi"
|
||||
"github.com/docker/docker/libnetwork/iptables"
|
||||
"github.com/docker/docker/libnetwork/netlabel"
|
||||
"github.com/docker/docker/libnetwork/types"
|
||||
"github.com/vishvananda/netlink"
|
||||
"golang.org/x/sys/unix"
|
||||
"gotest.tools/v3/assert"
|
||||
@@ -464,27 +467,8 @@ func TestMirroredWSL2Workaround(t *testing.T) {
|
||||
} {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
defer netnsutils.SetupTestOSContext(t)()
|
||||
|
||||
if tc.loopback0 {
|
||||
loopback0 := &netlink.Dummy{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: "loopback0",
|
||||
},
|
||||
}
|
||||
err := netlink.LinkAdd(loopback0)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
if tc.wslinfoPerm != 0 {
|
||||
wslinfoPathOrig := wslinfoPath
|
||||
defer func() {
|
||||
wslinfoPath = wslinfoPathOrig
|
||||
}()
|
||||
tmpdir := t.TempDir()
|
||||
wslinfoPath = filepath.Join(tmpdir, "wslinfo")
|
||||
err := os.WriteFile(wslinfoPath, []byte("#!/bin/sh\necho dummy file\n"), tc.wslinfoPerm)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
restoreWslinfoPath := simulateWSL2MirroredMode(t, tc.loopback0, tc.wslinfoPerm)
|
||||
defer restoreWslinfoPath()
|
||||
|
||||
assert.NilError(t, setupHashNetIpset(ipsetExtBridges4, unix.AF_INET))
|
||||
|
||||
@@ -499,3 +483,94 @@ func TestMirroredWSL2Workaround(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// simulateWSL2MirroredMode simulates the WSL2 mirrored mode by creating a
|
||||
// loopback0 interface and optionally creating a wslinfo file with the given
|
||||
// permissions.
|
||||
// A clean up function is returned and will restore the original wslinfoPath
|
||||
// used within the 'bridge' package. The loopback0 interface isn't cleaned up.
|
||||
// Instead this function should be called from a disposable network namespace.
|
||||
func simulateWSL2MirroredMode(t *testing.T, loopback0 bool, wslinfoPerm os.FileMode) func() {
|
||||
if loopback0 {
|
||||
iface := &netlink.Dummy{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: "loopback0",
|
||||
},
|
||||
}
|
||||
err := netlink.LinkAdd(iface)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
wslinfoPathOrig := wslinfoPath
|
||||
if wslinfoPerm != 0 {
|
||||
tmpdir := t.TempDir()
|
||||
p := filepath.Join(tmpdir, "wslinfo")
|
||||
err := os.WriteFile(p, []byte("#!/bin/sh\necho dummy file\n"), wslinfoPerm)
|
||||
assert.NilError(t, err)
|
||||
wslinfoPath = p
|
||||
}
|
||||
|
||||
return func() {
|
||||
wslinfoPath = wslinfoPathOrig
|
||||
}
|
||||
}
|
||||
|
||||
func TestMirroredWSL2LoopbackFiltering(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
desc string
|
||||
loopback0 bool
|
||||
wslinfoPerm os.FileMode // 0 for no-file
|
||||
expLoopback0Rule bool
|
||||
}{
|
||||
{
|
||||
desc: "No loopback0",
|
||||
},
|
||||
{
|
||||
desc: "WSL2 mirrored",
|
||||
loopback0: true,
|
||||
wslinfoPerm: 0o777,
|
||||
expLoopback0Rule: true,
|
||||
},
|
||||
{
|
||||
desc: "loopback0 but wslinfo not executable",
|
||||
loopback0: true,
|
||||
wslinfoPerm: 0o666,
|
||||
},
|
||||
{
|
||||
desc: "loopback0 but no wslinfo",
|
||||
loopback0: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
defer netnsutils.SetupTestOSContext(t)()
|
||||
restoreWslinfoPath := simulateWSL2MirroredMode(t, tc.loopback0, tc.wslinfoPerm)
|
||||
defer restoreWslinfoPath()
|
||||
|
||||
nw := bridgeNetwork{
|
||||
driver: &driver{
|
||||
config: configuration{EnableIPTables: true},
|
||||
},
|
||||
}
|
||||
err := nw.filterPortMappedOnLoopback(portBinding{
|
||||
PortBinding: types.PortBinding{
|
||||
Proto: types.TCP,
|
||||
IP: net.ParseIP("127.0.0.1"),
|
||||
HostPort: 8000,
|
||||
},
|
||||
childHostIP: net.ParseIP("127.0.0.1"),
|
||||
}, true)
|
||||
assert.NilError(t, err)
|
||||
|
||||
out, err := exec.Command("iptables-save", "-t", "raw").CombinedOutput()
|
||||
assert.NilError(t, err)
|
||||
|
||||
if tc.expLoopback0Rule {
|
||||
assert.Check(t, is.Equal(strings.Count(string(out), "-A PREROUTING"), 2))
|
||||
assert.Check(t, is.Contains(string(out), "-A PREROUTING -d 127.0.0.1/32 -i loopback0 -p tcp -m tcp --dport 8000 -j ACCEPT"))
|
||||
} else {
|
||||
assert.Check(t, is.Equal(strings.Count(string(out), "-A PREROUTING"), 1))
|
||||
assert.Check(t, !strings.Contains(string(out), "loopback0"), "There should be no rule in the raw-PREROUTING chain")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user