Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,14 @@ jobs:
run: |
docker run --rm --security-opt seccomp=unconfined --security-opt apparmor=unconfined --device /dev/net/tun \
rootlesskit:test-integration ./benchmark-iperf3-port.sh implicit --net=pasta --detach-netns
- name: "Benchmark: TCP Ports (network driver=pasta, port driver=pesto)"
run: |
docker run --rm --security-opt seccomp=unconfined --security-opt apparmor=unconfined --device /dev/net/tun \
rootlesskit:test-integration ./benchmark-iperf3-port.sh pesto --net=pasta
- name: "Benchmark: TCP Ports (network driver=pasta, port driver=pesto) with detach-netns"
run: |
docker run --rm --security-opt seccomp=unconfined --security-opt apparmor=unconfined --device /dev/net/tun \
rootlesskit:test-integration ./benchmark-iperf3-port.sh pesto --net=pasta --detach-netns
- name: "Benchmark: TCP Ports (network driver=gvisor-tap-vsock, port driver=gvisor-tap-vsock)"
run: |
docker run --rm --security-opt seccomp=unconfined --security-opt apparmor=unconfined --device /dev/net/tun \
Expand All @@ -206,6 +214,14 @@ jobs:
run: |
docker run --rm --security-opt seccomp=unconfined --security-opt apparmor=unconfined --device /dev/net/tun \
rootlesskit:test-integration ./benchmark-iperf3-port-udp.sh implicit --net=pasta --detach-netns
- name: "Benchmark: UDP Ports (network driver=pasta, port driver=pesto)"
run: |
docker run --rm --security-opt seccomp=unconfined --security-opt apparmor=unconfined --device /dev/net/tun \
rootlesskit:test-integration ./benchmark-iperf3-port-udp.sh pesto --net=pasta
- name: "Benchmark: UDP Ports (network driver=pasta, port driver=pesto) with detach-netns"
run: |
docker run --rm --security-opt seccomp=unconfined --security-opt apparmor=unconfined --device /dev/net/tun \
rootlesskit:test-integration ./benchmark-iperf3-port-udp.sh pesto --net=pasta --detach-netns
- name: "Benchmark: UDP Ports (port driver=builtin)"
run: |
docker run --rm --security-opt seccomp=unconfined --security-opt apparmor=unconfined --device /dev/net/tun \
Expand Down Expand Up @@ -282,6 +298,13 @@ jobs:
docker exec test docker info
docker exec test ./integration-docker.sh
docker rm -f test
- name: "Docker Integration test: net=pasta, port-driver=pesto"
run: |
docker run -d --name test --network custom --privileged -e DOCKERD_ROOTLESS_ROOTLESSKIT_NET=pasta -e DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=pesto rootlesskit:test-integration-docker
sleep 5
docker exec test docker info
docker exec test ./integration-docker.sh
docker rm -f test
- name: "Docker Integration test: net=gvisor-tap-vsock, port-driver=builtin"
run: |
docker run -d --name test --network custom --privileged -e DOCKERD_ROOTLESS_ROOTLESSKIT_NET=gvisor-tap-vsock -e DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=builtin rootlesskit:test-integration-docker
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ ARG UBUNTU_VERSION=24.04
ARG SHADOW_VERSION=4.17.4
ARG SLIRP4NETNS_VERSION=v1.3.4
ARG VPNKIT_VERSION=0.6.0
ARG PASST_VERSION=2026_01_20.386b5f5
ARG PASST_VERSION=2026_07_28.f8df3f1
ARG DOCKER_VERSION=29.5.2
ARG DOCKER_CHANNEL=stable

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ OPTIONS:

Network [pasta]:
--pasta-binary value path of pasta binary for --net=pasta (default: "pasta")
--pesto-binary value path of pesto binary for --port-driver=pesto (default: "pesto")

Network [slirp4netns]:
--slirp4netns-binary value path of slirp4netns binary for --net=slirp4netns (default: "slirp4netns")
Expand All @@ -214,7 +215,7 @@ OPTIONS:
--vpnkit-binary value path of VPNKit binary for --net=vpnkit (default: "vpnkit")

