From 776911228fcb1f149d6f77ca4580dc35e162559e Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Sat, 1 Aug 2026 16:52:14 +0900 Subject: [PATCH] feat: support --port-driver=pesto for IPv4 Signed-off-by: Hayato Kiwata --- .github/workflows/main.yaml | 23 +++++ Dockerfile | 2 +- README.md | 3 +- cmd/rootlesskit/main.go | 39 ++++++- docs/network.md | 6 +- docs/port.md | 9 +- hack/benchmark-iperf3-port.sh | 2 + hack/integration-port.sh | 141 ++++++++++++++++++++++++- pkg/network/pasta/pasta.go | 24 ++++- pkg/port/pasta/pesto.go | 187 ++++++++++++++++++++++++++++++++++ pkg/port/port.go | 1 + 11 files changed, 421 insertions(+), 16 deletions(-) create mode 100644 pkg/port/pasta/pesto.go diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index e1f68de4..e88834e0 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -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 \ @@ -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 \ @@ -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 diff --git a/Dockerfile b/Dockerfile index 100ba8a5..68dbfc0e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md index 084776c5..2b917762 100644 --- a/README.md +++ b/README.md @@ -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") @@ -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) diff --git a/cmd/rootlesskit/main.go b/cmd/rootlesskit/main.go index 1d83cb3d..e01d6cfc 100644 --- a/cmd/rootlesskit/main.go +++ b/cmd/rootlesskit/main.go @@ -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" @@ -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{ @@ -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", @@ -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 @@ -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 } @@ -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": @@ -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) } @@ -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) } diff --git a/docs/network.md b/docs/network.md index d51a5f38..ba3236e5 100644 --- a/docs/network.md +++ b/docs/network.md @@ -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. @@ -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. diff --git a/docs/port.md b/docs/port.md index 4097237f..4d201239 100644 --- a/docs/port.md +++ b/docs/port.md @@ -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. diff --git a/hack/benchmark-iperf3-port.sh b/hack/benchmark-iperf3-port.sh index 2eb7909f..f4e51d99 100755 --- a/hack/benchmark-iperf3-port.sh +++ b/hack/benchmark-iperf3-port.sh @@ -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 diff --git a/hack/integration-port.sh b/hack/integration-port.sh index 2c197965..3378672a 100755 --- a/hack/integration-port.sh +++ b/hack/integration-port.sh @@ -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="$@" @@ -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 @@ -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 } @@ -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 =====" diff --git a/pkg/network/pasta/pasta.go b/pkg/network/pasta/pasta.go index 62f27935..dcb1b407 100644 --- a/pkg/network/pasta/pasta.go +++ b/pkg/network/pasta/pasta.go @@ -23,6 +23,9 @@ type Features struct { // Has `--host-lo-to-ns-lo` (introduced in passt 2024_10_30.ee7d0b6) // https://passt.top/passt/commit/?id=b4dace8f462b346ae2135af1f8d681a99a849a5f HasHostLoToNsLo bool + // Has `--conf-path` (introduced in passt 2026_05_07.1afd4ed) + // https://passt.top/passt/commit/?id=f1d893ca1926e58ae5a2bf5602b515a883f3f994 + HasConfPath bool } func DetectFeatures(binary string) (*Features, error) { @@ -46,11 +49,15 @@ func DetectFeatures(binary string) (*Features, error) { if cmd.Run() == nil { f.HasHostLoToNsLo = true } + cmd = exec.Command(realBinary, "--conf-path=/dev/null", "--version") + if cmd.Run() == nil { + f.HasConfPath = true + } return &f, nil } // NewParentDriver instantiates new parent driver. -func NewParentDriver(logWriter io.Writer, binary string, mtu int, ipnet *net.IPNet, ifname string, +func NewParentDriver(logWriter io.Writer, binary string, mtu int, ipnet *net.IPNet, ifname, apiSocketPath string, disableHostLoopback, enableIPv6, implicitPortForwarding bool) (network.ParentDriver, error) { if binary == "" { return nil, errors.New("got empty pasta binary") @@ -79,6 +86,10 @@ func NewParentDriver(logWriter io.Writer, binary string, mtu int, ipnet *net.IPN return nil, err } + if !feat.HasConfPath && apiSocketPath != "" { + return nil, errors.New("port driver \"pesto\" requires pasta with --conf-path support (passt >= 2026_05_07.1afd4ed)") + } + return &parentDriver{ logWriter: logWriter, binary: binary, @@ -87,6 +98,7 @@ func NewParentDriver(logWriter io.Writer, binary string, mtu int, ipnet *net.IPN disableHostLoopback: disableHostLoopback, enableIPv6: enableIPv6, ifname: ifname, + apiSocketPath: apiSocketPath, implicitPortForwarding: implicitPortForwarding, feat: feat, }, nil @@ -100,6 +112,7 @@ type parentDriver struct { disableHostLoopback bool enableIPv6 bool ifname string + apiSocketPath string infoMu sync.RWMutex implicitPortForwarding bool info func() *api.NetworkDriverInfo @@ -168,7 +181,10 @@ func (d *parentDriver) ConfigureNetwork(childPID int, stateDir, detachedNetNSPat "--udp-ports=none") } if d.feat != nil { - if d.feat.HasHostLoToNsLo { + // Skipped for the "pesto" port driver: inbound connections must be + // forwarded to the child IP, because that is where the listener in the + // namespace (e.g. docker-proxy) is bound. + if d.feat.HasHostLoToNsLo && d.apiSocketPath == "" { // Needed to keep `docker run -p 127.0.0.1:8080:80` functional with // passt >= 2024_10_30.ee7d0b6 // @@ -176,6 +192,10 @@ func (d *parentDriver) ConfigureNetwork(childPID int, stateDir, detachedNetNSPat opts = append(opts, "--host-lo-to-ns-lo") } } + if d.apiSocketPath != "" { + // passt >= 2026_05_07.1afd4ed + opts = append(opts, "--conf-path="+d.apiSocketPath) + } if detachedNetNSPath == "" { opts = append(opts, strconv.Itoa(childPID)) } else { diff --git a/pkg/port/pasta/pesto.go b/pkg/port/pasta/pesto.go new file mode 100644 index 00000000..c72dca66 --- /dev/null +++ b/pkg/port/pasta/pesto.go @@ -0,0 +1,187 @@ +package pasta + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "os/exec" + "sync" + + "github.com/rootless-containers/rootlesskit/v3/pkg/api" + "github.com/rootless-containers/rootlesskit/v3/pkg/port" + "github.com/rootless-containers/rootlesskit/v3/pkg/port/portutil" + "github.com/sirupsen/logrus" +) + +func NewParentDriver(logWriter io.Writer, binary, socketPath string) (port.ParentDriver, error) { + if socketPath == "" { + return nil, errors.New("configuration socket path is not set") + } + + cmd := exec.Command(binary, "--version") + b, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf(`command "%s --version" failed, make sure pesto is installed: %q: %w`, + binary, string(b), err) + } + + d := driver{ + logWriter: logWriter, + ports: make(map[int]*port.Status), + apiSocketPath: socketPath, + binary: binary, + nextID: 1, + } + + return &d, nil +} + +type driver struct { + logWriter io.Writer + apiSocketPath string + mu sync.Mutex + childIP string // can be empty + ports map[int]*port.Status + binary string + nextID int +} + +func (d *driver) Info(ctx context.Context) (*api.PortDriverInfo, error) { + info := &api.PortDriverInfo{ + Driver: "pesto", + // No IPv6 support yet + Protos: []string{"tcp", "tcp4", "udp", "udp4"}, + DisallowLoopbackChildIP: true, + } + return info, nil +} + +func (d *driver) OpaqueForChild() map[string]string { + // NOP, as this driver does not have child-side logic. + return nil +} + +func (d *driver) RunParentDriver(initComplete chan struct{}, quit <-chan struct{}, cctx *port.ChildContext) error { + if cctx != nil && cctx.IP != nil { + if cctx.IP.To4() != nil { + d.childIP = cctx.IP.To4().String() + } + } + initComplete <- struct{}{} + <-quit + return nil +} + +func (d *driver) createArgs(method string, spec port.Spec) ([]string, error) { + opts := []string{method} + o := "" + + switch spec.Proto { + case "tcp", "tcp4": + o += "--tcp-ports=" + case "udp", "udp4": + o += "--udp-ports=" + default: + return nil, fmt.Errorf("unsupported protocol: %s", spec.Proto) + } + parentIP := spec.ParentIP + if parentIP == "" { + parentIP = "0.0.0.0" + } + p := net.ParseIP(parentIP) + if p == nil { + return nil, fmt.Errorf("invalid IP: %q", parentIP) + } + p = p.To4() + if p == nil { + return nil, fmt.Errorf("unsupported IP: %s", parentIP) + } + o += fmt.Sprintf("%s/%d:%d", p.String(), spec.ParentPort, spec.ChildPort) + opts = append(opts, o) + + return append(opts, d.apiSocketPath), nil +} + +func (d *driver) AddPort(ctx context.Context, spec port.Spec) (*port.Status, error) { + d.mu.Lock() + defer d.mu.Unlock() + + if err := portutil.ValidatePortSpec(spec, d.ports); err != nil { + return nil, err + } + if spec.ChildIP != "" && spec.ChildIP != d.childIP { + return nil, fmt.Errorf("unsupported ChildIP %q: the pesto port driver only supports the namespace address %q", spec.ChildIP, d.childIP) + } + + opts, err := d.createArgs("-A", spec) + if err != nil { + return nil, err + } + cmd := exec.Command(d.binary, opts...) + logrus.Debugf("Executing %v", cmd.Args) + out, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("pesto failed: %w\noutput: %s", err, out) + } + + id := d.nextID + st := port.Status{ + ID: id, + Spec: spec, + } + d.ports[id] = &st + d.nextID++ + + return &st, nil +} + +func (d *driver) ListPorts(ctx context.Context) ([]port.Status, error) { + var ports []port.Status + d.mu.Lock() + for _, p := range d.ports { + ports = append(ports, *p) + } + d.mu.Unlock() + return ports, nil +} + +func (d *driver) RemovePort(ctx context.Context, id int) error { + d.mu.Lock() + defer d.mu.Unlock() + + st, ok := d.ports[id] + if !ok { + return fmt.Errorf("invalid ID: %d", id) + } + opts, err := d.createArgs("-D", st.Spec) + if err != nil { + return err + } + cmd := exec.Command(d.binary, opts...) + logrus.Debugf("Executing %v", cmd.Args) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("pesto failed: %w\noutput: %s", err, out) + } + delete(d.ports, id) + + return nil +} + +func NewChildDriver() port.ChildDriver { + return &childDriver{} +} + +type childDriver struct { +} + +func (d *childDriver) RunChildDriver(opaque map[string]string, quit <-chan struct{}, detachedNetNSPath string) error { + // NOP + <-quit + return nil +} + +// Available indicates whether this port driver is compiled in (used for generating help text) +const Available = true diff --git a/pkg/port/port.go b/pkg/port/port.go index 24904076..ea646e5c 100644 --- a/pkg/port/port.go +++ b/pkg/port/port.go @@ -20,6 +20,7 @@ type Spec struct { // - builtin driver: 127.0.0.1 // - slirp4netns driver: slirp4netns's child IP, e.g., 10.0.2.100 // - gvisor-tap-vsock driver: gvisor-tap-vsock's child IP, e.g., 10.0.2.100 + // - pesto driver: pasta's child IP, e.g., 10.0.2.100 ChildIP string `json:"childIP,omitempty"` }