Skip to content

Commit e06724b

Browse files
committed
refactor: DRY cleanup and improved documentation for NetnsResolv
Improvements: - Extracted resolve_resolv_conf_target() helper to eliminate duplication - Consolidated symlink handling documentation in struct-level comments - Simplified create_with_nameserver() and cleanup() methods - Replaced verbose comments with references to struct documentation - Used match expression for cleaner error handling in cleanup All functionality preserved - tests still pass (23/23 on ml-1)
1 parent 98046b1 commit e06724b

1 file changed

Lines changed: 61 additions & 68 deletions

File tree

src/jail/linux/resources.rs

Lines changed: 61 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -186,66 +186,82 @@ impl SystemResource for NFTable {
186186
}
187187
}
188188

189-
/// /etc/netns/ resolv.conf resource
189+
/// /etc/netns/ resolv.conf resource for network namespace DNS configuration
190+
///
191+
/// # How it works
190192
///
191193
/// Uses Linux kernel's built-in /etc/netns/ mechanism. When a process enters
192-
/// a network namespace, the kernel automatically bind-mounts files from
193-
/// /etc/netns/<namespace>/ over their corresponding paths.
194+
/// a network namespace via `ip netns exec`, the kernel automatically bind-mounts
195+
/// files from /etc/netns/<namespace>/ over their corresponding paths.
196+
///
197+
/// # Symlinked /etc/resolv.conf handling
198+
///
199+
/// When /etc/resolv.conf is a symlink (common with systemd-resolved pointing to
200+
/// /run/systemd/resolve/stub-resolv.conf), ip netns exec follows the symlink and
201+
/// bind-mounts onto the target file. This requires:
202+
///
203+
/// 1. **Creation**: Ensure the symlink target exists on the host (we create an empty
204+
/// placeholder in /run/systemd/resolve/ if needed - safe since /run is tmpfs)
205+
/// 2. **Cleanup**: Explicitly unmount the bind-mount at the symlink target during
206+
/// cleanup to prevent stale mounts from accumulating
194207
///
195-
/// This approach:
196-
/// - Leverages standard Linux kernel feature
197-
/// - No manual mount commands needed
198-
/// - Works with symlinked /etc/resolv.conf
199-
/// - Simple and robust
208+
/// # Safety
209+
///
210+
/// - Host's /etc/resolv.conf is never modified directly
211+
/// - Placeholder creation is best-effort and won't affect systemd-resolved
212+
/// - Cleanup unmounts are idempotent and won't fail if already unmounted
200213
pub struct NetnsResolv {
201214
netns_dir: PathBuf,
202215
created: bool,
203216
}
204217

