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
10 changes: 10 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ jobs:
run: DOCKER_BUILDKIT=1 docker build -t rootlesskit:test-unit --target test-unit .
- name: "Unit test"
run: docker run --rm --privileged rootlesskit:test-unit
test-unit-iptables-fallback:
name: "Unit test (source-ip-transparent iptables fallback, no nft)"
runs-on: ubuntu-24.04
steps:
- name: "Check out"
uses: actions/checkout@v7
- name: "Build unit test image without nft"
run: DOCKER_BUILDKIT=1 docker build -t rootlesskit:test-unit-iptables-fallback --target test-unit --build-arg TEST_UNIT_APT_EXTRA=iptables .
- name: "Unit test"
run: docker run --rm --privileged rootlesskit:test-unit-iptables-fallback
test-cross:
name: "Cross compilation test"
runs-on: ubuntu-24.04
Expand Down
12 changes: 8 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ COPY --from=cross /go/src/github.com/rootless-containers/rootlesskit/_artifact/*

# `go test -race` requires non-Alpine
FROM golang:${GO_VERSION} AS test-unit
# iptables: used for source-ip-transparent
RUN apt-get update && apt-get install -y git iproute2 netcat-openbsd iptables
# iptables, nftables: used for source-ip-transparent (nft preferred, iptables as fallback).
# TEST_UNIT_APT_EXTRA can be overridden to "iptables" only, to exercise the
# fallback path in CI when nft isn't available.
ARG TEST_UNIT_APT_EXTRA="iptables nftables"
RUN apt-get update && apt-get install -y git iproute2 netcat-openbsd $TEST_UNIT_APT_EXTRA
ADD . /go/src/github.com/rootless-containers/rootlesskit
WORKDIR /go/src/github.com/rootless-containers/rootlesskit
RUN go mod verify && go vet ./...
Expand Down Expand Up @@ -65,8 +68,9 @@ FROM ubuntu:${UBUNTU_VERSION} AS test-integration
# libcap2-bin and curl: used by the RUN instructions in this Dockerfile.
# bind9-dnsutils: for `nslookup` command used by integration-net.sh
# systemd and uuid-runtime: for systemd-socket-activate used by integration-systemd-socket.sh
# iptables: for source-ip-transparent. Also for Docker.
RUN apt-get update && apt-get install -y iproute2 liblxc-common lxc-utils iperf3 busybox sudo libcap2-bin curl bind9-dnsutils systemd uuid-runtime iptables
# iptables: for Docker (dockerd-rootless itself still uses iptables).
# nftables: for source-ip-transparent (rootlesskit's own builtin port driver).
RUN apt-get update && apt-get install -y iproute2 liblxc-common lxc-utils iperf3 busybox sudo libcap2-bin curl bind9-dnsutils systemd uuid-runtime iptables nftables
COPY --from=idmap /usr/bin/newuidmap /usr/bin/newuidmap
COPY --from=idmap /usr/bin/newgidmap /usr/bin/newgidmap
RUN /sbin/setcap cap_setuid+eip /usr/bin/newuidmap && \
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ OPTIONS:
--port-driver value port driver for non-host network. [none, implicit (for pasta), builtin, slirp4netns, gvisor-tap-vsock(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)
--source-ip-transparent-backend value firewall backend for --source-ip-transparent (builtin port driver) [auto, nft, iptables] (default: "auto")

Process:
--pidns create a PID namespace (default: false)
Expand Down
14 changes: 13 additions & 1 deletion cmd/rootlesskit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,11 @@ See https://rootlesscontaine.rs/getting-started/common/ .
Usage: "preserve real client source IP using IP_TRANSPARENT (builtin port driver, TCP only)",
Value: true,
}, CategoryPort),
Categorize(&cli.StringFlag{
Name: "source-ip-transparent-backend",
Usage: "firewall backend for --source-ip-transparent (builtin port driver) [auto, nft, iptables]",
Value: "auto",
}, CategoryPort),
Categorize(&cli.BoolFlag{
Name: "pidns",
Usage: "create a PID namespace",
Expand Down Expand Up @@ -625,7 +630,14 @@ func createParentOpt(clicontext *cli.Context) (parent.Opt, error) {
if opt.NetworkDriver == nil {
return opt, errors.New("port driver requires non-host network")
}
opt.PortDriver, err = builtin.NewParentDriver(&logrusDebugWriter{label: "port/builtin"}, opt.StateDir, clicontext.Bool("source-ip-transparent"))
sourceIPTransparentBackend := clicontext.String("source-ip-transparent-backend")
switch sourceIPTransparentBackend {
case "auto", "nft", "iptables":
// OK
default:
return opt, fmt.Errorf("unknown source-ip-transparent-backend: %s", sourceIPTransparentBackend)
}
opt.PortDriver, err = builtin.NewParentDriver(&logrusDebugWriter{label: "port/builtin"}, opt.StateDir, clicontext.Bool("source-ip-transparent"), sourceIPTransparentBackend)
if err != nil {
return opt, err
}
Expand Down
2 changes: 1 addition & 1 deletion docs/port.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ The default value is `none` (do not expose ports).
| `--port-driver` | Throughput | Source IP | Notes
|----------------------|-------------|----------|-------
| `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.
| `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. The underlying firewall rules use `nft`, falling back to `iptables` if `nft` is unavailable, and are set up eagerly at startup (not lazily on first use); `--source-ip-transparent-backend` can be used to force one or the other. The resolved backend is reported via the REST API (`PortDriverInfo.Extra["sourceIPTransparentBackend"]`).
| `implicit` | 37.6 Gbps | Propagated | Requires `pasta` network
| `gvisor-tap-vsock` (Experimental) | 3.83 Gbps | Not propagated | Throughput is currently limited; see issue link below for improvement ideas.

Expand Down
9 changes: 5 additions & 4 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "net"
const (
// Version of the REST API, not implementation version.
// See openapi.yaml for the definition.
Version = "1.1.2"
Version = "1.1.3"
)

// Info is the structure returned by `GET /info`
Expand All @@ -29,7 +29,8 @@ type NetworkDriverInfo struct {

// PortDriverInfo in Info
type PortDriverInfo struct {
Driver string `json:"driver"`
Protos []string `json:"protos"`
DisallowLoopbackChildIP bool `json:"disallowLoopbackChildIP,omitempty"` // since API v1.1.1
Driver string `json:"driver"`
Protos []string `json:"protos"`
DisallowLoopbackChildIP bool `json:"disallowLoopbackChildIP,omitempty"` // since API v1.1.1
Extra map[string]string `json:"extra,omitempty"` // since API v1.1.3, driver-specific details, e.g. {"sourceIPTransparentBackend": "nft"} for the builtin driver
}
7 changes: 6 additions & 1 deletion pkg/api/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# When you made a change to this YAML, please validate with https://editor.swagger.io
openapi: 3.0.3
info:
version: 1.1.2
version: 1.1.3
title: RootlessKit API
servers:
- url: 'http://rootlesskit/v1'
Expand Down Expand Up @@ -172,3 +172,8 @@ components:
disallowLoopbackChildIP:
type: boolean
description: "If this field is set to true, loopback IP such as 127.0.0.1 cannot be specified as a child IP"
extra:
type: object
description: "Driver-specific details, e.g. {\"sourceIPTransparentBackend\": \"nft\"} for the builtin driver"
additionalProperties:
type: string
4 changes: 2 additions & 2 deletions pkg/port/builtin/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (
)

var (
NewParentDriver func(logWriter io.Writer, stateDir string, sourceIPTransparent bool) (port.ParentDriver, error) = parent.NewDriver
NewChildDriver func(logWriter io.Writer) port.ChildDriver = child.NewDriver
NewParentDriver func(logWriter io.Writer, stateDir string, sourceIPTransparent bool, sourceIPTransparentBackend string) (port.ParentDriver, error) = parent.NewDriver
NewChildDriver func(logWriter io.Writer) port.ChildDriver = child.NewDriver
)

// Available indicates whether this port driver is compiled in (used for generating help text)
Expand Down
45 changes: 44 additions & 1 deletion pkg/port/builtin/builtin_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package builtin

import (
"context"
"os"
"os/exec"
"testing"

"github.com/rootless-containers/rootlesskit/v3/pkg/port"
Expand All @@ -21,7 +23,7 @@ func TestBuiltIn(t *testing.T) {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
d, err := NewParentDriver(os.Stderr, tmpDir, true)
d, err := NewParentDriver(os.Stderr, tmpDir, true, "auto")
if err != nil {
t.Fatal(err)
}
Expand All @@ -32,3 +34,44 @@ func TestBuiltIn(t *testing.T) {
testsuite.RunTCPTransparent(t, pf)
testsuite.RunUDPTransparent(t, pf)
}

// TestSourceIPTransparentBackend exercises an explicit
// --source-ip-transparent-backend selection end to end, and checks that the
// backend actually resolved by the child (via the init handshake) is
// reported back via PortDriverInfo.Extra.
func TestSourceIPTransparentBackend(t *testing.T) {
for _, backend := range []string{"nft", "iptables"} {
t.Run(backend, func(t *testing.T) {
if backend == "nft" {
ensureNFT(t)
}
tmpDir, err := os.MkdirTemp("", "test-builtin-backend")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
d, err := NewParentDriver(os.Stderr, tmpDir, true, backend)
if err != nil {
t.Fatal(err)
}
// RunTCPTransparent drives RunParentDriver, which performs the
// init handshake that eagerly resolves the backend. Info() only
// reflects the resolved value once that handshake has happened.
testsuite.RunTCPTransparent(t, func() port.ParentDriver { return d })
info, err := d.Info(context.Background())
if err != nil {
t.Fatal(err)
}
if got := info.Extra["sourceIPTransparentBackend"]; got != backend {
t.Fatalf("expected PortDriverInfo.Extra[sourceIPTransparentBackend]=%q, got %q", backend, got)
}
})
}
}

func ensureNFT(t *testing.T) {
t.Helper()
if _, err := exec.LookPath("nft"); err != nil {
t.Skipf("nft not found: %v", err)
}
}
Loading
Loading