Port:
--port-driver value port driver for non-host network. [none, implicit (for pasta), builtin, slirp4netns, gvisor-tap-vsock(experimental)] (default: "none")
--port-driver value port driver for non-host network. [none, implicit (for pasta), builtin, slirp4netns, gvisor-tap-vsock(experimental), pesto(experimental)] (default: "none")
--publish value, -p value [ --publish value, -p value ] publish ports. e.g. "127.0.0.1:8080:80/tcp"
--source-ip-transparent preserve real client source IP using IP_TRANSPARENT (builtin port driver, TCP only) (default: true)

Expand Down
39 changes: 35 additions & 4 deletions cmd/rootlesskit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/rootless-containers/rootlesskit/v3/pkg/parent"
"github.com/rootless-containers/rootlesskit/v3/pkg/port/builtin"
gvisortapvsock_port "github.com/rootless-containers/rootlesskit/v3/pkg/port/gvisortapvsock"
pasta_port "github.com/rootless-containers/rootlesskit/v3/pkg/port/pasta"
"github.com/rootless-containers/rootlesskit/v3/pkg/port/portutil"
slirp4netns_port "github.com/rootless-containers/rootlesskit/v3/pkg/port/slirp4netns"
"github.com/rootless-containers/rootlesskit/v3/pkg/systemd/activation"
Expand Down Expand Up @@ -108,6 +109,9 @@ See https://rootlesscontaine.rs/getting-started/common/ .
if gvisortapvsock_port.Available {
portDrivers = append(portDrivers, "gvisor-tap-vsock(experimental)")
}
if pasta_port.Available {
portDrivers = append(portDrivers, "pesto(experimental)")
}
portDriversHelp := strings.Join(portDrivers, ", ")