205218
impl NetnsResolv {
206-
/// Create /etc/netns/httpjail_<id>/resolv.conf with specified nameserver
219+
/// Resolve /etc/resolv.conf to its canonical path, handling symlinks
207220
///
208-
/// The kernel will automatically bind-mount this over /etc/resolv.conf
209-
/// when entering the namespace.
221+
/// Returns None if /etc/resolv.conf is not a symlink or cannot be resolved
222+
fn resolve_resolv_conf_target() -> Option<PathBuf> {
223+
let symlink_target = fs::read_link("/etc/resolv.conf").ok()?;
224+
225+
// Convert relative path to absolute (e.g., "../run/systemd/resolve/stub-resolv.conf")
226+
let absolute_path = if symlink_target.is_absolute() {
227+
symlink_target
228+
} else {
229+
PathBuf::from("/etc").join(symlink_target)
230+
};
231+
232+
fs::canonicalize(absolute_path).ok()
233+
}
234+
235+
/// Create /etc/netns/httpjail_<id>/resolv.conf with specified nameserver
210236
pub fn create_with_nameserver(jail_id: &str, nameserver_ip: &str) -> Result<Self> {
211237
let netns_dir = PathBuf::from(format!("/etc/netns/httpjail_{}", jail_id));
212238

213-
// Create /etc/netns/<namespace>/ directory
239+
// Create directory and write resolv.conf
214240
fs::create_dir_all(&netns_dir)
215241
.with_context(|| format!("Failed to create {}", netns_dir.display()))?;
216242

217-
// Write resolv.conf as a regular file (not symlink)
218243
let resolv_path = netns_dir.join("resolv.conf");
219-
let content = format!("# httpjail managed\nnameserver {}\n", nameserver_ip);
220-
fs::write(&resolv_path, content)
221-
.with_context(|| format!("Failed to write {}", resolv_path.display()))?;
244+
fs::write(
245+
&resolv_path,
246+
format!("# httpjail managed\nnameserver {}\n", nameserver_ip),
247+
)
248+
.with_context(|| format!("Failed to write {}", resolv_path.display()))?;
222249

223250
info!(
224251
"Created {} with nameserver {}",
225252
resolv_path.display(),
226253
nameserver_ip
227254
);
228255

229-
// CRITICAL FALLBACK: Create placeholder for symlink target on the HOST
230-
//
231-
// When /etc/resolv.conf is a symlink (e.g., to /run/systemd/resolve/stub-resolv.conf),
232-
// ip netns exec needs the symlink target to exist for bind-mounting to work.
233-
//
234-
// We create an empty placeholder file on the HOST at /run/systemd/resolve/stub-resolv.conf.
235-
// This is safe because:
236-
// 1. /run is a tmpfs (RAM-based, cleared on reboot)
237-
// 2. systemd-resolved manages this file and will overwrite it if needed
238-
// 3. Our file is empty (0 bytes), won't affect anything
239-
// 4. This is only a fallback for systems where the file doesn't already exist
240-
//
241-
// We silently ignore errors if the directory doesn't exist or we don't have permissions.
242-
let _ = std::fs::create_dir_all("/run/systemd/resolve");
243-
let _ = std::fs::OpenOptions::new()
256+
// Ensure symlink target exists (see struct documentation for details)
257+
// Best-effort: ignore errors since the file might already exist or we lack permissions
258+
let _ = fs::create_dir_all("/run/systemd/resolve");
259+
let _ = fs::OpenOptions::new()
244260
.create_new(true)
245261
.write(true)
246262
.open("/run/systemd/resolve/stub-resolv.conf");
247263

248-
debug!("Created placeholder /run/systemd/resolve/stub-resolv.conf if needed");
264+
debug!("Ensured /run/systemd/resolve/stub-resolv.conf exists");
249265

250266
Ok(Self {
251267
netns_dir,
@@ -266,44 +282,21 @@ impl SystemResource for NetnsResolv {
266282
return Ok(());
267283
}
268284

269-
// CRITICAL: Unmount bind-mount created by ip netns exec
270-
//
271-
// When /etc/resolv.conf is a symlink (e.g., to /run/systemd/resolve/stub-resolv.conf),
272-
// ip netns exec bind-mounts our custom resolv.conf onto the symlink target.
273-
// This creates a bind-mount on the HOST that must be explicitly unmounted.
274-
//
275-
// We try to resolve /etc/resolv.conf and unmount it if it's a symlink target.
276-
// This is safe because:
277-
// - We only unmount if the file exists (best-effort)
278-
// - Failed unmounts are logged but don't fail cleanup
279-
// - Multiple unmounts are idempotent (second unmount will fail silently)
280-
if let Ok(resolved_path) = fs::read_link("/etc/resolv.conf") {
281-
// resolved_path might be relative like "../run/systemd/resolve/stub-resolv.conf"
282-
// Convert to absolute path
283-
let absolute_path = if resolved_path.is_absolute() {
284-
resolved_path
285-
} else {
286-
PathBuf::from("/etc").join(&resolved_path)
287-
};
288-
289-
// Canonicalize to get the real path
290-
if let Ok(canonical_path) = fs::canonicalize(&absolute_path) {
291-
// Try to unmount the bind-mount (best effort)
292-
let _ = Command::new("umount").arg(&canonical_path).output();
293-
debug!(
294-
"Attempted to unmount bind-mount at {}",
295-
canonical_path.display()
296-
);
297-
}
285+
// Unmount bind-mount at symlink target (see struct documentation for why)
286+
// Best-effort: ignore failures since mount might already be cleaned up
287+
if let Some(target_path) = Self::resolve_resolv_conf_target() {
288+
let _ = Command::new("umount").arg(&target_path).output();
289+
debug!(
290+
"Attempted to unmount bind-mount at {}",
291+
target_path.display()
292+
);
298293
}
299294

300-
// Remove /etc/netns/<namespace>/ directory and all contents
301-
if let Err(e) = fs::remove_dir_all(&self.netns_dir) {
302-
if e.kind() != std::io::ErrorKind::NotFound {
303-
warn!("Failed to remove {}: {}", self.netns_dir.display(), e);
304-
}
305-
} else {
306-
debug!("Removed {}", self.netns_dir.display());
295+
// Remove /etc/netns/<namespace>/ directory
296+
match fs::remove_dir_all(&self.netns_dir) {
297+
Ok(()) => debug!("Removed {}", self.netns_dir.display()),
298+
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
299+
Err(e) => warn!("Failed to remove {}: {}", self.netns_dir.display(), e),
307300
}
308301

309302
self.created = false;

0 commit comments

Comments
 (0)