Merge pull request #51616 from akerouanton/fix-51591

libnet/pms/nat: don't bind IPv6 ports if not supported by port driver
This commit is contained in:
Sebastiaan van Stijn
2025-11-29 00:54:16 +01:00
committed by GitHub
4 changed files with 81 additions and 33 deletions

View File

@@ -220,7 +220,7 @@ func TestAddPortMappings(t *testing.T) {
enableProxy bool enableProxy bool
hairpin bool hairpin bool
busyPortIPv4 int busyPortIPv4 int
rootless bool newPDC func() nat.PortDriverClient
hostAddrs []string hostAddrs []string
noProxy6To4 bool noProxy6To4 bool
@@ -667,7 +667,7 @@ func TestAddPortMappings(t *testing.T) {
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}},
}, },
enableProxy: true, enableProxy: true,
rootless: true, newPDC: func() nat.PortDriverClient { return newMockPortDriverClient(true) },
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero, HostPort: firstEphemPort}, {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}, {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}, {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", name: "rootless without proxy",
epAddrV4: ctrIP4, 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: 22}},
{PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}}, {PortBinding: types.PortBinding{Proto: types.TCP, Port: 80}},
}, },
rootless: true, newPDC: func() nat.PortDriverClient { return newMockPortDriverClient(true) },
hairpin: true, hairpin: true,
expPBs: []types.PortBinding{ expPBs: []types.PortBinding{
{Proto: types.TCP, IP: ctrIP4.IP, Port: 22, HostIP: net.IPv4zero, HostPort: firstEphemPort}, {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}, {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 var pdc nat.PortDriverClient
if tc.rootless { if tc.newPDC != nil {
pdc = newMockPortDriverClient() pdc = tc.newPDC()
} }
pms := &drvregistry.PortMappers{} pms := &drvregistry.PortMappers{}
@@ -780,7 +795,7 @@ func TestAddPortMappings(t *testing.T) {
n.firewallerNetwork = fwn n.firewallerNetwork = fwn
expChildIP := func(hostIP net.IP) net.IP { expChildIP := func(hostIP net.IP) net.IP {
if !tc.rootless { if pdc == nil {
return hostIP return hostIP
} }
if hostIP.To4() == nil { if hostIP.To4() == nil {
@@ -938,16 +953,21 @@ func (p mockPortDriverPort) String() string {
type mockPortDriverClient struct { type mockPortDriverClient struct {
openPorts map[mockPortDriverPort]bool openPorts map[mockPortDriverPort]bool
supportV6 bool
} }
func newMockPortDriverClient() *mockPortDriverClient { func newMockPortDriverClient(supportV6 bool) *mockPortDriverClient {
return &mockPortDriverClient{ return &mockPortDriverClient{
openPorts: map[mockPortDriverPort]bool{}, 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 hostIP.Is6() {
if !c.supportV6 {
return netip.Addr{}
}
return netip.IPv6Loopback() return netip.IPv6Loopback()
} }
return netip.MustParseAddr("127.0.0.1") return netip.MustParseAddr("127.0.0.1")

View File

@@ -75,14 +75,38 @@ func NewPortDriverClient(ctx context.Context) (*PortDriverClient, error) {
return pdc, nil 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 // ChildHostIP returns the address that must be used in the child network
// namespace in place of hostIP, a host IP address. In particular, port // namespace in place of hostIP, a host IP address. In particular, port
// mappings from host IP addresses, and DNAT rules, must use this child // mappings from host IP addresses, and DNAT rules, must use this child
// address in place of the real host address. // address in place of the real host address. It may return an invalid
func (c *PortDriverClient) ChildHostIP(hostIP netip.Addr) netip.Addr { // 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 { if c == nil {
return hostIP 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() { if c.childIP.IsValid() {
return c.childIP return c.childIP
} }
@@ -117,20 +141,8 @@ func (c *PortDriverClient) AddPort(
if c == nil { if c == nil {
return func() error { return nil }, 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 { if _, ok := c.protos[apiProto]; !ok {
// This happens when apiProto="tcp6", portDriverName="slirp4netns", // This happens when apiProto="tcp6", portDriverName="slirp4netns",
// because "slirp4netns" port driver does not support listening on IPv6 yet. // because "slirp4netns" port driver does not support listening on IPv6 yet.

View File

@@ -135,6 +135,7 @@ func StartProxy(pb types.PortBinding,
return nil return nil
} }
stopped.Store(true) stopped.Store(true)
log.G(context.Background()).WithField("pb", pb).Debug("Stopping userland proxy")
if err := cmd.Process.Signal(os.Interrupt); err != nil { if err := cmd.Process.Signal(os.Interrupt); err != nil {
return err return err
} }

View File

@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
"slices"
"strconv" "strconv"
"github.com/containerd/log" "github.com/containerd/log"
@@ -13,12 +14,13 @@ import (
"github.com/moby/moby/v2/daemon/libnetwork/portallocator" "github.com/moby/moby/v2/daemon/libnetwork/portallocator"
"github.com/moby/moby/v2/daemon/libnetwork/portmapperapi" "github.com/moby/moby/v2/daemon/libnetwork/portmapperapi"
"github.com/moby/moby/v2/daemon/libnetwork/types" "github.com/moby/moby/v2/daemon/libnetwork/types"
"github.com/moby/moby/v2/internal/sliceutil"
) )
const driverName = "nat" const driverName = "nat"
type PortDriverClient interface { 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) 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 := len(cfg) - 1; i >= 0; i-- {
for i := range cfg { var supported bool
cfg[i] = setChildHostIP(pm.pdc, cfg[i]) if cfg[i], supported = setChildHostIP(pm.pdc, cfg[i]); !supported {
addrs = append(addrs, cfg[i].ChildHostIP) cfg = slices.Delete(cfg, i, i+1)
continue
}
} }
addrs := sliceutil.Map(cfg, func(req portmapperapi.PortBindingReq) net.IP {
return req.ChildHostIP
})
pa := portallocator.NewOSAllocator() pa := portallocator.NewOSAllocator()
allocatedPort, socks, err := pa.RequestPortsInRange(addrs, proto, int(hostPort), int(hostPortEnd)) allocatedPort, socks, err := pa.RequestPortsInRange(addrs, proto, int(hostPort), int(hostPortEnd))
if err != nil { if err != nil {
@@ -127,14 +135,21 @@ func (pm PortMapper) UnmapPorts(ctx context.Context, pbs []portmapperapi.PortBin
return errors.Join(errs...) 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 { if pdc == nil {
req.ChildHostIP = req.HostIP req.ChildHostIP = req.HostIP
return req return req, true
} }
hip, _ := netip.AddrFromSlice(req.HostIP) hip, _ := netip.AddrFromSlice(req.HostIP)
req.ChildHostIP = pdc.ChildHostIP(hip.Unmap()).AsSlice() chip := pdc.ChildHostIP(req.Proto.String(), hip.Unmap())
return req 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 // configPortDriver passes the port binding's details to rootlesskit, and updates the