Skip to content
Merged
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
16 changes: 14 additions & 2 deletions src/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IpAddr>) -> 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(())
}
Expand Down Expand Up @@ -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(())
Expand Down
61 changes: 61 additions & 0 deletions test-restart-eacces.sh
Original file line number Diff line number Diff line change
@@ -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=<tun2proxy image> [PROXY=<ip:port>] ./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"
Loading