Repository: moby/moby (language: go)

Issue:
v28 regression: `protocol "tcp" is not supported by the RootlessKit port driver "slirp4netns", discarding request for "[::]:8080"`
### Description

`DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns` no longer works with Docker v28.0.0.
```console
$ docker run -p 8080:80 nginx:alpine
docker: Error response from daemon: driver failed programming external connectivity on endpoint hungry_darwin (57b84dd9bada5b76efdeb749a36675ead1cd67c2eab22912b0ea48b9839fdb08): protocol "tcp" is not supported by the RootlessKit port driver "slirp4netns", discarding request for "[::]:8080"

Run 'docker run --help' for more information
```

### Reproduce

```bash
dockerd-rootless-setuptool.sh install
```
`docker run -p` still works here, but it fails after setting `DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns`

```bash
mkdir -p ~/.config/systemd/user/docker.service.d
cat <<EOF > ~/.config/systemd/user/docker.service.d/override.conf
[Service]
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns"
EOF
systemctl --user daemon-reload 
systemctl --user restart docker
```

### Expected behavior

It should still work

### docker version

```bash
$ docker version
Client: Docker Engine - Community
 Version:           28.0.0
 API version:       1.48
 Go version:        go1.23.6
 Git commit:        f9ced58
 Built:             Wed Feb 19 22:11:33 2025
 OS/Arch:           linux/arm64
 Context:           rootless

Server: Docker Engine - Community
 Engine:
  Version:          28.0.0
  API version:      1.48 (minimum version 1.24)
  Go version:       go1.23.6
  Git commit:       af898ab
  Built:            Wed Feb 19 22:11:33 2025
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          1.7.25
  GitCommit:        bcc810d6b9066471b0b6fa75f557a15a1cbf31bb
 runc:
  Version:          1.2.4
  GitCommit:        v1.2.4-0-g6c52b3f
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
 rootlesskit:
  Version:          2.3.2
  ApiVersion:       1.1.1
  NetworkDriver:    slirp4netns
  PortDriver:       slirp4netns
  StateDir:         /run/user/501/dockerd-rootless
 slirp4netns:
  Version:          1.2.1
  GitCommit:        09e31e92fa3d2a1d3ca261adaeb012c8d75a8194
```

### docker info

```bash
Client: Docker Engine - Community
 Version:    28.0.0
 Context:    rootless
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.21.0
    Path:     /usr/libexec/docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  v2.33.0
    Path:     /usr/libexec/docker/cli-plugins/docker-compose

Server:
 Containers: 5
  Running: 0
  Paused: 0
  Stopped: 5
 Images: 1
 Server Version: 28.0.0
 Storage Driver: overlay2
  Backing Filesystem: extfs
  Supports d_type: true
  Using metacopy: false
  Native Overlay Diff: true
  userxattr: true
 Logging Driver: json-file
 Cgroup Driver: systemd
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: bcc810d6b9066471b0b6fa75f557a15a1cbf31bb
 runc version: v1.2.4-0-g6c52b3f
 init version: de40ad0
 Security Options:
  seccomp
   Profile: builtin
  rootless
  cgroupns
 Kernel Version: 6.8.0-51-generic
 Operating System: Ubuntu 24.04.1 LTS
 OSType: linux
 Architecture: aarch64
 CPUs: 4
 Total Memory: 3.814GiB
 Name: lima-docker
 ID: ea17e0ba-f1e8-4d02-8576-21a1f620e4d3
 Docker Root Dir: /home/suda.linux/.local/share/docker
 Debug Mode: false
 Experimental: false
 Insecure Registries:
  ::1/128
  127.0.0.0/8
 Live Restore Enabled: false
```

### Additional Info

### Workaround

Downgrade Docker to v27.5

```bash
sudo apt-get install docker-ce=5:27.5.1-1~ubuntu.24.04~noble docker-ce-rootless-extras=5:27.5.1-1~ubuntu.24.04~noble docker-ce-cli=5:27.5.0-1~ubuntu.24.04~noble
systemctl --user restart docker
```


Discussion / hints:
Caused in:
- #48132

