fix(linux): stop chmod'ing resolv.conf to 0444 in direct-write path (EACCES crash loop on container restart) - #36
Conversation
Why fresh creation works but every restart fails (mechanism walkthrough)Adding a detailed explanation of the failure mode here, since it's the non-obvious part of this bug and the reason it ships silently. Linux DAC check. The process runs as root (uid 0), and the file is
The trap window. A deployment that only ever Bonus: the cleanup path fails too. When setup fails, Three log lines, one root cause. Why the fix has two parts:
|
The direct-write fallback in setup_resolv_conf (used when bind-mounting is unavailable, e.g. inside Docker without CAP_SYS_ADMIN) called fchmod(fd, 0444) on /etc/resolv.conf via write_nameserver. In containers, /etc/resolv.conf is Docker's bind-mounted host file (/var/lib/docker/containers/<id>/resolv.conf), so the mode change persists across restarts. On the next start, open(O_WRONLY) on the now-0444 file fails with EACCES unless the process holds CAP_DAC_OVERRIDE - which capability-dropped containers don't. With --exit-on-fatal-error this becomes a permanent crash loop that only recreating the container can fix. The 0444 does not even serve its stated purpose on this path: it is meant to keep NetworkManager from overwriting resolv.conf, but NM runs as root with CAP_DAC_OVERRIDE and can overwrite a 0444 file anyway. The real protection is the MS_RDONLY bind-mount remount on the mount path. On the direct-write path the chmod protects nothing and only guarantees the next start fails. Two changes: - write_nameserver: drop the fchmod(0444). The tempfile on the mount path doesn't need it either (it is bind-mounted read-only). - direct-write branch: on EACCES, restore mode 0644 and reopen, so deployments already poisoned by an older version heal themselves on the first restart instead of crash-looping. Verified with an integration test (test-restart-eacces.sh) against Docker Engine 29.1.3 with --cap-drop ALL --cap-add NET_ADMIN: - stock v0.8.3: host resolv.conf becomes 0444 after first run - patched: mode stays 0644, restart healthy, and a pre-poisoned 0444 file is healed back to 0644 with no EACCES
4b60b9b to
49d9c0e
Compare
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The integration test has portability, recovery-coverage, log-capture, and startup-timeout issues.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 2
Open (2)
What changed in this PR
Fixes Linux resolv.conf permission poisoning and container restart failures.
Changes:
- Removes the direct-write
0444chmod. - Restores
0644and retries for poisoned files. - Adds Docker restart integration coverage.
| File | Summary |
|---|---|
src/linux.rs |
Updates direct-write permissions and recovery handling. |
test-restart-eacces.sh |
Adds restart and permission validation tests. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Resolve the host resolv.conf path via `docker info --format
'{{.DockerRootDir}}'` instead of hardcoding /var/lib/docker, so the
test works with custom data-root and rootless Docker.
- Pre-poison the host file to 0444 before restart, so the EACCES
heal-to-0644 branch in setup_resolv_conf is actually exercised
(upgrade scenario), and assert the mode is restored.
- Drop unused CIDFILE/mktemp leftover.
Addresses Copilot review feedback on PR tun2proxy#36.
|
Thanks |

Summary
Fixes a restart crash loop in containers: the direct-write fallback in
setup_resolv_confchmod'ed/etc/resolv.confto0444viawrite_nameserver, which poisons Docker's bind-mounted host-side resolv.conf file. Every subsequent container restart fails withEACCESunless the process holdsCAP_DAC_OVERRIDE.fchmod(fd, 0444)fromwrite_nameserver.open()fails withEACCES, restore mode0644and reopen — heals deployments already poisoned by older versions.Why the 0444 was wrong
The comment says it protects resolv.conf from NetworkManager, but NM runs as root with
CAP_DAC_OVERRIDEand can overwrite a0444file anyway. The real protection on the mount path is theMS_REMOUNT|MS_RDONLYremount, not the file mode. On the direct-write path the chmod protects nothing — it only guarantees the next startup fails.Failure mechanism (Docker)
docker run: resolv.conf is0644 root:root(Docker-owned host file, bind-mounted) → tun2proxy opens, writes, chmods to0444→ healthy.docker restart(also daemon restart / boot restore): same file, still0444.open(O_WRONLY)→ owner has no write bit → needsCAP_DAC_OVERRIDE→cap_drop: ALLcontainers fail withEACCES→ with--exit-on-fatal-error, permanent crash loop. Onlydocker rm+ re-createfixes it.Observed in production (Docker Engine 29.1.3,
ghcr.io/tun2proxy/tun2proxy:v0.8.3): host-side resolv.conf of the tun2proxy container was444, all other containers on the host were644.Log signature:
Testing
Integration test added (
test-restart-eacces.sh), run against Docker Engine 29.1.3, container flags--cap-drop ALL --cap-add NET_ADMIN --device /dev/net/tun:docker restarthealthycargo check/cargo testpass (crate has no other tests).Notes
tun2proxyconsumes this crate via crates.io (tproxy-config = "^7.0.7"); it will need a version bump + release for users on official images.tun2proxy/tun2proxylinking here.