Troubleshooting
Start here
Section titled “Start here”wardcell doctorIt answers the question most failures turn out to be about — whether this
machine would enforce a run at all — and names where the records are. Exit 0
means nothing it checks is wrong; 1 prints each problem with the command that
fixes it. On macOS, run it before the first real run on a new machine: an
unapproved or reboot-pending system extension leaves the machine on a weaker
floor with nothing saying so. See wardcell doctor.
A network call is blocked
Section titled “A network call is blocked”The proxy refused a request. The run’s summary names each blocked destination with the exact DSL line that would allow it; the full record is in the log:
wardcell logs show <id> --category net # add -f to watch a run still goingCopy the unblock hint into your policy and re-run. Common causes:
- Host allowed, port not. A bare
allow_http "x"covers 80 and 443 only; for anything else writeallow_http "x:8443". - Host allowed, request not. The
allow_httpblock needs anallow_requestsrule matching this method and path. - A
denyrule matched. Deny always wins, from any fragment.wardcell policy explain -- <command>shows which fragment it came from.
See Network Access.
A run failed and already exited
Section titled “A run failed and already exited”Its log is still there:
wardcell logs list --all # newest first, with run id, status and commandwardcell logs show <id> # a short id prefix works, like a git short hashA crashed status means the process is gone with no clean exit — the log holds
whatever it managed to write. See Viewing Logs.
A command hangs, or reports no network
Section titled “A command hangs, or reports no network”On macOS, allow_tcp works through SOCKS5 via ALL_PROXY, and clients that
do not speak SOCKS — libpq, psycopg, most MySQL and Redis drivers — just sit
there. Use a SOCKS-capable client, or turn on transparent
capture, which needs no client cooperation.
On Linux, interception is transparent, so a hang is more likely a refused connection with no reset — check stderr for the denial.
macOS capture: a host you allowed is denied by its address
Section titled “macOS capture: a host you allowed is denied by its address”wardcell: denied network-outbound on '203.0.113.10:443'A program doing its own DNS (a Go binary built CGO_ENABLED=0, anything linked
against c-ares) connects to a bare address, so macOS attaches no hostname and
allow_http has nothing to match. Allow the address instead:
net { allow_tcp ["203.0.113.10:443"] } # or a CIDR: "203.0.113.0/24:443"This applies only under transparent capture; see Platform Support for why.
macOS capture: a denial naming a resolver
Section titled “macOS capture: a denial naming a resolver”wardcell: denied network-outbound on 'dns resolver 10.0.0.53:53'Lookups reach the resolvers in /etc/resolv.conf without a rule, but any other
resolver — usually a VPN’s split-DNS one, which macOS keeps elsewhere — must be
named:
net { allow_udp ["10.0.0.53:53"] allow_tcp ["10.0.0.53:53"]}A filesystem denial names a path you did not expect
Section titled “A filesystem denial names a path you did not expect”Usually a symlink: rules resolve against the real path, so ~/.config/foo
pointing into ~/Dropbox shows up as the Dropbox path. Also common is a tool
writing to a temp directory it derived at runtime — grant $TMPDIR rather than
the specific path.
macOS: a run failed and named no path at all
Section titled “macOS: a run failed and named no path at all”The kernel writes filesystem denials to the unified log and nowhere else, so an
enforcing wardcell run prints nothing about a refused path — the only symptom
is whatever the tool says downstream (a JVM PKIX failure, a build tool that
cannot create a cache dir). The denials are still there; re-run under
wardcell policy draft, which reads that log and
renders the grants that would have covered them. wardcell doctor also prints a
log show query that works after the run has finished.
Linux: a sandbox will not start
Section titled “Linux: a sandbox will not start”bwrap: No permissions to create new namespaceunshare: write failed /proc/self/uid_map: Operation not permittedwardcell builds every sandbox in an unprivileged user namespace. The first
message is a kernel with them off outright (sysctl user.max_user_namespaces
should be > 0; some hardened distros disable them permanently). The second is
Ubuntu 23.10+, which require an AppArmor profile granting userns — the .deb
installs it; otherwise it is a four-line file, with the recipe in Linux
(Bubblewrap).
Resist sysctl -w kernel.apparmor_restrict_unprivileged_userns=0: it lifts the
restriction for every process on the machine to solve it for one.
Linux: missing helpers
Section titled “Linux: missing helpers”bwrap: command not foundbwrap and iptables must be on PATH. The apt/rpm package depends on both;
a source build needs them installed by hand.
macOS: sandbox-exec deprecation warnings
Section titled “macOS: sandbox-exec deprecation warnings”Noise. The API is still the supported one for this purpose and wardcell uses it deliberately.
A connection the policy allows still fails
Section titled “A connection the policy allows still fails”Not every failure is a refusal — a grant gets the connection as far as the
proxy, which can still fail to carry it, with no denial printed because nothing
was denied. The proxy’s per-request diagnostics are written only at trace, so
opt in and re-read:
[logs]file_min_level = "trace"wardcell logs show <id> --source mitmThat is a record per request with the URI and the reason the proxy gave up on
any connection it could not complete — the firehose, meant for exactly this: a
client that retries, downgrades or reports a cut stream while wardcell says
nothing is wrong. See [logs].
Filing a useful bug
Section titled “Filing a useful bug”Include the version, the resolved policy, the rendered profile, and the run’s log:
wardcell --versionwardcell policy explain -- <command> --output jsonwardcell policy render -- <command>wardcell logs show <id> --output json > run.jsonlrender is the actual profile handed to the backend — that plus the log is
usually enough to reproduce. Where the failure is a connection that was
allowed, add the proxy’s own view (--source mitm, with
logs.file_min_level = "trace").