Skip to content

Commit c66abe4

Browse files
committed
nftabler: add mirrored WSL2 loopback0 workaround
Signed-off-by: Rob Murray <rob.murray@docker.com>
1 parent d31956b commit c66abe4

File tree

2 files changed

+54
-0
lines changed

2 files changed

+54
-0
lines changed

libnetwork/drivers/bridge/internal/nftabler/nftabler.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,12 @@ func (nft *nftabler) init(ctx context.Context, family nftables.Family) (nftables
184184
return nftables.TableRef{}, err
185185
}
186186

187+
if !nft.config.Hairpin && nft.config.WSL2Mirrored {
188+
if err := mirroredWSL2Workaround(ctx, table); err != nil {
189+
return nftables.TableRef{}, err
190+
}
191+
}
192+
187193
return table, nil
188194
}
189195

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//go:build linux
2+
3+
package nftabler
4+
5+
import (
6+
"context"
7+
8+
"github.com/docker/docker/libnetwork/internal/nftables"
9+
)
10+
11+
// mirroredWSL2Workaround adds IPv4 NAT rule if docker's host Linux appears to
12+
// be a guest running under WSL2 in with mirrored mode networking.
13+
// https://learn.microsoft.com/en-us/windows/wsl/networking#mirrored-mode-networking
14+
//
15+
// Without mirrored mode networking, or for a packet sent from Linux, packets
16+
// sent to 127.0.0.1 are processed as outgoing - they hit the nat-OUTPUT chain,
17+
// which does not jump to the nat-DOCKER chain because the rule has an exception
18+
// for "-d 127.0.0.0/8". The default action on the nat-OUTPUT chain is ACCEPT (by
19+
// default), so the packet is delivered to 127.0.0.1 on lo, where docker-proxy
20+
// picks it up and acts as a man-in-the-middle; it receives the packet and
21+
// re-sends it to the container (or acks a SYN and sets up a second TCP
22+
// connection to the container). So, the container sees packets arrive with a
23+
// source address belonging to the network's bridge, and it is able to reply to
24+
// that address.
25+
//
26+
// In WSL2's mirrored networking mode, Linux has a loopback0 device as well as lo
27+
// (which owns 127.0.0.1 as normal). Packets sent to 127.0.0.1 from Windows to a
28+
// server listening on Linux's 127.0.0.1 are delivered via loopback0, and
29+
// processed as packets arriving from outside the Linux host (which they are).
30+
//
31+
// So, these packets hit the nat-PREROUTING chain instead of nat-OUTPUT. It would
32+
// normally be impossible for a packet ->127.0.0.1 to arrive from outside the
33+
// host, so the nat-PREROUTING jump to nat-DOCKER has no exception for it. The
34+
// packet is processed by a per-bridge DNAT rule in that chain, so it is
35+
// delivered directly to the container (not via docker-proxy) with source address
36+
// 127.0.0.1, so the container can't respond.
37+
//
38+
// DNAT is normally skipped by RETURN rules in the nat-DOCKER chain for packets
39+
// arriving from any other bridge network. Similarly, this function adds (or
40+
// removes) a rule to RETURN early for packets delivered via loopback0 with
41+
// destination 127.0.0.0/8.
42+
func mirroredWSL2Workaround(ctx context.Context, table nftables.TableRef) error {
43+
// WSL2 does not (currently) support Windows<->Linux communication via ::1.
44+
if table.Family() != nftables.IPv4 {
45+
return nil
46+
}
47+
return table.Chain(natChain).AppendRule(initialRuleGroup, `iifname "loopback0" ip daddr 127.0.0.0/8 counter return`)
48+
}

0 commit comments

Comments
 (0)