diff --git a/daemon/libnetwork/drivers/bridge/port_mapping_linux_test.go b/daemon/libnetwork/drivers/bridge/port_mapping_linux_test.go index 381a5c20b2..cd8e2b063f 100644 --- a/daemon/libnetwork/drivers/bridge/port_mapping_linux_test.go +++ b/daemon/libnetwork/drivers/bridge/port_mapping_linux_test.go @@ -220,7 +220,7 @@ func TestAddPortMappings(t *testing.T) { enableProxy bool hairpin bool busyPortIPv4 int - rootless bool + newPDC func() nat.PortDriverClient hostAddrs []string noProxy6To4 bool @@ -667,7 +667,7 @@ func TestAddPortMappings(t *testing.T) { {PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}, }, enableProxy: true, - rootless: true, + newPDC: func() nat.PortDriverClient { return newMockPortDriverClient(true) }, expPBs: []types.PortBinding{ {Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero, HostPort: firstEphemPort}, {Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero, HostPort: firstEphemPort}, @@ -675,6 +675,21 @@ func TestAddPortMappings(t *testing.T) { {Proto: types.TCP, IP: ctrIP6.IP, Port: 80, HostIP: net.IPv6zero, HostPort: firstEphemPort + 1}, }, }, + { + name: "rootless, ipv6 not supported", + epAddrV4: ctrIP4, + epAddrV6: ctrIP6, + cfg: []portmapperapi.PortBindingReq{ + {PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}}, + {PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}, + }, + enableProxy: true, + newPDC: func() nat.PortDriverClient { return newMockPortDriverClient(false) }, + expPBs: []types.PortBinding{ + {Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero, HostPort: firstEphemPort}, + {Proto: types.TCP, IP: ctrIP4.IP, Port: 80, HostIP: net.IPv4zero, HostPort: firstEphemPort + 1}, + }, + }, { name: "rootless without proxy", epAddrV4: ctrIP4, @@ -683,8 +698,8 @@ func TestAddPortMappings(t *testing.T) { {PortBinding: types.PortBinding{Proto: types.TCP, Port: 22}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}, }, - rootless: true, - hairpin: true, + newPDC: func() nat.PortDriverClient { return newMockPortDriverClient(true) }, + hairpin: true, expPBs: []types.PortBinding{ {Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero, HostPort: firstEphemPort}, {Proto: types.TCP, IP: ctrIP6.IP, Port: 22, HostIP: net.IPv6zero, HostPort: firstEphemPort}, @@ -745,8 +760,8 @@ func TestAddPortMappings(t *testing.T) { } var pdc nat.PortDriverClient - if tc.rootless { - pdc = newMockPortDriverClient() + if tc.newPDC != nil { + pdc = tc.newPDC() } pms := &drvregistry.PortMappers{} @@ -780,7 +795,7 @@ func TestAddPortMappings(t *testing.T) { n.firewallerNetwork = fwn expChildIP := func(hostIP net.IP) net.IP { - if !tc.rootless { + if pdc == nil { return hostIP } if hostIP.To4() == nil { @@ -938,16 +953,21 @@ func (p mockPortDriverPort) String() string { type mockPortDriverClient struct { openPorts map[mockPortDriverPort]bool + supportV6 bool } -func newMockPortDriverClient() *mockPortDriverClient { +func newMockPortDriverClient(supportV6 bool) *mockPortDriverClient { return &mockPortDriverClient{ openPorts: map[mockPortDriverPort]bool{}, + supportV6: supportV6, } } -func (c *mockPortDriverClient) ChildHostIP(hostIP netip.Addr) netip.Addr { +func (c *mockPortDriverClient) ChildHostIP(proto string, hostIP netip.Addr) netip.Addr { if hostIP.Is6() { + if !c.supportV6 { + return netip.Addr{} + } return netip.IPv6Loopback() } return netip.MustParseAddr("127.0.0.1") diff --git a/daemon/libnetwork/internal/rlkclient/rootlesskit_client_linux.go b/daemon/libnetwork/internal/rlkclient/rootlesskit_client_linux.go index d1a1087f8f..d603124131 100644 --- a/daemon/libnetwork/internal/rlkclient/rootlesskit_client_linux.go +++ b/daemon/libnetwork/internal/rlkclient/rootlesskit_client_linux.go @@ -75,14 +75,38 @@ func NewPortDriverClient(ctx context.Context) (*PortDriverClient, error) { return pdc, nil } +// proto normalizes the protocol to match what the rootlesskit API expects. +func (c *PortDriverClient) proto(proto string, hostIP netip.Addr) string { + // proto is like "tcp", but we need to convert it to "tcp4" or "tcp6" explicitly + // for libnetwork >= 20201216 + // + // See https://github.com/moby/libnetwork/pull/2604/files#diff-8fa48beed55dd033bf8e4f8c40b31cf69d0b2cc5d4bb53cde8594670ea6c938aR20 + // See also https://github.com/rootless-containers/rootlesskit/issues/231 + apiProto := proto + if !strings.HasSuffix(apiProto, "4") && !strings.HasSuffix(apiProto, "6") { + if hostIP.Is6() { + apiProto += "6" + } else { + apiProto += "4" + } + } + return apiProto +} + // ChildHostIP returns the address that must be used in the child network // namespace in place of hostIP, a host IP address. In particular, port // mappings from host IP addresses, and DNAT rules, must use this child -// address in place of the real host address. -func (c *PortDriverClient) ChildHostIP(hostIP netip.Addr) netip.Addr { +// address in place of the real host address. It may return an invalid +// netip.Addr if the proto and IP family aren't supported. +func (c *PortDriverClient) ChildHostIP(proto string, hostIP netip.Addr) netip.Addr { if c == nil { return hostIP } + if _, ok := c.protos[c.proto(proto, hostIP)]; !ok { + // This happens when apiProto="tcp6", portDriverName="slirp4netns", + // because "slirp4netns" port driver does not support listening on IPv6 yet. + return netip.Addr{} + } if c.childIP.IsValid() { return c.childIP } @@ -117,20 +141,8 @@ func (c *PortDriverClient) AddPort( if c == nil { return func() error { return nil }, nil } - // proto is like "tcp", but we need to convert it to "tcp4" or "tcp6" explicitly - // for libnetwork >= 20201216 - // - // See https://github.com/moby/libnetwork/pull/2604/files#diff-8fa48beed55dd033bf8e4f8c40b31cf69d0b2cc5d4bb53cde8594670ea6c938aR20 - // See also https://github.com/rootless-containers/rootlesskit/issues/231 - apiProto := proto - if !strings.HasSuffix(apiProto, "4") && !strings.HasSuffix(apiProto, "6") { - if hostIP.Is6() { - apiProto += "6" - } else { - apiProto += "4" - } - } + apiProto := c.proto(proto, hostIP) if _, ok := c.protos[apiProto]; !ok { // This happens when apiProto="tcp6", portDriverName="slirp4netns", // because "slirp4netns" port driver does not support listening on IPv6 yet. diff --git a/daemon/libnetwork/portmappers/nat/mapper_linux.go b/daemon/libnetwork/portmappers/nat/mapper_linux.go index 82c35b79dc..1630c66aaa 100644 --- a/daemon/libnetwork/portmappers/nat/mapper_linux.go +++ b/daemon/libnetwork/portmappers/nat/mapper_linux.go @@ -6,6 +6,7 @@ import ( "fmt" "net" "net/netip" + "slices" "strconv" "github.com/containerd/log" @@ -13,12 +14,13 @@ import ( "github.com/moby/moby/v2/daemon/libnetwork/portallocator" "github.com/moby/moby/v2/daemon/libnetwork/portmapperapi" "github.com/moby/moby/v2/daemon/libnetwork/types" + "github.com/moby/moby/v2/internal/sliceutil" ) const driverName = "nat" type PortDriverClient interface { - ChildHostIP(hostIP netip.Addr) netip.Addr + ChildHostIP(proto string, hostIP netip.Addr) netip.Addr AddPort(ctx context.Context, proto string, hostIP, childIP netip.Addr, hostPort int) (func() error, error) } @@ -73,12 +75,18 @@ func (pm PortMapper) MapPorts(ctx context.Context, cfg []portmapperapi.PortBindi } }() - addrs := make([]net.IP, 0, len(cfg)) - for i := range cfg { - cfg[i] = setChildHostIP(pm.pdc, cfg[i]) - addrs = append(addrs, cfg[i].ChildHostIP) + for i := len(cfg) - 1; i >= 0; i-- { + var supported bool + if cfg[i], supported = setChildHostIP(pm.pdc, cfg[i]); !supported { + cfg = slices.Delete(cfg, i, i+1) + continue + } } + addrs := sliceutil.Map(cfg, func(req portmapperapi.PortBindingReq) net.IP { + return req.ChildHostIP + }) + pa := portallocator.NewOSAllocator() allocatedPort, socks, err := pa.RequestPortsInRange(addrs, proto, int(hostPort), int(hostPortEnd)) if err != nil { @@ -127,14 +135,21 @@ func (pm PortMapper) UnmapPorts(ctx context.Context, pbs []portmapperapi.PortBin return errors.Join(errs...) } -func setChildHostIP(pdc PortDriverClient, req portmapperapi.PortBindingReq) portmapperapi.PortBindingReq { +// setChildHostIP returns a modified PortBindingReq that contains the IP +// address that should be used for port allocation, firewall rules, etc. It +// returns false when the PortBindingReq isn't supported by the PortDriverClient. +func setChildHostIP(pdc PortDriverClient, req portmapperapi.PortBindingReq) (portmapperapi.PortBindingReq, bool) { if pdc == nil { req.ChildHostIP = req.HostIP - return req + return req, true } hip, _ := netip.AddrFromSlice(req.HostIP) - req.ChildHostIP = pdc.ChildHostIP(hip.Unmap()).AsSlice() - return req + chip := pdc.ChildHostIP(req.Proto.String(), hip.Unmap()) + if !chip.IsValid() { + return req, false + } + req.ChildHostIP = chip.AsSlice() + return req, true } // configPortDriver passes the port binding's details to rootlesskit, and updates the