Skip to content

Commit

Permalink
Merge pull request moby#48075 from robmry/wsl2_mirrored_loopback0_wor…
Browse files Browse the repository at this point in the history
…karound

Do not DNAT packets from WSL2's loopback0
  • Loading branch information
robmry authored Sep 17, 2024
2 parents fe09cab + f9c0103 commit d89eaad
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 0 deletions.
88 changes: 88 additions & 0 deletions libnetwork/drivers/bridge/setup_ip_tables_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net"
"os"
"strings"

"github.com/containerd/log"
Expand Down Expand Up @@ -32,6 +33,11 @@ const (
IsolationChain2 = "DOCKER-ISOLATION-STAGE-2"
)

// Path to the executable installed in Linux under WSL2 that reports on
// WSL config. https://github.com/microsoft/WSL/releases/tag/2.0.4
// Can be modified by tests.
var wslinfoPath = "/usr/bin/wslinfo"

func setupIPChains(config configuration, version iptables.IPVersion) (natChain *iptables.ChainInfo, filterChain *iptables.ChainInfo, isolationChain1 *iptables.ChainInfo, isolationChain2 *iptables.ChainInfo, retErr error) {
// Sanity check.
if version == iptables.IPv4 && !config.EnableIPTables {
Expand Down Expand Up @@ -99,6 +105,10 @@ func setupIPChains(config configuration, version iptables.IPVersion) (natChain *
return nil, nil, nil, nil, err
}

if err := mirroredWSL2Workaround(config, version); err != nil {
return nil, nil, nil, nil, err
}

return natChain, filterChain, isolationChain1, isolationChain2, nil
}

Expand Down Expand Up @@ -502,3 +512,81 @@ func clearConntrackEntries(nlh nlwrap.Handle, ep *bridgeEndpoint) {
iptables.DeleteConntrackEntries(nlh, ipv4List, ipv6List)
iptables.DeleteConntrackEntriesByPort(nlh, types.UDP, udpPorts)
}

// mirroredWSL2Workaround adds or removes an IPv4 NAT rule, depending on whether
// docker's host Linux appears to be a guest running under WSL2 in with mirrored
// mode networking.
// https://learn.microsoft.com/en-us/windows/wsl/networking#mirrored-mode-networking
//
// Without mirrored mode networking, or for a packet sent from Linux, packets
// sent to 127.0.0.1 are processed as outgoing - they hit the nat-OUTPUT chain,
// which does not jump to the nat-DOCKER chain because the rule has an exception
// for "-d 127.0.0.0/8". The default action on the nat-OUTPUT chain is ACCEPT (by
// default), so the packet is delivered to 127.0.0.1 on lo, where docker-proxy
// picks it up and acts as a man-in-the-middle; it receives the packet and
// re-sends it to the container (or acks a SYN and sets up a second TCP
// connection to the container). So, the container sees packets arrive with a
// source address belonging to the network's bridge, and it is able to reply to
// that address.
//
// In WSL2's mirrored networking mode, Linux has a loopback0 device as well as lo
// (which owns 127.0.0.1 as normal). Packets sent to 127.0.0.1 from Windows to a
// server listening on Linux's 127.0.0.1 are delivered via loopback0, and
// processed as packets arriving from outside the Linux host (which they are).
//
// So, these packets hit the nat-PREROUTING chain instead of nat-OUTPUT. It would
// normally be impossible for a packet ->127.0.0.1 to arrive from outside the
// host, so the nat-PREROUTING jump to nat-DOCKER has no exception for it. The
// packet is processed by a per-bridge DNAT rule in that chain, so it is
// delivered directly to the container (not via docker-proxy) with source address
// 127.0.0.1, so the container can't respond.
//
// DNAT is normally skipped by RETURN rules in the nat-DOCKER chain for packets
// arriving from any other bridge network. Similarly, this function adds (or
// removes) a rule to RETURN early for packets delivered via loopback0 with
// destination 127.0.0.0/8.
func mirroredWSL2Workaround(config configuration, ipv iptables.IPVersion) error {
// WSL2 does not (currently) support Windows<->Linux communication via ::1.
if ipv != iptables.IPv4 {
return nil
}
return programChainRule(mirroredWSL2Rule(), "WSL2 loopback", insertMirroredWSL2Rule(config))
}

// insertMirroredWSL2Rule returns true if the NAT rule for mirrored WSL2 workaround
// is required. It is required if:
// - the userland proxy is running. If not, there's nothing on the host to catch
// the packet, so the loopback0 rule as wouldn't be useful. However, without
// 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.)
func insertMirroredWSL2Rule(config configuration) bool {
if !config.EnableUserlandProxy || config.UserlandProxyPath == "" {
return false
}
if _, err := netlink.LinkByName("loopback0"); err != nil {
if !errors.As(err, &netlink.LinkNotFoundError{}) {
log.G(context.TODO()).WithError(err).Warn("Failed to check for WSL interface")
}
return false
}
stat, err := os.Stat(wslinfoPath)
if err != nil {
return false
}
return stat.Mode().IsRegular() && (stat.Mode().Perm()&0111) != 0
}

func mirroredWSL2Rule() iptRule {
return iptRule{
ipv: iptables.IPv4,
table: iptables.Nat,
chain: DockerChain,
args: []string{"-i", "loopback0", "-d", "127.0.0.0/8", "-j", "RETURN"},
}
}
74 changes: 74 additions & 0 deletions libnetwork/drivers/bridge/setup_ip_tables_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package bridge

import (
"net"
"os"
"path/filepath"
"testing"

"github.com/docker/docker/internal/nlwrap"
Expand All @@ -10,6 +12,7 @@ import (
"github.com/docker/docker/libnetwork/iptables"
"github.com/docker/docker/libnetwork/netlabel"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)

const (
Expand Down Expand Up @@ -374,3 +377,74 @@ func TestOutgoingNATRules(t *testing.T) {
})
}
}

func TestMirroredWSL2Workaround(t *testing.T) {
for _, tc := range []struct {
desc string
loopback0 bool
userlandProxy bool
wslinfoPerm os.FileMode // 0 for no-file
expLoopback0Rule bool
}{
{
desc: "No loopback0",
},
{
desc: "WSL2 mirrored",
loopback0: true,
userlandProxy: true,
wslinfoPerm: 0777,
expLoopback0Rule: true,
},
{
desc: "loopback0 but wslinfo not executable",
loopback0: true,
userlandProxy: true,
wslinfoPerm: 0666,
},
{
desc: "loopback0 but no wslinfo",
loopback0: true,
userlandProxy: true,
},
{
desc: "loopback0 but no userland proxy",
loopback0: true,
wslinfoPerm: 0777,
},
} {
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)
}

config := configuration{EnableIPTables: true}
if tc.userlandProxy {
config.UserlandProxyPath = "some-proxy"
config.EnableUserlandProxy = true
}
_, _, _, _, err := setupIPChains(config, iptables.IPv4)
assert.NilError(t, err)
assert.Check(t, is.Equal(mirroredWSL2Rule().Exists(), tc.expLoopback0Rule))
})
}
}

0 comments on commit d89eaad

Please sign in to comment.