From cd21af7e411c114dcc877cead6456d3dda8ef9cc Mon Sep 17 00:00:00 2001 From: Rob Murray Date: Thu, 27 Jun 2024 13:38:20 +0100 Subject: [PATCH] Do not DNAT packets from WSL2's loopback0 When running WSL2 with mirrored mode networking, add an iptables rule to skip DNAT for packets arriving on interface loopback0 that are addressed to a localhost address - they're from the Windows host. Signed-off-by: Rob Murray (cherry picked from commit f9c01034131dd3e6b80e3bd605b4b38fcda4ee00) Signed-off-by: Rob Murray --- .../drivers/bridge/setup_ip_tables_linux.go | 88 +++++++++++++++++++ .../bridge/setup_ip_tables_linux_test.go | 74 ++++++++++++++++ 2 files changed, 162 insertions(+) diff --git a/libnetwork/drivers/bridge/setup_ip_tables_linux.go b/libnetwork/drivers/bridge/setup_ip_tables_linux.go index dd8810fbbd8a0..3d6150d7556ae 100644 --- a/libnetwork/drivers/bridge/setup_ip_tables_linux.go +++ b/libnetwork/drivers/bridge/setup_ip_tables_linux.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net" + "os" "strings" "github.com/containerd/log" @@ -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 { @@ -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 } @@ -502,3 +512,81 @@ func clearConntrackEntries(nlh *netlink.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"}, + } +} diff --git a/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go b/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go index 440286cd73476..17e573b6f7c68 100644 --- a/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go +++ b/libnetwork/drivers/bridge/setup_ip_tables_linux_test.go @@ -2,6 +2,8 @@ package bridge import ( "net" + "os" + "path/filepath" "testing" "github.com/docker/docker/internal/testutils/netnsutils" @@ -10,6 +12,7 @@ import ( "github.com/docker/docker/libnetwork/netlabel" "github.com/vishvananda/netlink" "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" ) const ( @@ -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)) + }) + } +}