diff --git a/src/linux.rs b/src/linux.rs index e789843..4d5f054 100644 --- a/src/linux.rs +++ b/src/linux.rs @@ -316,7 +316,6 @@ fn write_buffer_to_fd(fd: std::os::fd::BorrowedFd<'_>, data: &[u8]) -> Result<() fn write_nameserver(fd: std::os::fd::BorrowedFd<'_>, tun_gateway: Option) -> Result<()> { let tun_gateway = tun_gateway.unwrap_or_else(|| "198.18.0.1".parse().unwrap()); let data = format!("nameserver {tun_gateway}\n"); - nix::sys::stat::fchmod(fd.as_fd(), nix::sys::stat::Mode::from_bits(0o444).unwrap())?; write_buffer_to_fd(fd, data.as_bytes())?; Ok(()) } @@ -368,8 +367,21 @@ fn setup_resolv_conf(restore: &mut TproxyStateInner) -> Result<()> { restore.restore_resolvconf_content = Some(fs::read(ETC_RESOLV_CONF_FILE)?); + // Older versions chmod'ed /etc/resolv.conf to 0444 on this path, which made the + // write here fail with EACCES on every subsequent start in containers that + // drop CAP_DAC_OVERRIDE (e.g. Docker's bind-mounted resolv.conf). Restore + // writability before reopening. + let write_mode = nix::sys::stat::Mode::from_bits(0o644).unwrap(); let flags = nix::fcntl::OFlag::O_WRONLY | nix::fcntl::OFlag::O_CLOEXEC | nix::fcntl::OFlag::O_TRUNC; - let fd = nix::fcntl::open(ETC_RESOLV_CONF_FILE, flags, nix::sys::stat::Mode::from_bits(0o644).unwrap())?; + let fd = match nix::fcntl::open(ETC_RESOLV_CONF_FILE, flags, write_mode) { + Ok(fd) => fd, + Err(nix::errno::Errno::EACCES) => { + // Recover from the 0444 mode left behind by older versions (see comment above). + fs::set_permissions(ETC_RESOLV_CONF_FILE, Permissions::from_mode(0o644))?; + nix::fcntl::open(ETC_RESOLV_CONF_FILE, flags, write_mode)? + } + Err(err) => return Err(err.into()), + }; write_nameserver(fd.as_fd(), tun_gateway)?; } Ok(()) diff --git a/test-restart-eacces.sh b/test-restart-eacces.sh new file mode 100755 index 0000000..ab4d58e --- /dev/null +++ b/test-restart-eacces.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Integration test: tun2proxy container restart EACCES (tproxy-config issue) +# +# Validates two properties of the tproxy-config resolv.conf direct-write path: +# 1. After a fresh container start, the host-side resolv.conf file keeps mode 0644 +# (old versions chmod'ed it to 0444, which broke every subsequent restart). +# 2. If the host-side file was left at 0444 by an older version (upgrade case), +# a restart must heal it back to 0644 and come back up healthy. +# +# Usage: IMAGE= [PROXY=] ./test-restart-eacces.sh +set -euo pipefail + +IMAGE="${IMAGE:?set IMAGE to the tun2proxy image under test}" +PROXY="${PROXY:-192.0.2.1:1080}" # dummy proxy target; reachability irrelevant for this test +NAME="t2p-restart-test" + +cleanup() { docker rm -f "$NAME" >/dev/null 2>&1 || true; } +trap cleanup EXIT + +fail() { echo "FAIL: $*" >&2; exit 1; } +pass() { echo "PASS: $*"; } + +# Phase 1: fresh create must start (tun2proxy exits on fatal error if setup fails) +docker run -d --name "$NAME" --device /dev/net/tun \ + --cap-drop ALL --cap-add NET_ADMIN \ + "$IMAGE" --proxy "http://$PROXY" --dns virtual --exit-on-fatal-error \ + >/dev/null + +# Wait up to 15s for either a healthy run or a crash +sleep 5 +STATE1=$(docker inspect "$NAME" --format '{{.State.Status}} {{.State.ExitCode}}' 2>/dev/null || echo "gone") +echo "after create: $STATE1" +[ "$STATE1" = "running 0" ] || [ "${STATE1%% *}" = "running" ] || fail "fresh container not running: $STATE1" + +# Phase 2: host-side resolv.conf mode must still be 0644 (not poisoned to 0444) +DOCKER_ROOT=$(docker info --format '{{.DockerRootDir}}') +CID=$(docker inspect "$NAME" --format '{{.Id}}') +HOST_RESOLV="$DOCKER_ROOT/containers/$CID/resolv.conf" +MODE=$(stat -c %a "$HOST_RESOLV") +echo "host resolv.conf mode after first run: $MODE" +[ "$MODE" = "644" ] || fail "resolv.conf poisoned: mode $MODE (expected 644)" + +# Phase 3: poison host file to 0444 (upgrade scenario) then restart; +# the EACCES heal path must restore 0644 and the container must come back up +chmod 0444 "$HOST_RESOLV" 2>/dev/null || sudo chmod 0444 "$HOST_RESOLV" +docker restart "$NAME" >/dev/null +sleep 5 +STATE2=$(docker inspect "$NAME" --format '{{.State.Status}} {{.State.ExitCode}}' 2>/dev/null || echo "gone") +echo "after restart: $STATE2" +[ "${STATE2%% *}" = "running" ] || fail "restart failed: $STATE2 (EACCES crash loop?)" + +MODE2=$(stat -c %a "$HOST_RESOLV") +echo "host resolv.conf mode after restart: $MODE2" +[ "$MODE2" = "644" ] || fail "heal failed: mode $MODE2 (expected 644)" + +# Phase 4: double-check no EACCES in logs +if docker logs "$NAME" 2>&1 | grep -q "EACCES"; then + fail "EACCES present in logs after restart" +fi + +pass "create healthy, resolv.conf 0644 preserved, restart heals 0444->0644, no EACCES" \ No newline at end of file