Files
moby/daemon/libnetwork/internal/rlkclient/rootlesskit_client_linux.go
Albin Kerouanton 52fae09ec0 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>
2025-11-28 19:40:34 +01:00

181 lines
5.7 KiB
Go

// RootlessKit integration - if required by RootlessKit's port driver, let it know
// about port mappings as they're added and removed.
//
// This is based on / copied from rootlesskit-docker-proxy, which was previously
// installed as a proxy for docker-proxy:
// https://github.com/rootless-containers/rootlesskit/blob/4fb2e2cb80bf13eb28b7f2a4317b63406b89ad32/cmd/rootlesskit-docker-proxy/main.go
package rlkclient
import (
"context"
"errors"
"fmt"
"net/netip"
"os"
"path/filepath"
"strings"
"github.com/rootless-containers/rootlesskit/v2/pkg/api/client"
"github.com/rootless-containers/rootlesskit/v2/pkg/port"
)
type PortDriverClient struct {
client client.Client
portDriverName string
protos map[string]struct{}
childIP netip.Addr
}
func NewPortDriverClient(ctx context.Context) (*PortDriverClient, error) {
stateDir := os.Getenv("ROOTLESSKIT_STATE_DIR")
if stateDir == "" {
return nil, errors.New("$ROOTLESSKIT_STATE_DIR needs to be set")
}
socketPath := filepath.Join(stateDir, "api.sock")
c, err := client.New(socketPath)
if err != nil {
return nil, fmt.Errorf("error while connecting to RootlessKit API socket: %w", err)
}
info, err := c.Info(ctx)
if err != nil {
return nil, fmt.Errorf("failed to call info API, probably RootlessKit binary is too old (needs to be v0.14.0 or later): %w", err)
}
// info.PortDriver is currently nil for "none" and "implicit", but this may change in future
if info.PortDriver == nil || info.PortDriver.Driver == "none" || info.PortDriver.Driver == "implicit" {
return nil, nil
}
pdc := &PortDriverClient{
client: c,
portDriverName: info.PortDriver.Driver,
}
if info.PortDriver.DisallowLoopbackChildIP {
// i.e., port-driver="slirp4netns"
if info.NetworkDriver.ChildIP == nil {
return nil, fmt.Errorf("RootlessKit port driver (%q) does not allow loopback child IP, but network driver (%q) has no non-loopback IP",
info.PortDriver.Driver, info.NetworkDriver.Driver)
}
childIP, ok := netip.AddrFromSlice(info.NetworkDriver.ChildIP)
if !ok {
return nil, fmt.Errorf("unable to use child IP %s from network driver (%q)",
info.NetworkDriver.ChildIP, info.NetworkDriver.Driver)
}
pdc.childIP = childIP.Unmap()
}
pdc.protos = make(map[string]struct{}, len(info.PortDriver.Protos))
for _, p := range info.PortDriver.Protos {
pdc.protos[p] = struct{}{}
}
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. 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
}
if hostIP.Is6() {
return netip.IPv6Loopback()
}
return netip.MustParseAddr("127.0.0.1")
}
// ProtocolUnsupportedError is returned when apiProto is not supported by portDriverName.
type ProtocolUnsupportedError struct {
apiProto string
portDriverName string
}
func (e *ProtocolUnsupportedError) Error() string {
return fmt.Sprintf("protocol %q is not supported by the RootlessKit port driver %q",
e.apiProto, e.portDriverName)
}
// AddPort makes a request to RootlessKit asking it to set up a port
// mapping between a host IP address and a child host IP address.
//
// AddPort may return [ProtocolUnsupportedError].
func (c *PortDriverClient) AddPort(
ctx context.Context,
proto string,
hostIP netip.Addr,
childIP netip.Addr,
hostPort int,
) (func() error, error) {
if c == nil {
return func() error { return nil }, nil
}
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.
//
// Note that "slirp4netns" port driver is not used by default,
// even when network driver is set to "slirp4netns".
//
// Most users are using "builtin" port driver and will not see this warning.
err := &ProtocolUnsupportedError{
apiProto: apiProto,
portDriverName: c.portDriverName,
}
return nil, err
}
pm := c.client.PortManager()
p := port.Spec{
Proto: apiProto,
ParentIP: hostIP.String(),
ParentPort: hostPort,
ChildIP: childIP.String(),
ChildPort: hostPort,
}
st, err := pm.AddPort(ctx, p)
if err != nil {
return nil, fmt.Errorf("error while calling RootlessKit PortManager.AddPort(): %w", err)
}
deferFunc := func() error {
if dErr := pm.RemovePort(context.WithoutCancel(ctx), st.ID); dErr != nil {
return fmt.Errorf("error while calling RootlessKit PortManager.RemovePort(): %w", dErr)
}
return nil
}
return deferFunc, nil
}