Skip to content

Commit d7582ce

Browse files
ammarioclaude
andcommitted
feat: run DNS server inside namespace for localhost compatibility
Run the dummy DNS server inside the network namespace on 127.0.0.1:53 instead of on the host. This fixes DNS resolution on systems using systemd-resolved (nameserver 127.0.0.53) while still working with public DNS servers. Changes: - Update nftables DNAT to redirect DNS queries to 127.0.0.1:53 - Spawn DNS server inside namespace using `ip netns exec` - Add --__internal-dns-server flag for the spawned process - Bring up loopback interface before starting DNS server This approach is simpler than PR #56's fork+exec machinery and works universally across different Linux DNS configurations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b8f4832 commit d7582ce

4 files changed

Lines changed: 92 additions & 23 deletions

File tree

src/jail/linux/dns.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,47 @@ fn build_dummy_response(query: Packet<'_>) -> Result<Vec<u8>> {
141141
.map_err(|e| anyhow::anyhow!("Failed to build DNS response: {}", e))
142142
}
143143

144+
/// Run DNS server synchronously (blocks forever). Used when spawned inside namespace.
145+
pub fn run_dns_server_blocking() -> Result<()> {
146+
info!("Starting blocking DNS server on 0.0.0.0:53");
147+
148+
let socket =
149+
UdpSocket::bind("0.0.0.0:53").context("Failed to bind DNS server to 0.0.0.0:53")?;
150+
151+
socket.set_read_timeout(Some(Duration::from_millis(500)))?;
152+
153+
let mut buf = [0u8; MAX_DNS_PACKET_SIZE];
154+
loop {
155+
match socket.recv_from(&mut buf) {
156+
Ok((size, src)) => {
157+
debug!("Received DNS query from {}: {} bytes", src, size);
158+
159+
match Packet::parse(&buf[..size]) {
160+
Ok(query) => {
161+
if let Ok(response) = build_dummy_response(query) {
162+
if let Err(e) = socket.send_to(&response, src) {
163+
warn!("Failed to send DNS response to {}: {}", src, e);
164+
} else {
165+
debug!("Sent dummy DNS response to {}", src);
166+
}
167+
}
168+
}
169+
Err(e) => {
170+
debug!("Failed to parse DNS query: {}", e);
171+
}
172+
}
173+
}
174+
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
175+
Err(e) if e.kind() == std::io::ErrorKind::TimedOut => continue,
176+
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
177+
Err(e) => {
178+
error!("DNS server receive error: {}", e);
179+
return Err(e.into());
180+
}
181+
}
182+
}
183+
}
184+
144185
#[cfg(test)]
145186
mod tests {
146187
use super::*;

src/jail/linux/mod.rs

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
mod dns;
1+
pub mod dns;
22
mod nftables;
33
mod resources;
44

@@ -9,10 +9,8 @@ use super::Jail;
99
use super::JailConfig;
1010
use crate::sys_resource::ManagedResource;
1111
use anyhow::{Context, Result};
12-
use dns::DummyDnsServer;
1312
use resources::{NFTable, NetworkNamespace, VethPair};
1413
use std::process::{Command, ExitStatus};
15-
use std::sync::{Arc, Mutex};
1614
use tracing::{debug, info, warn};
1715

1816
// Linux namespace network configuration constants were previously fixed; the
@@ -66,7 +64,7 @@ pub struct LinuxJail {
6664
namespace: Option<ManagedResource<NetworkNamespace>>,
6765
veth_pair: Option<ManagedResource<VethPair>>,
6866
nftables: Option<ManagedResource<NFTable>>,
69-
dns_server: Option<Arc<Mutex<DummyDnsServer>>>,
67+
dns_server_child: Option<std::process::Child>,
7068
// Per-jail computed networking (unique /30 inside 10.99/16)
7169
host_ip: [u8; 4],
7270
host_cidr: String,
@@ -83,7 +81,7 @@ impl LinuxJail {
8381
namespace: None,
8482
veth_pair: None,
8583
nftables: None,
86-
dns_server: None,
84+
dns_server_child: None,
8785
host_ip,
8886
host_cidr,
8987
guest_cidr,
@@ -368,29 +366,43 @@ impl LinuxJail {
368366
fn start_dns_server(&mut self) -> Result<()> {
369367
let namespace_name = self.namespace_name();
370368

371-
info!("Starting dummy DNS server in namespace {}", namespace_name);
369+
info!(
370+
"Starting dummy DNS server inside namespace {}",
371+
namespace_name
372+
);
372373

373-
// Start the DNS server on the host side
374-
let mut dns_server = DummyDnsServer::new();
374+
// Spawn httpjail inside the namespace to run the DNS server
375+
// This uses the --__internal-dns-server flag which calls run_dns_server_blocking()
376+
let exe_path = std::env::current_exe().context("Failed to get current executable path")?;
375377

376-
// Bind directly to port 53 on the host IP - no redirection needed
377-
let dns_bind_addr = format!("{}:53", format_ip(self.host_ip));
378-
dns_server.start(&dns_bind_addr)?;
378+
let child = std::process::Command::new("ip")
379+
.args([
380+
"netns",
381+
"exec",
382+
&namespace_name,
383+
exe_path.to_str().context("Invalid executable path")?,
384+
"--__internal-dns-server",
385+
])
386+
.stdin(std::process::Stdio::null())
387+
.stdout(std::process::Stdio::null())
388+
.stderr(std::process::Stdio::null())
389+
.spawn()
390+
.context("Failed to spawn in-namespace DNS server")?;
379391

380-
info!("Started dummy DNS server on {}", dns_bind_addr);
392+
info!("Started in-namespace DNS server (pid {})", child.id());
381393

382-
self.dns_server = Some(Arc::new(Mutex::new(dns_server)));
394+
self.dns_server_child = Some(child);
383395

384396
Ok(())
385397
}
386398

387399
/// Stop the DNS server
388400
fn stop_dns_server(&mut self) {
389-
if let Some(dns_server_arc) = self.dns_server.take() {
390-
if let Ok(mut dns_server) = dns_server_arc.lock() {
391-
dns_server.stop();
392-
info!("Stopped dummy DNS server");
393-
}
401+
if let Some(mut child) = self.dns_server_child.take() {
402+
debug!("Stopping in-namespace DNS server (pid {})", child.id());
403+
let _ = child.kill();
404+
let _ = child.wait();
405+
debug!("Stopped in-namespace DNS server");
394406
}
395407
}
396408
}
@@ -567,7 +579,7 @@ impl Clone for LinuxJail {
567579
namespace: None,
568580
veth_pair: None,
569581
nftables: None,
570-
dns_server: None,
582+
dns_server_child: None,
571583
host_ip: self.host_ip,
572584
host_cidr: self.host_cidr.clone(),
573585
guest_cidr: self.guest_cidr.clone(),

src/jail/linux/nftables.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,9 @@ table ip {table_name} {{
127127
chain output {{
128128
type nat hook output priority -100; policy accept;
129129
130-
# Redirect all DNS queries to our dummy DNS server on host
130+
# Redirect all DNS queries to our dummy DNS server running in namespace
131131
# This works regardless of what nameserver is in /etc/resolv.conf
132-
udp dport 53 dnat to {host_ip}
132+
udp dport 53 dnat to 127.0.0.1:53
133133
134134
# Redirect HTTP to proxy running on host
135135
tcp dport 80 dnat to {host_ip}:{http_port}
@@ -145,8 +145,8 @@ table ip {table_name} {{
145145
# Always allow established/related traffic
146146
ct state established,related accept
147147
148-
# Allow DNS traffic to the host (after DNAT redirection)
149-
ip daddr {host_ip} udp dport 53 accept
148+
# Allow DNS traffic to localhost (after DNAT redirection)
149+
ip daddr 127.0.0.1 udp dport 53 accept
150150
151151
# Allow traffic to the host proxy ports after DNAT
152152
ip daddr {host_ip} tcp dport {{ {http_port}, {https_port} }} accept

src/main.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,22 @@ fn cleanup_orphans() -> Result<()> {
306306

307307
#[tokio::main]
308308
async fn main() -> Result<()> {
309+
// Handle internal DNS server flag (must be first, before clap parsing)
310+
// This is called by LinuxJail when spawning DNS server inside namespace
311+
#[cfg(target_os = "linux")]
312+
if std::env::args().any(|arg| arg == "--__internal-dns-server") {
313+
use httpjail::jail::linux::dns::run_dns_server_blocking;
314+
315+
// Bring up loopback interface in the namespace
316+
std::process::Command::new("ip")
317+
.args(["link", "set", "lo", "up"])
318+
.output()
319+
.context("Failed to bring up loopback interface")?;
320+
321+
// Run the DNS server (blocks forever)
322+
return run_dns_server_blocking();
323+
}
324+
309325
let args = Args::parse();
310326

311327
// Handle trust subcommand (takes precedence)

0 commit comments

Comments
 (0)