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
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")

View File

@@ -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.

View File

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

View File

@@ -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