use anyhow::{Context, Result};
use clap::Subcommand;
use macos_resolver::{FileResolver, ResolverConfig, to_env_prefix};
use std::net::{SocketAddr, UdpSocket};
use std::time::Duration;
const PREFIX: &str = "arcbox";
const DEFAULT_DNS_PORT: u16 = 5553;
const DEFAULT_DNS_DOMAIN: &str = "arcbox.local";
fn dns_port() -> u16 {
let key = format!("{}_DNS_PORT", to_env_prefix(PREFIX));
std::env::var(key)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_DNS_PORT)
}
fn dns_domain() -> String {
let key = format!("{}_DNS_DOMAIN", to_env_prefix(PREFIX));
std::env::var(key).unwrap_or_else(|_| DEFAULT_DNS_DOMAIN.to_string())
}
#[derive(Debug, Subcommand)]
pub enum DnsCommands {
Install,
Uninstall,
Status,
}
pub async fn execute(cmd: DnsCommands) -> Result<()> {
match cmd {
DnsCommands::Install => execute_install().await,
DnsCommands::Uninstall => execute_uninstall().await,
DnsCommands::Status => execute_status().await,
}
}
async fn execute_install() -> Result<()> {
let resolver = FileResolver::new(PREFIX);
let domain = dns_domain();
let port = dns_port();
let config = ResolverConfig::new(&domain, "127.0.0.1", port);
match resolver.register_permanent(&config) {
Ok(()) => {
println!("Installed DNS resolver: /etc/resolver/{domain}");
println!(" nameserver 127.0.0.1");
println!(" port {port}");
println!();
println!("All *.{domain} queries will be routed to the ArcBox DNS server.");
Ok(())
}
Err(ref e) if e.is_permission_denied() => {
eprintln!("Error: permission denied writing to /etc/resolver/");
eprintln!();
eprintln!("Run with sudo:");
eprintln!(" sudo arcbox dns install");
std::process::exit(1);
}
Err(e) => Err(e).context("Failed to install DNS resolver"),
}
}
async fn execute_uninstall() -> Result<()> {
let resolver = FileResolver::new(PREFIX);
let domain = dns_domain();
match resolver.unregister(&domain) {
Ok(()) => {
println!("Removed DNS resolver: /etc/resolver/{domain}");
Ok(())
}
Err(ref e) if e.is_permission_denied() => {
eprintln!("Error: permission denied removing /etc/resolver/{domain}");
eprintln!();
eprintln!("Run with sudo:");
eprintln!(" sudo arcbox dns uninstall");
std::process::exit(1);
}
Err(e) => Err(e).context("Failed to uninstall DNS resolver"),
}
}
async fn execute_status() -> Result<()> {
let resolver = FileResolver::new(PREFIX);
let domain = dns_domain();
let port = dns_port();
let installed = resolver.is_registered(&domain);
if installed {
println!("Resolver file: installed (/etc/resolver/{domain})");
} else {
println!("Resolver file: not installed");
}
let reachable = is_dns_port_reachable(port);
if reachable {
println!("DNS server: reachable (127.0.0.1:{port})");
} else {
println!("DNS server: not reachable (127.0.0.1:{port})");
}
if !installed {
println!();
println!("Run 'sudo arcbox dns install' to enable *.{domain} DNS resolution.");
}
if !reachable {
println!();
println!("Start the ArcBox daemon to provide DNS service on port {port}.");
}
Ok(())
}
fn is_dns_port_reachable(port: u16) -> bool {
let addr: SocketAddr = ([127, 0, 0, 1], port).into();
let Ok(socket) = UdpSocket::bind("0.0.0.0:0") else {
return false;
};
let _ = socket.set_read_timeout(Some(Duration::from_millis(200)));
let query: [u8; 17] = [
0x12, 0x34, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, ];
if socket.send_to(&query, addr).is_err() {
return false;
}
let mut buf = [0u8; 512];
socket.recv_from(&mut buf).is_ok()
}