https://github.com/moby/moby/pull/48132/commits/f1e0746c08bf4daa3d9dba6dc6bf6ae928db7eaf#diff-b95aa0c1044b8c00c55e56f126b449763f64d2252bbc8073f6e2aaf211e919f4R96

Fix patch:
diff --git a/libnetwork/drivers/bridge/internal/rlkclient/rootlesskit_client_linux.go b/libnetwork/drivers/bridge/internal/rlkclient/rootlesskit_client_linux.go
index 538f7ace904c2..62f8162a5048e 100644
--- a/libnetwork/drivers/bridge/internal/rlkclient/rootlesskit_client_linux.go
+++ b/libnetwork/drivers/bridge/internal/rlkclient/rootlesskit_client_linux.go
@@ -11,11 +11,9 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"net"
 	"net/netip"
 	"os"
 	"path/filepath"
-	"strconv"
 	"strings"
 
 	"github.com/rootless-containers/rootlesskit/v2/pkg/api/client"
@@ -94,8 +92,21 @@ func (c *PortDriverClient) ChildHostIP(hostIP netip.Addr) netip.Addr {
 	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,
@@ -128,10 +139,11 @@ func (c *PortDriverClient) AddPort(
 		// even when network driver is set to "slirp4netns".
 		//
 		// Most users are using "builtin" port driver and will not see this warning.
-		return nil, fmt.Errorf("protocol %q is not supported by the RootlessKit port driver %q, discarding request for %q",
-			proto,
-			c.portDriverName,
-			net.JoinHostPort(hostIP.String(), strconv.Itoa(hostPort)))
+		err := &ProtocolUnsupportedError{
+			apiProto:       apiProto,
+			portDriverName: c.portDriverName,
+		}
+		return nil, err
 	}
 
 	pm := c.client.PortManager()
diff --git a/libnetwork/drivers/bridge/port_mapping_linux.go b/libnetwork/drivers/bridge/port_mapping_linux.go
index d4c5e46056c69..aa5a3727e5ad3 100644
--- a/libnetwork/drivers/bridge/port_mapping_linux.go
+++ b/libnetwork/drivers/bridge/port_mapping_linux.go
@@ -16,6 +16,7 @@ import (
 	"unsafe"
 
 	"github.com/containerd/log"
+	"github.com/docker/docker/libnetwork/drivers/bridge/internal/rlkclient"
 	"github.com/docker/docker/libnetwork/iptables"
 	"github.com/docker/docker/libnetwork/netutils"
 	"github.com/docker/docker/libnetwork/portallocator"
@@ -46,6 +47,9 @@ type portBinding struct {
 	// stopProxy is a function to stop the userland proxy for this binding,
 	// if a proxy has been started - else nil.
 	stopProxy func() error
+	// rootlesskitUnsupported is set to true when the port binding is not
+	// supported by the port driver of RootlessKit.
+	rootlesskitUnsupported bool
 }
 
 // childPortBinding is pb.PortBinding, with the host address the daemon
@@ -189,6 +193,14 @@ func (n *bridgeNetwork) addPortMappings(
 			}
 			bindings[i].portDriverRemove, err = pdc.AddPort(ctx, b.Proto.String(), hip, chip, int(b.HostPort))
 			if err != nil {
+				var pErr *rlkclient.ProtocolUnsupportedError
+				if errors.As(err, &pErr) {
+					log.G(ctx).WithFields(log.Fields{
+						"error": pErr,
+					}).Warnf("discarding request for %q", net.JoinHostPort(hip.String(), strconv.Itoa(int(b.HostPort))))
+					bindings[i].rootlesskitUnsupported = true
+					continue
+				}
 				return nil, err
 			}
 		}
@@ -208,7 +220,7 @@ func (n *bridgeNetwork) addPortMappings(
 		somaxconn = -1 // silently capped to "/proc/sys/net/core/somaxconn"
 	}
 	for i := range bindings {
-		if bindings[i].boundSocket == nil {
+		if bindings[i].boundSocket == nil || bindings[i].rootlesskitUnsupported {
 			continue
 		}
 		if bindings[i].Proto == types.TCP {


Test patch: