use super::DnsState; use anyhow::{Context, Result}; use std::net::IpAddr; use tracing::{info, warn}; // --------------------------------------------------------------------------- // systemd-resolved D-Bus proxy // --------------------------------------------------------------------------- #[zbus::proxy( interface = "org.freedesktop.resolve1.Manager", default_service = "org.freedesktop.resolve1", default_path = "/org/freedesktop/resolve1" )] trait Resolve1Manager { // a(iay): array of (address_family, raw_bytes) #[zbus(name = "SetLinkDNS")] fn set_link_dns( &self, ifindex: i32, addresses: Vec<(i32, Vec)>, ) -> zbus::Result<()>; // a(sb): array of (domain, is_routing_domain) fn set_link_domains( &self, ifindex: i32, domains: Vec<(String, bool)>, ) -> zbus::Result<()>; fn revert_link(&self, ifindex: i32) -> zbus::Result<()>; } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- pub async fn set_dns( interface: &str, if_index: Option, dns_servers: &[IpAddr], _assigned_ip: IpAddr, state: &mut DnsState, ) -> Result<()> { if dns_servers.is_empty() { return Ok(()); } // Prefer in-process D-Bus (no polkit when caller has CAP_NET_ADMIN). // Fall back to spawning resolvectl for environments without systemd-resolved. let result = match if_index { Some(idx) => set_dns_dbus(idx, dns_servers).await, None => { warn!("Interface index unknown, falling back to resolvectl subprocess"); set_dns_resolvectl(interface, dns_servers).await } }; match result { Ok(_) => { state.configured = true; } Err(e) => { warn!( "D-Bus DNS config failed ({:#}), trying resolvectl subprocess", e ); match set_dns_resolvectl(interface, dns_servers).await { Ok(_) => { state.configured = true; } Err(e2) => { warn!("Could not configure DNS: {}", e2); warn!("VPN DNS servers: {:?}", dns_servers); warn!( "Configure manually: resolvectl dns {} {}", interface, dns_servers .iter() .map(|ip| ip.to_string()) .collect::>() .join(" ") ); } } } } Ok(()) } pub async fn restore( interface: &str, if_index: Option, state: &mut DnsState, ) -> Result<()> { if !state.configured { return Ok(()); } info!("Restoring DNS settings..."); // Always attempt both paths: D-Bus RevertLink clears per-link config in // systemd-resolved's in-memory state; resolvectl revert is a belt-and-suspenders // pass that ensures the +DefaultRoute domain is removed even if D-Bus races // with interface teardown. if let Some(idx) = if_index { if let Err(e) = revert_link_dbus(idx).await { warn!("D-Bus RevertLink({}) failed: {}", idx, e); } } // Always run resolvectl revert as a second pass. revert_resolvectl(interface).await.ok(); state.configured = false; Ok(()) } // --------------------------------------------------------------------------- // D-Bus implementation (no subprocess, no polkit with CAP_NET_ADMIN) // --------------------------------------------------------------------------- async fn set_dns_dbus(if_index: u32, dns_servers: &[IpAddr]) -> Result<()> { let conn = zbus::Connection::system() .await .context("Failed to connect to D-Bus system bus")?; let proxy = Resolve1ManagerProxy::new(&conn) .await .context("Failed to create resolve1 proxy")?; // Encode addresses as (address_family: i32, raw_bytes: Vec) let addrs: Vec<(i32, Vec)> = dns_servers .iter() .map(|ip| match ip { IpAddr::V4(v4) => (2i32, v4.octets().to_vec()), IpAddr::V6(v6) => (10i32, v6.octets().to_vec()), }) .collect(); proxy .set_link_dns(if_index as i32, addrs) .await .context("SetLinkDNS failed")?; // Route all queries through the VPN link. // D-Bus SetLinkDomains takes (domain, is_routing_only): pass root domain "." // with is_routing_only=true, which systemd-resolved displays as "~." and marks // the link as DefaultRoute:yes. Passing "~." with true double-flags it and // results in a malformed domain that shows as DefaultRoute:no. proxy .set_link_domains(if_index as i32, vec![(".".to_string(), true)]) .await .context("SetLinkDomains failed")?; info!( "DNS configured via D-Bus for index {}: {}", if_index, dns_servers .iter() .map(|ip| ip.to_string()) .collect::>() .join(", ") ); Ok(()) } async fn revert_link_dbus(if_index: u32) -> Result<()> { let conn = zbus::Connection::system() .await .context("Failed to connect to D-Bus system bus")?; let proxy = Resolve1ManagerProxy::new(&conn) .await .context("Failed to create resolve1 proxy")?; // Clear DNS servers and routing domains explicitly before the full revert. // RevertLink alone can race with interface teardown in systemd-resolved; // zeroing each setting first ensures they are gone even if RevertLink races. let _ = proxy.set_link_dns(if_index as i32, vec![]).await; let _ = proxy .set_link_domains(if_index as i32, vec![]) .await; proxy .revert_link(if_index as i32) .await .context("RevertLink failed")?; Ok(()) } // --------------------------------------------------------------------------- // resolvectl subprocess fallback // --------------------------------------------------------------------------- async fn set_dns_resolvectl(interface: &str, dns_servers: &[IpAddr]) -> Result<()> { let dns_strs: Vec = dns_servers.iter().map(|ip| ip.to_string()).collect(); let status = tokio::process::Command::new("resolvectl") .arg("dns") .arg(interface) .args(&dns_strs) .status() .await .context("resolvectl not found")?; if !status.success() { anyhow::bail!("resolvectl dns failed"); } let status = tokio::process::Command::new("resolvectl") .args(["domain", interface, "~."]) .status() .await .context("resolvectl domain failed")?; if !status.success() { anyhow::bail!("resolvectl domain failed"); } info!( "DNS configured via resolvectl for {}: {}", interface, dns_strs.join(", ") ); Ok(()) } async fn revert_resolvectl(interface: &str) -> Result<()> { let _ = tokio::process::Command::new("resolvectl") .args(["revert", interface]) .status() .await; Ok(()) }