app.Flags = []cli.Flag{
Expand All @@ -134,6 +138,11 @@ See https://rootlesscontaine.rs/getting-started/common/ .
Usage: "path of pasta binary for --net=pasta",
Value: "pasta",
}, CategoryPasta),
Categorize(&cli.StringFlag{
Name: "pesto-binary",
Usage: "path of pesto binary for --port-driver=pesto",
Value: "pesto",
}, CategoryPasta),
Categorize(&cli.StringFlag{
Name: "slirp4netns-binary",
Usage: "path of slirp4netns binary for --net=slirp4netns",
Expand Down Expand Up @@ -431,6 +440,14 @@ func createParentOpt(clicontext *cli.Context) (parent.Opt, error) {
if clicontext.String("port-driver") == "slirp4netns" {
slirp4netnsAPISocketPath = filepath.Join(opt.StateDir, ".s4nn.sock")
}
portDriver := clicontext.String("port-driver")
if portDriver == "pasta" {
return opt, errors.New(`unknown port driver "pasta": did you mean "pesto"?`)
}
pestoSocketPath := ""
if portDriver == "pesto" {
pestoSocketPath = filepath.Join(opt.StateDir, ".pasta.sock")
}
switch s := clicontext.String("net"); s {
case "host":
// NOP
Expand Down Expand Up @@ -471,14 +488,14 @@ func createParentOpt(clicontext *cli.Context) (parent.Opt, error) {
}
var implicitPortForward bool
switch portDriver := clicontext.String("port-driver"); portDriver {
case "none":
case "none", "pesto":
implicitPortForward = false
case "implicit":
implicitPortForward = true
default:
return opt, errors.New("network \"pasta\" requires port driver \"none\" or \"implicit\"")
return opt, errors.New("network \"pasta\" requires port driver \"none\" or \"implicit\" or \"pesto\"")
}
opt.NetworkDriver, err = pasta.NewParentDriver(&logrusDebugWriter{label: "network/pasta"}, binary, mtu, ipnet, ifname, disableHostLoopback, ipv6, implicitPortForward)
opt.NetworkDriver, err = pasta.NewParentDriver(&logrusDebugWriter{label: "network/pasta"}, binary, mtu, ipnet, ifname, pestoSocketPath, disableHostLoopback, ipv6, implicitPortForward)
if err != nil {
return opt, err
}
Expand Down Expand Up @@ -610,7 +627,7 @@ func createParentOpt(clicontext *cli.Context) (parent.Opt, error) {
}
case "implicit":
if clicontext.String("net") != "pasta" {
return opt, errors.New("port driver requires pasta network")
return opt, errors.New("port driver \"implicit\" requires --net=pasta")
}
// NOP
case "slirp4netns":
Expand Down Expand Up @@ -638,6 +655,18 @@ func createParentOpt(clicontext *cli.Context) (parent.Opt, error) {
if err != nil {
return opt, err
}
case "pesto":
if clicontext.String("net") != "pasta" {
return opt, errors.New("port driver \"pesto\" requires --net=pasta")
}
binary := clicontext.String("pesto-binary")
if _, err := exec.LookPath(binary); err != nil {
return opt, err
}
opt.PortDriver, err = pasta_port.NewParentDriver(&logrusDebugWriter{label: "port/pesto"}, binary, pestoSocketPath)
if err != nil {
return opt, err
}
default:
return opt, fmt.Errorf("unknown port driver: %s", s)
}
Expand Down Expand Up @@ -731,6 +760,8 @@ func createChildOpt(clicontext *cli.Context) (child.Opt, error) {
opt.PortDriver = builtin.NewChildDriver(&logrusDebugWriter{label: "port/builtin"})
case "gvisor-tap-vsock":
opt.PortDriver = gvisortapvsock_port.NewChildDriver()
case "pesto":
opt.PortDriver = pasta_port.NewChildDriver()
default:
return opt, fmt.Errorf("unknown port driver: %s", s)
}
Expand Down
6 changes: 2 additions & 4 deletions docs/network.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ If `--disable-host-loopback` is not specified, ports listening on 127.0.0.1 in t
### `--net=pasta` (experimental)

`--net=pasta` (since RootlessKit v2.0, EXPERIMENTAL) uses [pasta (passt)](https://passt.top/passt/).
`--net=pasta` is expected to be used in conjunction with `--port-driver=implicit`.
`--net=pasta` is expected to be used in conjunction with `--port-driver=pesto` or `--port-driver=implicit`.

> **Note**
> `--net=pasta` needs [pasta (passt)](https://passt.top/passt/) `2023_06_25.32660ce` or later.
Expand All @@ -170,9 +170,7 @@ Pros:
* Supports ICMP Echo (`ping`) when `/proc/sys/net/ipv4/ping_group_range` is configured
* TCP port forwarding (`--port-driver=implicit`) is very fast
* TCP port forwarding (`--port-driver=implicit`) can retain source IP addresses

Cons:
* Lacks API for explicit port forwarding (`rootlessctl (list-ports|add-ports|remove-ports)`)
* Explicit port forwarding via `rootlessctl (list-ports|add-ports|remove-ports)` is supported with `--port-driver=pesto` (experimental, IPv4-only, requires pasta `2026_05_07.1afd4ed` or later)

The network configuration for pasta is similar to slirp4netns.
As in `--net=slirp4netns`, specifying `--copy-up=/etc` and `--disable-host-loopback` is highly recommended.
Expand Down
9 changes: 8 additions & 1 deletion docs/port.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@ The default value is `none` (do not expose ports).
| `slirp4netns` | 8.03 Gbps | Propagated |
| `builtin` | 29.9 Gbps | Propagated for TCP (since v3.0) | Source IP propagation (`--source-ip-transparent`) applies to TCP only; UDP is not propagated. In the case of Rootless Docker, userland-proxy has to be disabled for propagating the source IP.
| `implicit` | 37.6 Gbps | Propagated | Requires `pasta` network
| `pesto` (Experimental) | TBD | Propagated | Requires `pasta` network and passt `2026_05_07.1afd4ed` or later. Supports explicit port management through `rootlessctl`.
| `gvisor-tap-vsock` (Experimental) | 3.83 Gbps | Not propagated | Throughput is currently limited; see issue link below for improvement ideas.

Benchmark: iperf3 from the parent to the child is measured on GitHub Actions ([Apr 10, 2026](https://github.com/rootless-containers/rootlesskit/actions/runs/24200485791/job/70642399211))

The `builtin` driver is fast and should be the best choice for most use cases.

For [`pasta`](./network.md) networks, the `implicit` port driver is the best choice.
For [`pasta`](./network.md) networks, choose the port driver based on how
ports should be exposed:

* Use `pesto` to expose only explicitly published ports using `rootlessctl list-ports`, `add-ports`, and `remove-ports`.
* Use `implicit` to automatically expose ports listened on in the network namespace. This may expose ports that were not intended to be reachable from the host.

The `pesto` port driver is experimental and currently supports IPv4 port forwarding only. Both the `pasta` and `pesto` executables must be installed.

> [!NOTE]
> The `gvisor-tap-vsock` port driver is experimental.
Expand Down
2 changes: 2 additions & 0 deletions hack/benchmark-iperf3-port.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ function benchmark::iperf3::port() {
portid=$($rootlessctl add-ports 127.0.0.1:5201:5201/tcp)
$rootlessctl list-ports
fi
# wait for the port to be bound
sleep 3
$IPERF3C 127.0.0.1
if [ $portdriver != "implicit" ]; then
$rootlessctl remove-ports $portid
Expand Down
141 changes: 138 additions & 3 deletions hack/integration-port.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#!/bin/bash
source $(realpath $(dirname $0))/common.inc.sh

ROOTLESSCTL="rootlessctl"

# test_port PORT_DRIVER CURL_URL EXPECTATION [ROOTLESSKIT ARGS...]
function test_port() {
args="$@"
Expand All @@ -12,19 +14,30 @@ function test_port() {
shift
rootlesskit_args="$@"
INFO "Testing port_driver=\"${port_driver}\" curl_url=\"${curl_url}\" expectation=\"${expectation}\" rootlesskit_args=\"${rootlesskit_args}\""
net=slirp4netns
if [ "${port_driver}" = "pesto" ]; then
# The pesto port driver is specific to the pasta network driver
net=pasta
fi
tmp=$(mktemp -d)
state_dir=${tmp}/state
html_dir=${tmp}/html
mkdir -p ${html_dir}
echo "test_port ($args)" >${html_dir}/index.html
httpd="busybox httpd -f -v -p 80 -h ${html_dir}"
if echo "${rootlesskit_args}" | grep -q -- --detach-netns; then
# With --detach-netns, the child command runs in the host's network
# namespace, so the server has to enter the detached netns explicitly.
httpd="nsenter -n${state_dir}/netns ${httpd}"
fi
$ROOTLESSKIT \
--state-dir=${state_dir} \
--net=slirp4netns \
--net=${net} \
--disable-host-loopback \
--copy-up=/etc \
--port-driver=${port_driver} \
${rootlesskit_args} \
busybox httpd -f -v -p 80 -h ${html_dir} \
${httpd} \
2>&1 &
pid=$!
sleep 1
Expand All @@ -49,8 +62,108 @@ function test_port() {
fi

INFO "Test pasing, stopping httpd (\"exit status 255\" is negligible here)"
kill -SIGTERM $(cat ${state_dir}/child_pid)
# child_pid does not exist when rootlesskit itself was expected to fail on startup
if [ -f ${state_dir}/child_pid ]; then
kill -SIGTERM $(cat ${state_dir}/child_pid) || true
fi
wait $pid >/dev/null 2>&1 || true
wait_for_pasta_exit
rm -rf $tmp
}

function wait_for_pasta_exit() {
for i in $(seq 50); do
pgrep -x pasta >/dev/null || break
sleep 0.1
done
}

# test_pesto_dynamic [ROOTLESSKIT ARGS...]
function test_pesto_dynamic() {
rootlesskit_args="$@"
INFO "Testing dynamic port management (rootlesskit_args=\"${rootlesskit_args}\")"
tmp=$(mktemp -d)
state_dir=${tmp}/state
html_dir=${tmp}/html
mkdir -p ${html_dir}
echo "test_pesto_dynamic" >${html_dir}/index.html
httpd="busybox httpd -f -v -p 80 -h ${html_dir}"
if echo "${rootlesskit_args}" | grep -q -- --detach-netns; then
# With --detach-netns, the child command runs in the host's network
# namespace, so the server has to enter the detached netns explicitly.
httpd="nsenter -n${state_dir}/netns ${httpd}"
fi
$ROOTLESSKIT \
--state-dir=${state_dir} \
--net=pasta \
--disable-host-loopback \
--copy-up=/etc \
--port-driver=pesto \
${rootlesskit_args} \
${httpd} \
2>&1 &
pid=$!
sleep 1
api_sock=${state_dir}/api.sock

INFO "= the port must not be reachable before add-ports ="
if curl -fsSL http://127.0.0.1:8080; then
ERROR "curl should not success before add-ports"
exit 1
fi

INFO "= add-ports, then the port must be reachable ="
id=$($ROOTLESSCTL --socket=${api_sock} add-ports 127.0.0.1:8080:80/tcp)
curl -fsSL http://127.0.0.1:8080
$ROOTLESSCTL --socket=${api_sock} list-ports --json | grep -q '"parentPort":8080'

INFO "= adding a conflicting port must fail ="
if $ROOTLESSCTL --socket=${api_sock} add-ports 127.0.0.1:8080:80/tcp; then
ERROR "add-ports should fail for a conflicting port"
exit 1
fi

INFO "= ChildIP matching the namespace address (10.0.2.100) is accepted ="
# 10.0.2.100 is the first address of the default CIDR (10.0.2.0/24) + 100
$ROOTLESSCTL --socket=${api_sock} add-ports 127.0.0.1:8081:10.0.2.100:80/tcp
curl -fsSL http://127.0.0.1:8081

INFO "= any other ChildIP must be rejected (pasta cannot honor it) ="
if $ROOTLESSCTL --socket=${api_sock} add-ports 127.0.0.1:8083:10.9.9.9:80/tcp; then
ERROR "add-ports should fail for an unsupported ChildIP"
exit 1
fi

INFO "= unsupported proto (tcp6) must be rejected ="
if $ROOTLESSCTL --socket=${api_sock} add-ports :8082:80/tcp6; then
ERROR "add-ports should fail for tcp6"
exit 1
fi

INFO "= udp rules can be added and removed ="
udp_id=$($ROOTLESSCTL --socket=${api_sock} add-ports 127.0.0.1:5353:5353/udp)
$ROOTLESSCTL --socket=${api_sock} list-ports --json | grep -q '"proto":"udp"'
$ROOTLESSCTL --socket=${api_sock} remove-ports ${udp_id}

INFO "= remove-ports, then new connections must be refused ="
$ROOTLESSCTL --socket=${api_sock} remove-ports ${id}
if curl -fsSL http://127.0.0.1:8080; then
ERROR "curl should not success after remove-ports"
exit 1
fi

INFO "= removing an unknown port ID must fail ="
if $ROOTLESSCTL --socket=${api_sock} remove-ports ${id}; then
ERROR "remove-ports should fail for an unknown ID"
exit 1
fi

INFO "Test passing, stopping httpd (\"exit status 255\" is negligible here)"
if [ -f ${state_dir}/child_pid ]; then
kill -SIGTERM $(cat ${state_dir}/child_pid) || true
fi
wait $pid >/dev/null 2>&1 || true
wait_for_pasta_exit
rm -rf $tmp
}

Expand Down Expand Up @@ -89,4 +202,26 @@ test_port slirp4netns http://127.0.0.1:8080 "should success" -p 0.0.0.0:8080:80/
INFO "=== protocol \"tcp4\" is strictly v4-only ==="
test_port slirp4netns http://[::1]:8080 "should fail" -p 0.0.0.0:8080:80/tcp4

INFO "===== Port driver: pesto (--net=pasta) ====="
INFO "=== static publishing via -p ==="
test_port pesto http://127.0.0.1:8080 "should success" -p 0.0.0.0:8080:80/tcp
test_port pesto http://127.0.0.1:8080 "should success" -p 127.0.0.1:8080:80/tcp
test_port pesto http://127.0.0.1:8080 "should success" -p 0.0.0.0:8080:80/tcp4

INFO "=== port translation (8080 -> 80) ==="
test_port pesto http://127.0.0.1:8080 "should success" -p :8080:80/tcp

INFO "=== a port that is not published must not be reachable ==="
test_port pesto http://127.0.0.1:9090 "should fail" -p 0.0.0.0:8080:80/tcp

INFO "=== unsupported proto (tcp6) makes the startup fail ==="
test_port pesto http://[::1]:8080 "should fail" -p [::]:8080:80/tcp6

INFO "=== dynamic port management via rootlessctl ==="
test_pesto_dynamic

INFO "=== with --detach-netns ==="
test_port pesto http://127.0.0.1:8080 "should success" -p 0.0.0.0:8080:80/tcp --detach-netns
test_pesto_dynamic --detach-netns

INFO "===== PASSING ====="
Loading