237 lines
7.2 KiB
Rust
237 lines
7.2 KiB
Rust
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<u8>)>,
|
|
) -> 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<u32>,
|
|
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::<Vec<_>>()
|
|
.join(" ")
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn restore(
|
|
interface: &str,
|
|
if_index: Option<u32>,
|
|
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<u8>)
|
|
let addrs: Vec<(i32, Vec<u8>)> = 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::<Vec<_>>()
|
|
.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<String> = 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(())
|
|
}
|