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
87 changes: 75 additions & 12 deletions pkg/port/builtin/child/child.go

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep iptables mode too and test the both in CI

@Akshitguptaa Akshitguptaa Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kept iptables as a fallback.
CI runs test with both nft and iptables, and a separate one with only iptables (no nft) to cover the fallback path.

Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@ fallback:

// setupTransparentRouting sets up policy routing so that response packets
// destined to transparent-bound source IPs are delivered locally.
// The firewall rules are implemented via nft, falling back to iptables if
// nft isn't available on the host (see setupTransparentRoutingNFT and
// setupTransparentRoutingIPTables).

@AkihiroSuda AkihiroSuda Jul 26, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have a CLI flag --source-ip-transparent-backend to explicitly set the backend?

Also, the backend should be visible in PortDriverInfo, perhaps as Extra map[string]string

See #607 for how to extend the API

//
// Transparent sockets (IP_TRANSPARENT) bind to non-local addresses (the real
// client IP). Response packets to these addresses must be routed locally instead
Expand All @@ -224,17 +227,8 @@ fallback:
// SYN-ACK is then routed via the fwmark table (local delivery) instead of
// the default route (TAP), allowing it to reach the transparent socket.
func (d *childDriver) setupTransparentRouting() bool {
// Check that iptables is available before proceeding.
if _, err := exec.LookPath("iptables"); err != nil {
fmt.Fprintf(d.logWriter, "source IP transparent: iptables not found, disabling: %v\n", err)
return false
}
// Verify the connmark module is usable (kernel module might not be loaded).
if out, err := exec.Command("iptables", "-t", "mangle", "-L", "-n").CombinedOutput(); err != nil {
fmt.Fprintf(d.logWriter, "source IP transparent: iptables mangle table not available, disabling: %v: %s\n", err, out)
return false
}
cmds := [][]string{
// Common prep, independent of the firewall backend used below.
prepCmds := [][]string{
// Table 100: treat all addresses as local (for delivery to transparent sockets)
{"ip", "route", "add", "local", "default", "dev", "lo", "table", "100"},
{"ip", "-6", "route", "add", "local", "default", "dev", "lo", "table", "100"},
Expand All @@ -244,6 +238,75 @@ func (d *childDriver) setupTransparentRouting() bool {
// Inherit fwmark from SYN to accepted socket (needed for userspace proxies
// like docker-proxy, so that SYN-ACK routing uses table 100)
{"sysctl", "-w", "net.ipv4.tcp_fwmark_accept=1"},
}
for _, args := range prepCmds {
if out, err := exec.Command(args[0], args[1:]...).CombinedOutput(); err != nil {
fmt.Fprintf(d.logWriter, "source IP transparent routing setup: %v: %s\n", err, out)
}
}
if d.setupTransparentRoutingNFT() {
return true
}
fmt.Fprintf(d.logWriter, "source IP transparent: nft unavailable, falling back to iptables\n")
return d.setupTransparentRoutingIPTables()
}

// setupTransparentRoutingNFT implements setupTransparentRouting using nft.
// The "inet" family covers both IPv4 and IPv6 in a single ruleset.
func (d *childDriver) setupTransparentRoutingNFT() bool {
// Check that nft is available before proceeding.
if _, err := exec.LookPath("nft"); err != nil {
fmt.Fprintf(d.logWriter, "source IP transparent (nft): nft not found, disabling: %v\n", err)
return false
}
// Verify nftables is usable (the nf_tables kernel module might not be loaded).
if out, err := exec.Command("nft", "list", "tables").CombinedOutput(); err != nil {
fmt.Fprintf(d.logWriter, "source IP transparent (nft): nft not available, disabling: %v: %s\n", err, out)
return false
}
const nftTable = "rootlesskit_transparent"
cmds := [][]string{
// Create a single nftables table in the "inet" family, which covers both
// IPv4 and IPv6, replacing the separate iptables/ip6tables tables used below.
{"nft", "add", "table", "inet", nftTable},
// Hook into OUTPUT and PREROUTING at the "mangle" priority, matching where
// the equivalent rules live in the iptables mangle table.
{"nft", "add", "chain", "inet", nftTable, "output",
"{", "type", "filter", "hook", "output", "priority", "mangle", ";", "}"},
{"nft", "add", "chain", "inet", nftTable, "prerouting",
"{", "type", "filter", "hook", "prerouting", "priority", "mangle", ";", "}"},
// In OUTPUT: tag transparent connections (non-local source) with a connection
// mark. Equivalent to iptables: -m addrtype ! --src-type LOCAL -j CONNMARK --set-mark 100
{"nft", "add", "rule", "inet", nftTable, "output",
"meta", "l4proto", "tcp", "fib", "saddr", "type", "!=", "local", "ct", "mark", "set", "100"},
// In PREROUTING: restore the connmark to the packet mark for routing.
// Equivalent to iptables: -m connmark --mark 100 -j MARK --set-mark 100
{"nft", "add", "rule", "inet", nftTable, "prerouting",
"meta", "l4proto", "tcp", "ct", "mark", "100", "meta", "mark", "set", "100"},
}
for _, args := range cmds {
if out, err := exec.Command(args[0], args[1:]...).CombinedOutput(); err != nil {
fmt.Fprintf(d.logWriter, "source IP transparent (nft) routing setup: %v: %s\n", err, out)
}
}
return true
}

// setupTransparentRoutingIPTables implements setupTransparentRouting using
// iptables/ip6tables, kept as a fallback for hosts where nft is unavailable
// and for environments that still expect iptables specifically.
func (d *childDriver) setupTransparentRoutingIPTables() bool {
// Check that iptables is available before proceeding.
if _, err := exec.LookPath("iptables"); err != nil {
fmt.Fprintf(d.logWriter, "source IP transparent (iptables): iptables not found, disabling: %v\n", err)
return false
}
// Verify the connmark module is usable (kernel module might not be loaded).
if out, err := exec.Command("iptables", "-t", "mangle", "-L", "-n").CombinedOutput(); err != nil {
fmt.Fprintf(d.logWriter, "source IP transparent (iptables): mangle table not available, disabling: %v: %s\n", err, out)
return false
}
cmds := [][]string{
// In OUTPUT: tag transparent connections (non-local source) with CONNMARK
{"iptables", "-t", "mangle", "-A", "OUTPUT", "-p", "tcp", "-m", "addrtype", "!", "--src-type", "LOCAL", "-j", "CONNMARK", "--set-mark", "100"},
{"ip6tables", "-t", "mangle", "-A", "OUTPUT", "-p", "tcp", "-m", "addrtype", "!", "--src-type", "LOCAL", "-j", "CONNMARK", "--set-mark", "100"},
Expand All @@ -253,7 +316,7 @@ func (d *childDriver) setupTransparentRouting() bool {
}
for _, args := range cmds {
if out, err := exec.Command(args[0], args[1:]...).CombinedOutput(); err != nil {
fmt.Fprintf(d.logWriter, "source IP transparent routing setup: %v: %s\n", err, out)
fmt.Fprintf(d.logWriter, "source IP transparent (iptables) routing setup: %v: %s\n", err, out)
}
}
return true
Expand Down