use std::io;
use std::net::Ipv4Addr;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DnsBackend {
Builtin,
Dnsmasq,
}
pub fn active_backend() -> DnsBackend {
static BACKEND: std::sync::OnceLock<DnsBackend> = std::sync::OnceLock::new();
*BACKEND.get_or_init(|| match std::env::var("PELAGOS_DNS_BACKEND").as_deref() {
Ok("dnsmasq") => DnsBackend::Dnsmasq,
_ => DnsBackend::Builtin,
})
}
pub fn dns_config_dir() -> PathBuf {
crate::paths::dns_config_dir()
}
fn daemon_pid() -> Option<i32> {
let pid_file = crate::paths::dns_pid_file();
let content = std::fs::read_to_string(pid_file).ok()?;
let pid: i32 = content.trim().parse().ok()?;
if unsafe { libc::kill(pid, 0) } == 0 {
Some(pid)
} else {
None
}
}
fn signal_reload() -> io::Result<()> {
if let Some(pid) = daemon_pid() {
let ret = unsafe { libc::kill(pid, libc::SIGHUP) };
if ret != 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
fn running_backend() -> Option<DnsBackend> {
let content = std::fs::read_to_string(crate::paths::dns_backend_file()).ok()?;
match content.trim() {
"dnsmasq" => Some(DnsBackend::Dnsmasq),
"builtin" => Some(DnsBackend::Builtin),
_ => None,
}
}
fn write_backend_marker(backend: DnsBackend) -> io::Result<()> {
let label = match backend {
DnsBackend::Builtin => "builtin",
DnsBackend::Dnsmasq => "dnsmasq",
};
std::fs::write(crate::paths::dns_backend_file(), label)
}
fn stop_daemon() -> io::Result<()> {
if let Some(pid) = daemon_pid() {
log::info!("stopping DNS daemon (PID {})", pid);
unsafe { libc::kill(pid, libc::SIGTERM) };
for _ in 0..20 {
std::thread::sleep(std::time::Duration::from_millis(50));
if unsafe { libc::kill(pid, 0) } != 0 {
break;
}
}
let _ = std::fs::remove_file(crate::paths::dns_pid_file());
}
Ok(())
}
pub fn ensure_dns_daemon() -> io::Result<()> {
let desired = active_backend();
if daemon_pid().is_some() {
if let Some(running) = running_backend() {
if running != desired {
log::info!(
"DNS backend changed ({:?} → {:?}), restarting daemon",
running,
desired
);
stop_daemon()?;
} else {
return Ok(());
}
} else {
return Ok(());
}
}
let config_dir = dns_config_dir();
std::fs::create_dir_all(&config_dir)?;
match desired {
DnsBackend::Builtin => {
ensure_builtin_daemon()?;
write_backend_marker(DnsBackend::Builtin)?;
}
DnsBackend::Dnsmasq => match ensure_dnsmasq_daemon() {
Ok(()) => {
write_backend_marker(DnsBackend::Dnsmasq)?;
}
Err(e) => {
log::warn!("dnsmasq backend failed ({}), falling back to builtin", e);
ensure_builtin_daemon()?;
write_backend_marker(DnsBackend::Builtin)?;
}
},
}
Ok(())
}
pub fn dns_add_entry(
network_name: &str,
container_name: &str,
ip: Ipv4Addr,
gateway: Ipv4Addr,
upstream: &[String],
) -> io::Result<()> {
let config_dir = dns_config_dir();
std::fs::create_dir_all(&config_dir)?;
let config_file = crate::paths::dns_network_file(network_name);
let lock_path = config_dir.join(format!("{}.lock", network_name));
let lock_file = std::fs::File::create(&lock_path)?;
flock_exclusive(&lock_file)?;
let content = std::fs::read_to_string(&config_file).unwrap_or_default();
let new_content = if content.is_empty() {
let upstream_str = if upstream.is_empty() {
"8.8.8.8,1.1.1.1".to_string()
} else {
upstream.join(",")
};
format!("{} {}\n{} {}\n", gateway, upstream_str, container_name, ip)
} else {
let mut lines: Vec<String> = content
.lines()
.filter(|line| {
let first_word = line.split_whitespace().next().unwrap_or("");
first_word != container_name
})
.map(|s| s.to_string())
.collect();
lines.push(format!("{} {}", container_name, ip));
lines.join("\n") + "\n"
};
std::fs::write(&config_file, new_content)?;
drop(lock_file);
let _ = std::fs::remove_file(&lock_path);
if let Ok(net_def) = crate::network::load_network_def(network_name) {
allow_dns_on_bridge(&net_def.bridge_name);
}
if active_backend() == DnsBackend::Dnsmasq {
regenerate_dnsmasq_hosts(network_name)?;
generate_dnsmasq_conf()?;
}
ensure_dns_daemon()?;
signal_reload()
}
pub fn dns_remove_entry(network_name: &str, container_name: &str) -> io::Result<()> {
let config_dir = dns_config_dir();
let config_file = crate::paths::dns_network_file(network_name);
if !config_file.exists() {
return Ok(());
}
let lock_path = config_dir.join(format!("{}.lock", network_name));
let lock_file = std::fs::File::create(&lock_path)?;
flock_exclusive(&lock_file)?;
let content = std::fs::read_to_string(&config_file)?;
let mut header = String::new();
let mut entries = Vec::new();
for (i, line) in content.lines().enumerate() {
if i == 0 {
header = line.to_string();
continue;
}
let first_word = line.split_whitespace().next().unwrap_or("");
if first_word != container_name && !line.trim().is_empty() {
entries.push(line.to_string());
}
}
if entries.is_empty() {
let _ = std::fs::remove_file(&config_file);
if let Ok(net_def) = crate::network::load_network_def(network_name) {
disallow_dns_on_bridge(&net_def.bridge_name);
}
if active_backend() == DnsBackend::Dnsmasq {
let _ = std::fs::remove_file(crate::paths::dns_hosts_file(network_name));
}
} else {
let mut new_content = header + "\n";
for entry in &entries {
new_content.push_str(entry);
new_content.push('\n');
}
std::fs::write(&config_file, new_content)?;
if active_backend() == DnsBackend::Dnsmasq {
regenerate_dnsmasq_hosts(network_name)?;
}
}
drop(lock_file);
let _ = std::fs::remove_file(&lock_path);
if active_backend() == DnsBackend::Dnsmasq {
generate_dnsmasq_conf()?;
}
signal_reload()
}
fn ensure_builtin_daemon() -> io::Result<()> {
if daemon_pid().is_some() {
return Ok(());
}
let config_dir = dns_config_dir();
std::fs::create_dir_all(&config_dir)?;
let dns_bin = find_dns_binary()?;
log::info!("starting builtin DNS daemon: {}", dns_bin.display());
let fork1 = unsafe { libc::fork() };
match fork1 {
-1 => return Err(io::Error::last_os_error()),
0 => {
unsafe { libc::setsid() };
let fork2 = unsafe { libc::fork() };
match fork2 {
-1 => unsafe { libc::_exit(1) },
0 => {
let devnull = unsafe { libc::open(c"/dev/null".as_ptr(), libc::O_RDWR) };
if devnull >= 0 {
unsafe {
libc::dup2(devnull, 0);
libc::dup2(devnull, 1);
libc::dup2(devnull, 2);
if devnull > 2 {
libc::close(devnull);
}
}
}
unsafe {
let mut fds_to_close: [i32; 256] = [-1; 256];
let mut n = 0usize;
let dir = libc::opendir(c"/proc/self/fd".as_ptr());
if !dir.is_null() {
loop {
let e = libc::readdir(dir);
if e.is_null() {
break;
}
let name = std::ffi::CStr::from_ptr((*e).d_name.as_ptr());
if let Ok(s) = name.to_str() {
if let Ok(fd) = s.parse::<i32>() {
if fd > 2 && n < fds_to_close.len() {
fds_to_close[n] = fd;
n += 1;
}
}
}
}
libc::closedir(dir);
}
for &fd in &fds_to_close[..n] {
libc::close(fd);
}
}
let config_dir_str = config_dir.to_string_lossy().to_string();
let _ = exec_dns_binary(&dns_bin, &config_dir_str);
unsafe { libc::_exit(1) };
}
_ => {
unsafe { libc::_exit(0) };
}
}
}
child_pid => {
unsafe {
libc::waitpid(child_pid, std::ptr::null_mut(), 0);
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
Ok(())
}
fn find_dns_binary() -> io::Result<PathBuf> {
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
let candidate = dir.join("pelagos-dns");
if candidate.exists() {
return Ok(candidate);
}
if let Some(parent) = dir.parent() {
let candidate = parent.join("pelagos-dns");
if candidate.exists() {
return Ok(candidate);
}
}
}
}
if let Ok(output) = std::process::Command::new("which")
.arg("pelagos-dns")
.output()
{
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !path.is_empty() {
return Ok(PathBuf::from(path));
}
}
}
Err(io::Error::other(
"pelagos-dns binary not found (expected next to pelagos binary or in PATH)",
))
}
fn exec_dns_binary(bin: &std::path::Path, config_dir: &str) -> io::Error {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let bin_c = CString::new(bin.as_os_str().as_bytes()).unwrap();
let arg_config = CString::new("--config-dir").unwrap();
let arg_dir = CString::new(config_dir).unwrap();
let args = [
bin_c.as_ptr(),
arg_config.as_ptr(),
arg_dir.as_ptr(),
std::ptr::null(),
];
unsafe {
libc::execv(bin_c.as_ptr(), args.as_ptr());
}
io::Error::last_os_error()
}
fn find_dnsmasq() -> io::Result<PathBuf> {
let output = std::process::Command::new("which")
.arg("dnsmasq")
.output()
.map_err(|e| io::Error::other(format!("failed to search for dnsmasq: {}", e)))?;
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !path.is_empty() {
return Ok(PathBuf::from(path));
}
}
Err(io::Error::other(
"dnsmasq not found on PATH (install dnsmasq or use --dns-backend builtin)",
))
}
fn generate_dnsmasq_conf() -> io::Result<()> {
let config_dir = dns_config_dir();
let mut listen_addresses = Vec::new();
let mut upstream_servers = Vec::new();
let mut hosts_files = Vec::new();
let entries = std::fs::read_dir(&config_dir)?;
for entry in entries {
let entry = entry?;
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str == "pid"
|| name_str == "backend"
|| name_str == "dnsmasq.conf"
|| name_str.ends_with(".lock")
|| name_str.starts_with("hosts.")
{
continue;
}
let content = match std::fs::read_to_string(entry.path()) {
Ok(c) => c,
Err(_) => continue,
};
if let Some(header) = content.lines().next() {
let mut parts = header.split_whitespace();
if let Some(gw) = parts.next() {
if gw.parse::<Ipv4Addr>().is_ok() && !listen_addresses.contains(&gw.to_string()) {
listen_addresses.push(gw.to_string());
}
}
if let Some(upstreams) = parts.next() {
for server in upstreams.split(',') {
let s = server.trim().to_string();
if !s.is_empty() && !upstream_servers.contains(&s) {
upstream_servers.push(s);
}
}
}
}
let hosts_path = crate::paths::dns_hosts_file(&name_str);
if hosts_path.exists() {
hosts_files.push(hosts_path.to_string_lossy().to_string());
}
}
if listen_addresses.is_empty() {
return Ok(());
}
let pid_file = crate::paths::dns_pid_file();
let mut conf = String::from("# Auto-generated by pelagos — do not edit\n");
conf.push_str("no-resolv\n");
conf.push_str("no-daemon\n");
conf.push_str("bind-dynamic\n");
conf.push_str("local-service\n");
conf.push_str(&format!("pid-file={}\n", pid_file.display()));
for addr in &listen_addresses {
conf.push_str(&format!("listen-address={}\n", addr));
}
for server in &upstream_servers {
conf.push_str(&format!("server={}\n", server));
}
for hosts in &hosts_files {
conf.push_str(&format!("addn-hosts={}\n", hosts));
}
std::fs::write(crate::paths::dns_dnsmasq_conf(), conf)
}
fn regenerate_dnsmasq_hosts(network_name: &str) -> io::Result<()> {
let config_file = crate::paths::dns_network_file(network_name);
let content = match std::fs::read_to_string(&config_file) {
Ok(c) => c,
Err(_) => {
let _ = std::fs::remove_file(crate::paths::dns_hosts_file(network_name));
return Ok(());
}
};
let mut hosts = String::new();
for (i, line) in content.lines().enumerate() {
if i == 0 {
continue; }
let mut parts = line.split_whitespace();
if let (Some(name), Some(ip)) = (parts.next(), parts.next()) {
hosts.push_str(&format!("{}\t{}\n", ip, name));
}
}
std::fs::write(crate::paths::dns_hosts_file(network_name), hosts)
}
fn ensure_dnsmasq_daemon() -> io::Result<()> {
if daemon_pid().is_some() {
return Ok(());
}
let dnsmasq_bin = find_dnsmasq()?;
generate_dnsmasq_conf()?;
let conf_path = crate::paths::dns_dnsmasq_conf();
if !conf_path.exists() {
return Err(io::Error::other("no DNS config to serve"));
}
log::info!("starting dnsmasq DNS daemon");
let fork1 = unsafe { libc::fork() };
match fork1 {
-1 => return Err(io::Error::last_os_error()),
0 => {
unsafe { libc::setsid() };
let fork2 = unsafe { libc::fork() };
match fork2 {
-1 => unsafe { libc::_exit(1) },
0 => {
let devnull = unsafe { libc::open(c"/dev/null".as_ptr(), libc::O_RDWR) };
if devnull >= 0 {
unsafe {
libc::dup2(devnull, 0);
libc::dup2(devnull, 1);
libc::close(devnull);
}
}
let err = exec_dnsmasq(&dnsmasq_bin, &conf_path);
eprintln!("pelagos: failed to exec dnsmasq: {}", err);
unsafe { libc::_exit(1) };
}
_ => {
unsafe { libc::_exit(0) };
}
}
}
child_pid => {
unsafe {
libc::waitpid(child_pid, std::ptr::null_mut(), 0);
}
std::thread::sleep(std::time::Duration::from_millis(200));
}
}
Ok(())
}
fn exec_dnsmasq(bin: &std::path::Path, conf: &std::path::Path) -> io::Error {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let bin_c = CString::new(bin.as_os_str().as_bytes()).unwrap();
let conf_str = format!("--conf-file={}", conf.display());
let arg_conf = CString::new(conf_str.as_bytes()).unwrap();
let arg_fg = CString::new("--keep-in-foreground").unwrap();
let args = [
bin_c.as_ptr(),
arg_conf.as_ptr(),
arg_fg.as_ptr(),
std::ptr::null(),
];
unsafe {
libc::execv(bin_c.as_ptr(), args.as_ptr());
}
io::Error::last_os_error()
}
fn allow_dns_on_bridge(bridge: &str) {
use std::process::Command as SysCmd;
while SysCmd::new("iptables")
.args([
"-D", "INPUT", "-i", bridge, "-p", "udp", "--dport", "53", "-j", "ACCEPT",
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|s| s.success())
{}
let _ = SysCmd::new("iptables")
.args([
"-I", "INPUT", "-i", bridge, "-p", "udp", "--dport", "53", "-j", "ACCEPT",
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
}
fn disallow_dns_on_bridge(bridge: &str) {
use std::process::Command as SysCmd;
while SysCmd::new("iptables")
.args([
"-D", "INPUT", "-i", bridge, "-p", "udp", "--dport", "53", "-j", "ACCEPT",
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|s| s.success())
{}
}
fn flock_exclusive(file: &std::fs::File) -> io::Result<()> {
use std::os::unix::io::AsRawFd;
let ret = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) };
if ret != 0 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}