cloudflare / cloudflare/workerd

Local dev: containers never start on hosts with net.ipv4.conf.all.src_valid_mark=1 (e.g. Tailscale) — sidecar handshake packets dropped as martians

Open
#6,860 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
C++
Stars
8.7k
Forks
739
Avg merge
2d 20h
Merged PRs (30d)
174

Description

## Summary

On Linux hosts where `net.ipv4.conf.all.src_valid_mark=1` is set in the init
netns — which **Tailscale does by default** — `wrangler dev` with a `containers`
config can never start the application container. workerd creates the
`cloudflare/proxy-everything` egress-interceptor sidecar, but the TCP handshake
between workerd and the sidecar never completes: the kernel inside the sidecar
netns drops every packet that the sidecar's own TPROXY rules have fwmark-marked,
because the inherited `src_valid_mark=1` makes source validation use the fwmark
and classify the peer as a martian source. workerd times out after ~20 s
(`kj/timer.c++:30: overloaded: operation timed out`), destroys the sidecar,
retries, and the Durable Object eventually fails with
`Container failed to start` (`outcome: 'unrecognized_error'`) / HTTP 500
`"Container is starting. Please retry in a moment."` after ~151 s.

The application container is **never** even created (no `container create` in
`docker events` for the app image over the whole retry budget — only the
`…-proxy` sidecar churns create/destroy every ~20–45 s).

A one-line fix is possible on workerd's side: pass
`HostConfig.Sysctls: {"net.ipv4.conf.all.src_valid_mark": "0"}` when creating
the sidecar (namespaced `net.*` sysctls are settable via the Docker API without
extra privileges). Optionally also `net.ipv4.conf.all.rp_filter: "0"`, which is
inherited the same way and can interfere with TPROXY setups too.

## Environment

- workerd `1.20260701.1` (via wrangler `4.107.0`, miniflare `4.20260701.0`)
- `@cloudflare/sandbox` 0.12.3 (but any `containers` config reproduces it)
- Sidecar image: `cloudflare/proxy-everything:3cb1195@sha256:0ef6716c…`
- Linux 7.1.3 (CachyOS/Arch), Docker 29.6.1 (iptables backend), iptables 1.8.11 (nf_tables)
- Tailscale installed and running on the host (this is what sets
`net.ipv4.conf.all.src_valid_mark=1` in the init netns)

## Reproduction

Tailscale is not required — the sysctl is the trigger:

```bash
# on any Linux host with working `wrangler dev` containers:
sudo sysctl -w net.ipv4.conf.all.src_valid_mark=1

# any Worker project with a `containers` config:
wrangler dev
curl -X POST http://localhost:8787/... # anything that starts the container DO
```

Result: sidecar create/destroy loop, ~20 s per attempt, app container never
created, request fails after the retry budget. Setting the sysctl back to `0`
(and recreating the containers) fixes it immediately.

Note on inheritance: with the kernel default
`net.core.devconf_inherit_init_net=0`, **new network namespaces inherit all
IPv4 `conf/{all,default}` values from init_net**, so a host-level
`src_valid_mark=1` silently propagates into every container netns that Docker
creates.

## Root cause analysis

The sidecar sets up transparent interception inside its netns:

```
# mangle PREROUTING (inside the sidecar netns)
-A PREROUTING -p tcp -m socket -j DIVERT # existing-socket match → mark
-A DIVERT -j MARK --set-xmark 0x1/0xffffffff
-A DIVERT -j ACCEPT
...
# policy routing
32765: from all fwmark 0x1 lookup 100
# table 100
local default dev lo scope host
```

When workerd connects from the docker bridge gateway (e.g. `172.17.0.1`) to the
sidecar's ingress (`:39001`) to push the egress configuration:

