mirror of
https://github.com/moby/moby.git
synced 2026-01-11 10:41:43 +00:00
libnet/pms/nat: don't bind IPv6 ports if not supported by port driver
In rootless mode, the Engine needs to call the rootless port driver to
know which IP address it should bind to inside of its network namespace.
The slirp4netns port drivers doesn't support binding to IPv6 address, so
we need to detect that before listening on the port.
Before commit 201968cc0, this wasn't a problem because the Engine was
binding the port, then calling rootless port driver to learn whether the
proto/IP family was supported, and listen on the port if so.
Starting with that commit, the Engine does bind + listen in one go, and
then calls the port driver — this is too late. Fix the bug by checking
if the port driver supports the PortBindingReq, and only allocate the
port if so.
Signed-off-by: Albin Kerouanton <albin.kerouanton@docker.com>
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user