mirror of
https://github.com/moby/moby.git
synced 2026-07-14 03:21:18 +00:00
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 <rob.murray@docker.com>
This commit is contained in:
@@ -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"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user