1. The **SYN** does not match `-m socket` (xt_socket ignores non-transparent
wildcard listeners), stays unmarked, and is delivered normally — the
listener replies SYN-ACK and goes to `SYN_RECV`.
2. The **final ACK and the request payload** match `-m socket` (the request
socket for the 4-tuple now exists), get fwmark `0x1`, and then hit source
validation. With `src_valid_mark=1`, `fib_validate_source()` includes the
fwmark in the reverse-path lookup, which therefore resolves via table 100
(`local default dev lo`) — the source `172.17.0.1` is classified as a
local/martian source and the packet is **dropped**.
3. The sidecar's TCP stack never sees the ACK: it stays in `SYN_RECV`
retransmitting SYN-ACK; workerd's side sits in `FIN-WAIT-1` with ~164 bytes
unacked. After ~20 s the kj timer fires, workerd tears the sidecar down and
retries.

Evidence gathered while debugging:

- `docker events`: only `…-proxy` create/start/kill/destroy cycles; zero
`create` events for the app image.
- Docker API capture (socat proxy on the docker socket): per attempt exactly
`GET /networks/bridge` → `POST /containers/create?name=…-proxy` →
`POST /containers/…-proxy/start` → `GET /containers/…-proxy/json`, then ~20 s
of silence, then force-DELETEs — i.e. workerd blocks between "inspect
sidecar" and "create app container".
- tcpdump on both ends: ACK+data visible on the sidecar's `eth0`, never
processed by TCP; `/proc/net/tcp6` inside the sidecar shows the connection
stuck in `SYN_RECV`; host side stuck in `FIN-WAIT-1` with Send-Q > 0.
- Routing verdict inside the sidecar netns:
`ip route get from 172.17.0.1 iif eth0 mark 0x1` →
`RTNETLINK answers: Invalid argument`, while the same query **without**
`mark 0x1` returns a normal local route.
- Live fix: `nsenter -t -n sysctl -w net.ipv4.conf.all.src_valid_mark=0`
→ the very next attempt succeeds; app container is created and the DO
request returns HTTP 200 in ~1.6 s.

## Suggested fix

Set the sysctl explicitly when creating the sidecar container (and, if it uses
mark-based routing too, the app container), so local dev does not depend on
host-inherited values:

```json
"HostConfig": {
"Sysctls": {
"net.ipv4.conf.all.src_valid_mark": "0"
}
}
```

`rp_filter` may deserve the same treatment (`"net.ipv4.conf.all.rp_filter": "0"`),
since strict rp_filter in the container netns (also inherited from hosts that
set it, e.g. via ufw's `/etc/ufw/sysctl.conf`) is the classic TPROXY breaker.

## Workarounds for affected users (until fixed)

- `net.core.devconf_inherit_init_net=2` (new netns get kernel defaults instead
of inheriting init_net's IPv4 conf values) — verified working:
```bash
echo 'net.core.devconf_inherit_init_net = 2' | sudo tee /etc/sysctl.d/99-docker-netns-defaults.conf
sudo sysctl -w net.core.devconf_inherit_init_net=2
```
- Not recommended: clearing `net.ipv4.conf.all.src_valid_mark` on the host —
Tailscale needs it and re-sets it.

## Related, but distinct

While debugging this we also hit an independent local-dev pitfall with
identical symptoms: host firewalls with a default-deny INPUT policy (e.g. plain
ufw) block the sidecar's dial-back to workerd's egress listener on the bridge
gateway (`172.17.0.1:`), because that's container→host INPUT
traffic. `ufw allow in on docker0` fixes it. Possibly worth a note in the local
development docs — both issues produce the same "sidecar churns, app container
never created, 20 s timeout" signature that is otherwise hard to attribute.

Related issues (similar symptoms, different causes as far as I can tell):
cloudflare/workers-sdk#14242, cloudflare/workerd#5996.

Contributor guide

Open the contributing guide

Research direction

The issue does not name a source file or test; start by locating the code that creates the cloudflare/proxy-everything sidecar and builds its Docker HostConfig. Reproduce with net.ipv4.conf.all.src_valid_mark=1, then verify the sidecar handshake succeeds, the application container is created, and the container request returns HTTP 200.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, docker, linux
Domain
backend, devops, networking
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
62/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.