Skip to content

Commit e252e3f

Browse files
ammarioclaude
andcommitted
fix: revert to simple ip netns exec with documented limitations
After extensive attempts to safely modify /etc/resolv.conf in mount namespaces, I've learned that **mount namespaces only isolate mount tables, not filesystems**. Any file operations (rm, cp, touch) always affect the host, regardless of mount propagation settings. Solution: Accept the limitation and use the standard ip netns exec approach: - Works perfectly when /etc/resolv.conf is a regular file - May fail to bind-mount when /etc/resolv.conf is a symlink to non-existent target - DNS still works via nftables interception even if bind-mount fails - Host's /etc/resolv.conf is NEVER modified This is the safest approach. The alternative (modifying files in mount namespaces) is fundamentally unsafe and corrupted the host's resolv.conf multiple times during testing. Tested on ml-1 with external DNS (8.8.8.8) - all 23 tests pass, host resolv.conf remains untouched. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 58139da commit e252e3f

1 file changed

Lines changed: 22 additions & 54 deletions

File tree

src/jail/linux/mod.rs

Lines changed: 22 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -497,73 +497,41 @@ impl Jail for LinuxJail {
497497
None
498498
};
499499

500-
// CRITICAL DNS CONFIGURATION APPROACH:
500+
// DNS CONFIGURATION: Use standard ip netns exec approach
501501
//
502-
// We use nsenter + unshare instead of ip netns exec to manually control the bind-mount
503-
// of our custom /etc/netns/.../resolv.conf. This is necessary because:
502+
// ip netns exec automatically creates a mount namespace and bind-mounts
503+
// /etc/netns/<namespace>/resolv.conf over /etc/resolv.conf when present.
504504
//
505-
// 1. ip netns exec automatically bind-mounts /etc/netns/.../resolv.conf, but it fails when
506-
// /etc/resolv.conf is a symlink to a non-existent target (common with systemd-resolved)
505+
// KNOWN LIMITATION: This may fail on systems where /etc/resolv.conf is a symlink
506+
// to a target that doesn't exist in the mount namespace (e.g., systemd-resolved).
507+
// In such cases, DNS queries will still reach our dummy DNS server via the nftables
508+
// rules, but applications that directly check /etc/resolv.conf may see stale content.
507509
//
508-
// 2. The order with ip netns exec is: create mount namespace -> bind-mount -> run command
509-
// So we can't create the symlink target before the bind-mount happens
510+
// We cannot safely "fix" this because:
511+
// - Mount namespaces only isolate mount tables, not filesystems
512+
// - Any file operations (rm, cp, touch) affect the host
513+
// - Bind-mounts over symlinks require the symlink target to exist
510514
//
511-
// 3. Solution: Use nsenter to enter the network namespace, then unshare to create a mount
512-
// namespace, then manually bind-mount our resolv.conf
513-
//
514-
// Command structure:
515-
// nsenter --net=/var/run/netns/<namespace> unshare --mount sh -c 'mkdir -p /run/systemd/resolve && touch /run/systemd/resolve/stub-resolv.conf && mount --bind /etc/netns/.../resolv.conf /etc/resolv.conf && exec <command>'
516-
//
517-
// Reference: https://unix.stackexchange.com/questions/443898
515+
// Reference: https://man7.org/linux/man-pages/man8/ip-netns.8.html
518516

519-
let netns_path = format!("/var/run/netns/httpjail_{}", self.config.jail_id);
520-
let resolv_path = format!("/etc/netns/httpjail_{}/resolv.conf", self.config.jail_id);
517+
// Build command: ip netns exec <namespace> [setpriv ...] <command>
518+
let mut cmd = Command::new("ip");
519+
cmd.args(["netns", "exec", &self.namespace_name()]);
521520

522-
// Build the inner command parts
523-
let mut inner_cmd_parts = Vec::new();
524-
525-
// Add setpriv if needed
521+
// Add setpriv for privilege dropping if needed
526522
if let Some((uid, gid)) = drop_privs {
527-
inner_cmd_parts.push("setpriv".to_string());
528-
inner_cmd_parts.push(format!("--reuid={}", uid));
529-
inner_cmd_parts.push(format!("--regid={}", gid));
530-
inner_cmd_parts.push("--init-groups".to_string());
531-
inner_cmd_parts.push("--".to_string());
523+
cmd.arg("setpriv");
524+
cmd.arg(format!("--reuid={}", uid));
525+
cmd.arg(format!("--regid={}", gid));
526+
cmd.arg("--init-groups");
527+
cmd.arg("--");
532528
}
533529

534530
// Add user command
535531
for arg in command {
536-
inner_cmd_parts.push(arg.to_string());
532+
cmd.arg(arg);
537533
}
538534

539-
// Shell-escape each argument
540-
let escaped_parts: Vec<String> = inner_cmd_parts
541-
.iter()
542-
.map(|s| format!("'{}'", s.replace('\'', "'\\''")))
543-
.collect();
544-
545-
// Build wrapper shell command that:
546-
// 1. Creates symlink target placeholder (if needed)
547-
// 2. Bind-mounts our custom resolv.conf to the RESOLVED path (following symlinks)
548-
// 3. Execs the user command
549-
//
550-
// CRITICAL: We mount to $(readlink -f /etc/resolv.conf) not /etc/resolv.conf directly,
551-
// because mount follows symlinks and we need the actual target path.
552-
let shell_cmd = format!(
553-
"mkdir -p /run/systemd/resolve && touch /run/systemd/resolve/stub-resolv.conf && mount --bind {} $(readlink -f /etc/resolv.conf || echo /etc/resolv.conf) && exec {}",
554-
resolv_path,
555-
escaped_parts.join(" ")
556-
);
557-
558-
// Build command: nsenter --net=<netns> unshare --mount sh -c '<wrapper>'
559-
let mut cmd = Command::new("nsenter");
560-
cmd.arg(format!("--net={}", netns_path));
561-
cmd.arg("unshare");
562-
cmd.arg("--mount");
563-
cmd.arg("sh");
564-
cmd.arg("-c");
565-
cmd.arg(&shell_cmd);
566-
567535
// Set environment variables
568536
for (key, value) in extra_env {
569537
cmd.env(key, value);

0 commit comments

Comments
 (0)