Skip to content

Commit 32a8e1e

Browse files
authored
Merge pull request #596 from AkihiroSuda/fix-592
port/builtin: fix UDP forwarding for non-loopback clients (#592)
2 parents fc0bb45 + 1a61a32 commit 32a8e1e

8 files changed

Lines changed: 64 additions & 32 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ OPTIONS:
216216
Port:
217217
--port-driver value port driver for non-host network. [none, implicit (for pasta), builtin, slirp4netns, gvisor-tap-vsock(experimental)] (default: "none")
218218
--publish value, -p value [ --publish value, -p value ] publish ports. e.g. "127.0.0.1:8080:80/tcp"
219-
--source-ip-transparent preserve real client source IP using IP_TRANSPARENT (builtin port driver) (default: true)
219+
--source-ip-transparent preserve real client source IP using IP_TRANSPARENT (builtin port driver, TCP only) (default: true)
220220
221221
Process:
222222
--pidns create a PID namespace (default: false)

cmd/rootlesskit/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ See https://rootlesscontaine.rs/getting-started/common/ .
206206
}, CategoryPort),
207207
Categorize(&cli.BoolFlag{
208208
Name: "source-ip-transparent",
209-
Usage: "preserve real client source IP using IP_TRANSPARENT (builtin port driver)",
209+
Usage: "preserve real client source IP using IP_TRANSPARENT (builtin port driver, TCP only)",
210210
Value: true,
211211
}, CategoryPort),
212212
Categorize(&cli.BoolFlag{

docs/port.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ The default value is `none` (do not expose ports).
77
| `--port-driver` | Throughput | Source IP | Notes
88
|----------------------|-------------|----------|-------
99
| `slirp4netns` | 8.03 Gbps | Propagated |
10-
| `builtin` | 29.9 Gbps | Propagated (since v3.0) | In the case of Rootless Docker, userland-proxy has to be disabled for propagating the source IP.
10+
| `builtin` | 29.9 Gbps | Propagated for TCP (since v3.0) | Source IP propagation (`--source-ip-transparent`) applies to TCP only; UDP is not propagated. In the case of Rootless Docker, userland-proxy has to be disabled for propagating the source IP.
1111
| `implicit` | 37.6 Gbps | Propagated | Requires `pasta` network
1212
| `gvisor-tap-vsock` (Experimental) | 3.83 Gbps | Not propagated | Throughput is currently limited; see issue link below for improvement ideas.
1313

pkg/port/builtin/child/child.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,18 @@ func (d *childDriver) handleConnectRequest(c *net.UnixConn, req *msg.Request) er
147147

148148
var targetConn net.Conn
149149
var err error
150-
if d.sourceIPTransparent && req.SourceIP != "" && req.SourcePort != 0 && (dialProto == "tcp" || dialProto == "udp") && !net.ParseIP(req.SourceIP).IsLoopback() {
150+
// IP_TRANSPARENT source IP preservation is only supported for TCP.
151+
//
152+
// For UDP it cannot be made to work reliably: the in-netns server replies to
153+
// the real (non-local) client address, and unlike TCP there is no per-flow
154+
// accepted socket to carry the fwmark (no udp_fwmark_accept), so the reply's
155+
// route and source address are selected at send time via the main table. The
156+
// reply is therefore sent out the default route (e.g. the slirp4netns TAP)
157+
// and never reaches the transparent socket, breaking UDP forwarding entirely
158+
// for non-loopback clients (rootless-containers/rootlesskit#592). UDP falls
159+
// back to the non-transparent path below, which works for all clients but
160+
// does not preserve the client source IP.
161+
if d.sourceIPTransparent && req.SourceIP != "" && req.SourcePort != 0 && dialProto == "tcp" && !net.ParseIP(req.SourceIP).IsLoopback() {
151162
d.routingSetup.Do(func() { d.routingReady = d.setupTransparentRouting() })
152163
if !d.routingReady {
153164
d.routingWarn.Do(func() {
@@ -250,17 +261,11 @@ func (d *childDriver) setupTransparentRouting() bool {
250261

251262
// transparentDial dials targetAddr using IP_TRANSPARENT, binding to the given
252263
// source IP and port so the backend service sees the real client address.
264+
// Only TCP is supported; see the comment in handleConnectRequest.
253265
func transparentDial(dialProto, targetAddr, sourceIP string, sourcePort int) (net.Conn, error) {
254-
var localAddr net.Addr
255-
switch dialProto {
256-
case "tcp":
257-
localAddr = &net.TCPAddr{IP: net.ParseIP(sourceIP), Port: sourcePort}
258-
case "udp":
259-
localAddr = &net.UDPAddr{IP: net.ParseIP(sourceIP), Port: sourcePort}
260-
}
261266
dialer := net.Dialer{
262267
Timeout: time.Second,
263-
LocalAddr: localAddr,
268+
LocalAddr: &net.TCPAddr{IP: net.ParseIP(sourceIP), Port: sourcePort},
264269
Control: func(network, address string, c syscall.RawConn) error {
265270
var sockErr error
266271
if err := c.Control(func(fd uintptr) {

pkg/port/builtin/msg/msg.go

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,10 @@ func ConnectToChild(c *net.UnixConn, spec port.Spec, sourceAddr net.Addr) (int,
8282
ParentIP: spec.ParentIP,
8383
HostGatewayIP: hostGatewayIP(),
8484
}
85-
switch a := sourceAddr.(type) {
86-
case *net.TCPAddr:
87-
if a != nil {
88-
req.SourceIP = a.IP.String()
89-
req.SourcePort = a.Port
90-
}
91-
case *net.UDPAddr:
92-
if a != nil {
93-
req.SourceIP = a.IP.String()
94-
req.SourcePort = a.Port
95-
}
85+
// Source IP preservation (IP_TRANSPARENT) is only supported for TCP.
86+
if tcpAddr, ok := sourceAddr.(*net.TCPAddr); ok && tcpAddr != nil {
87+
req.SourceIP = tcpAddr.IP.String()
88+
req.SourcePort = tcpAddr.Port
9689
}
9790
if _, err := lowlevelmsgutil.MarshalToWriter(c, &req); err != nil {
9891
return 0, err

pkg/port/builtin/parent/udp/udp.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ func Run(socketPath string, spec port.Spec, stopCh <-chan struct{}, stoppedCh ch
2424
udpp := &udpproxy.UDPProxy{
2525
LogWriter: logWriter,
2626
Listener: c,
27-
BackendDial: func(from *net.UDPAddr) (*net.UDPConn, error) {
27+
BackendDial: func() (*net.UDPConn, error) {
2828
// get fd from the child as an SCM_RIGHTS cmsg
29-
fd, err := msg.ConnectToChildWithRetry(socketPath, spec, 10, from)
29+
fd, err := msg.ConnectToChildWithRetry(socketPath, spec, 10, nil)
3030
if err != nil {
3131
return nil, err
3232
}

pkg/port/builtin/parent/udp/udpproxy/udp_proxy.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ type connTrackMap map[connTrackKey]*net.UDPConn
4949
type UDPProxy struct {
5050
LogWriter io.Writer
5151
Listener *net.UDPConn
52-
BackendDial func(from *net.UDPAddr) (*net.UDPConn, error)
52+
BackendDial func() (*net.UDPConn, error)
5353
connTrackTable connTrackMap
5454
connTrackLock sync.Mutex
5555
}
@@ -108,7 +108,7 @@ func (proxy *UDPProxy) Run() {
108108
proxy.connTrackLock.Lock()
109109
proxyConn, hit := proxy.connTrackTable[*fromKey]
110110
if !hit {
111-
proxyConn, err = proxy.BackendDial(from)
111+
proxyConn, err = proxy.BackendDial()
112112
if err != nil {
113113
fmt.Fprintf(proxy.LogWriter, "Can't proxy a datagram to udp: %v\n", err)
114114
proxy.connTrackLock.Unlock()

pkg/port/testsuite/testsuite.go

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -512,11 +512,25 @@ func transparentUDPDialAndSend(t *testing.T, parentAddr string) string {
512512
if err != nil {
513513
t.Fatal(err)
514514
}
515+
defer conn.Close()
515516
clientAddr := conn.LocalAddr().String()
516517
if _, err := conn.Write([]byte("hello")); err != nil {
517518
t.Fatal(err)
518519
}
519-
conn.Close()
520+
// Verify the return path: the echoed reply must reach the client. This is
521+
// the regression assertion for rootless-containers/rootlesskit#592, where
522+
// UDP responses were lost for non-loopback clients.
523+
if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
524+
t.Fatal(err)
525+
}
526+
buf := make([]byte, 64)
527+
n, err := conn.Read(buf)
528+
if err != nil {
529+
t.Fatalf("did not receive UDP echo reply (issue #592 return-path regression): %v", err)
530+
}
531+
if got := string(buf[:n]); got != "hello" {
532+
t.Fatalf("unexpected UDP echo reply: %q", got)
533+
}
520534
return clientAddr
521535
}
522536

@@ -652,8 +666,6 @@ func testTransparentWithPID(t *testing.T, proto string, d port.ParentDriver, chi
652666

653667
echoCmd.Wait()
654668

655-
// Parse and verify: the echo server should see the client's non-loopback IP,
656-
// not 127.0.0.1 or a hard-coded router address.
657669
clientHost, _, err := net.SplitHostPort(clientAddr)
658670
if err != nil {
659671
t.Fatalf("failed to parse client address %q: %v", clientAddr, err)
@@ -663,8 +675,23 @@ func testTransparentWithPID(t *testing.T, proto string, d port.ParentDriver, chi
663675
t.Fatalf("failed to parse server-seen address %q: %v", serverSawAddr, err)
664676
}
665677

666-
if clientHost != serverHost {
667-
t.Errorf("IP mismatch: client=%s, server saw=%s", clientHost, serverHost)
678+
switch proto {
679+
case "tcp":
680+
// TCP preserves the real client source IP via IP_TRANSPARENT: the echo
681+
// server must see the client's non-loopback IP, not 127.0.0.1 or a
682+
// hard-coded router address.
683+
if clientHost != serverHost {
684+
t.Errorf("IP mismatch: client=%s, server saw=%s", clientHost, serverHost)
685+
}
686+
case "udp":
687+
// UDP does not preserve the source IP: it falls back to the
688+
// non-transparent path (see rootless-containers/rootlesskit#592 and the
689+
// comment in pkg/port/builtin/child). The server therefore sees a
690+
// loopback source, and the reply still reaches the client (verified by
691+
// transparentUDPDialAndSend reading the echo above).
692+
if clientHost == serverHost {
693+
t.Errorf("expected UDP source IP not to be preserved, but server saw client IP %s", serverHost)
694+
}
668695
}
669696

670697
// Cleanup
@@ -707,6 +734,13 @@ func runUDPEchoServer() {
707734
conn.WriteToUDP(buf[:n], from)
708735
}
709736

737+
// RunUDPTransparent exercises the source-ip-transparent code path for UDP. UDP
738+
// does not actually support IP_TRANSPARENT (it falls back to the non-transparent
739+
// path), so this is also the regression test for
740+
// rootless-containers/rootlesskit#592: the client connects from a non-loopback
741+
// address (which previously triggered the broken path) and the test asserts that
742+
// the echo reply is delivered back to the client. Source IP preservation is
743+
// intentionally not expected for UDP.
710744
func RunUDPTransparent(t *testing.T, pf func() port.ParentDriver) {
711745
t.Run("TestUDPTransparent", func(t *testing.T) { TestUDPTransparent(t, pf()) })
712746
}

0 commit comments

Comments
 (0)