Dorian f07ce10b1a refactor: update dependencies and remove unused code
- Added new dependencies: `adler2`, `crc32fast`, `flate2`, `miniz_oxide`, and `libredox`.
- Updated existing dependencies: `tokio-rustls` to version 0.26.4 and `filetime` to version 0.2.27.
- Removed the `backup.rs` file as it is no longer needed.
- Introduced tests for configuration and credential management.
- Enhanced the `identity` module to generate W3C compliant DID documents.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 00:19:30 +00:00

383 lines
12 KiB
Rust

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
use tokio::fs;
use tracing::{debug, info};
const DNS_CONFIG_FILE: &str = "dns_config.json";
/// DNS provider presets with server addresses.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum DnsProvider {
/// Use system default (DHCP-assigned DNS)
System,
/// Cloudflare DNS-over-HTTPS (1.1.1.1)
Cloudflare,
/// Google DNS-over-HTTPS (8.8.8.8)
Google,
/// Quad9 DNS-over-HTTPS (9.9.9.9)
Quad9,
/// Mullvad DNS (no logging)
Mullvad,
/// Custom user-specified servers
Custom,
}
impl std::fmt::Display for DnsProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::System => write!(f, "system"),
Self::Cloudflare => write!(f, "cloudflare"),
Self::Google => write!(f, "google"),
Self::Quad9 => write!(f, "quad9"),
Self::Mullvad => write!(f, "mullvad"),
Self::Custom => write!(f, "custom"),
}
}
}
/// Persisted DNS configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DnsConfig {
pub provider: DnsProvider,
pub servers: Vec<String>,
pub doh_enabled: bool,
pub doh_url: Option<String>,
}
impl Default for DnsConfig {
fn default() -> Self {
Self {
provider: DnsProvider::System,
servers: Vec::new(),
doh_enabled: false,
doh_url: None,
}
}
}
/// Current DNS status read from the system.
#[derive(Debug, Serialize)]
pub struct DnsStatus {
pub provider: String,
pub servers: Vec<String>,
pub doh_enabled: bool,
pub doh_url: Option<String>,
pub resolv_conf_servers: Vec<String>,
}
/// Load persisted DNS config from disk.
pub async fn load_config(data_dir: &Path) -> Result<DnsConfig> {
let path = data_dir.join(DNS_CONFIG_FILE);
if !path.exists() {
return Ok(DnsConfig::default());
}
let data = fs::read_to_string(&path)
.await
.context("Reading DNS config")?;
serde_json::from_str(&data).context("Parsing DNS config")
}
/// Save DNS config to disk.
pub async fn save_config(data_dir: &Path, config: &DnsConfig) -> Result<()> {
let path = data_dir.join(DNS_CONFIG_FILE);
let data = serde_json::to_string_pretty(config)?;
fs::write(&path, data)
.await
.context("Writing DNS config")?;
Ok(())
}
/// Get the DNS servers for a given provider preset.
pub fn provider_servers(provider: &DnsProvider) -> (Vec<String>, Option<String>) {
match provider {
DnsProvider::System => (Vec::new(), None),
DnsProvider::Cloudflare => (
vec!["1.1.1.1".into(), "1.0.0.1".into()],
Some("https://cloudflare-dns.com/dns-query".into()),
),
DnsProvider::Google => (
vec!["8.8.8.8".into(), "8.8.4.4".into()],
Some("https://dns.google/dns-query".into()),
),
DnsProvider::Quad9 => (
vec!["9.9.9.9".into(), "149.112.112.112".into()],
Some("https://dns.quad9.net/dns-query".into()),
),
DnsProvider::Mullvad => (
vec!["194.242.2.2".into()],
Some("https://dns.mullvad.net/dns-query".into()),
),
DnsProvider::Custom => (Vec::new(), None),
}
}
/// Read current DNS servers from /etc/resolv.conf.
pub async fn read_resolv_conf() -> Result<Vec<String>> {
let content = fs::read_to_string("/etc/resolv.conf")
.await
.unwrap_or_default();
let servers: Vec<String> = content
.lines()
.filter_map(|line| {
let trimmed = line.trim();
if trimmed.starts_with("nameserver") {
trimmed.split_whitespace().nth(1).map(String::from)
} else {
None
}
})
.collect();
Ok(servers)
}
/// Get current DNS status combining config + system state.
pub async fn get_status(data_dir: &Path) -> Result<DnsStatus> {
let config = load_config(data_dir).await?;
let resolv_servers = read_resolv_conf().await.unwrap_or_default();
Ok(DnsStatus {
provider: config.provider.to_string(),
servers: if config.servers.is_empty() {
resolv_servers.clone()
} else {
config.servers.clone()
},
doh_enabled: config.doh_enabled,
doh_url: config.doh_url.clone(),
resolv_conf_servers: resolv_servers,
})
}
/// Apply DNS configuration to the system via nmcli.
///
/// Sets DNS servers on the active NetworkManager connection(s).
pub async fn apply_dns(config: &DnsConfig) -> Result<()> {
if config.provider == DnsProvider::System {
// Revert to DHCP-assigned DNS
info!("Reverting to system (DHCP) DNS");
apply_dns_via_nmcli(&[]).await?;
return Ok(());
}
let servers = &config.servers;
if servers.is_empty() {
anyhow::bail!("No DNS servers specified");
}
// Validate all server IPs
for s in servers {
if s.parse::<std::net::IpAddr>().is_err() {
anyhow::bail!("Invalid DNS server IP: {}", s);
}
}
info!(provider = %config.provider, servers = ?servers, "Applying DNS configuration");
apply_dns_via_nmcli(servers).await?;
Ok(())
}
/// Apply DNS servers to all active NetworkManager connections.
async fn apply_dns_via_nmcli(servers: &[String]) -> Result<()> {
// Get active connections
let output = tokio::process::Command::new("nmcli")
.args(["-t", "-f", "NAME,DEVICE,TYPE", "connection", "show", "--active"])
.output()
.await
.context("Failed to list nmcli connections")?;
if !output.status.success() {
anyhow::bail!(
"nmcli connection show failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let stdout = String::from_utf8(output.stdout).context("nmcli output not utf8")?;
let connections: Vec<&str> = stdout
.lines()
.filter_map(|line| {
let parts: Vec<&str> = line.splitn(3, ':').collect();
if parts.len() >= 3 {
let conn_type = parts[2];
// Only modify ethernet and wifi connections
if conn_type.contains("ethernet") || conn_type.contains("wireless") || conn_type.contains("wifi") {
return Some(parts[0]);
}
}
None
})
.collect();
if connections.is_empty() {
debug!("No active ethernet/wifi connections found, skipping DNS apply");
return Ok(());
}
let dns_value = if servers.is_empty() {
String::new() // Empty clears custom DNS, reverts to DHCP
} else {
servers.join(" ")
};
for conn_name in &connections {
// Set DNS servers
let dns_args = if dns_value.is_empty() {
vec![
"connection".to_string(),
"modify".to_string(),
conn_name.to_string(),
"ipv4.dns".to_string(),
String::new(),
"ipv4.ignore-auto-dns".to_string(),
"no".to_string(),
]
} else {
vec![
"connection".to_string(),
"modify".to_string(),
conn_name.to_string(),
"ipv4.dns".to_string(),
dns_value.clone(),
"ipv4.ignore-auto-dns".to_string(),
"yes".to_string(),
]
};
let modify = tokio::process::Command::new("nmcli")
.args(&dns_args)
.output()
.await
.context("Failed to modify DNS via nmcli")?;
if !modify.status.success() {
let stderr = String::from_utf8_lossy(&modify.stderr);
tracing::warn!(conn = conn_name, err = %stderr, "Failed to set DNS on connection");
continue;
}
// Reapply the connection to pick up changes
let reapply = tokio::process::Command::new("nmcli")
.args(["connection", "up", conn_name])
.output()
.await;
match reapply {
Ok(out) if out.status.success() => {
info!(conn = conn_name, "DNS updated successfully");
}
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
tracing::warn!(conn = conn_name, err = %stderr, "Failed to reapply connection");
}
Err(e) => {
tracing::warn!(conn = conn_name, err = %e, "Failed to reapply connection");
}
}
}
Ok(())
}
/// Configure DNS with a specific provider.
pub async fn configure(data_dir: &Path, provider: DnsProvider, custom_servers: Vec<String>) -> Result<DnsConfig> {
let (servers, doh_url) = if provider == DnsProvider::Custom {
(custom_servers, None)
} else {
let (preset_servers, preset_doh) = provider_servers(&provider);
(preset_servers, preset_doh)
};
let doh_enabled = doh_url.is_some();
let config = DnsConfig {
provider,
servers,
doh_enabled,
doh_url,
};
// Apply to system
apply_dns(&config).await?;
// Persist config
save_config(data_dir, &config).await?;
Ok(config)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_default_config() {
let config = DnsConfig::default();
assert_eq!(config.provider, DnsProvider::System);
assert!(config.servers.is_empty());
assert!(!config.doh_enabled);
}
#[test]
fn test_provider_servers() {
let (servers, doh) = provider_servers(&DnsProvider::Cloudflare);
assert_eq!(servers, vec!["1.1.1.1", "1.0.0.1"]);
assert!(doh.unwrap().contains("cloudflare"));
let (servers, doh) = provider_servers(&DnsProvider::System);
assert!(servers.is_empty());
assert!(doh.is_none());
let (servers, doh) = provider_servers(&DnsProvider::Custom);
assert!(servers.is_empty());
assert!(doh.is_none());
}
#[test]
fn test_provider_display() {
assert_eq!(DnsProvider::Cloudflare.to_string(), "cloudflare");
assert_eq!(DnsProvider::System.to_string(), "system");
assert_eq!(DnsProvider::Quad9.to_string(), "quad9");
}
#[tokio::test]
async fn test_config_persistence() {
let dir = tempdir().unwrap();
let config = DnsConfig {
provider: DnsProvider::Cloudflare,
servers: vec!["1.1.1.1".into(), "1.0.0.1".into()],
doh_enabled: true,
doh_url: Some("https://cloudflare-dns.com/dns-query".into()),
};
save_config(dir.path(), &config).await.unwrap();
let loaded = load_config(dir.path()).await.unwrap();
assert_eq!(loaded.provider, DnsProvider::Cloudflare);
assert_eq!(loaded.servers.len(), 2);
assert!(loaded.doh_enabled);
}
#[tokio::test]
async fn test_load_missing_config_returns_default() {
let dir = tempdir().unwrap();
let config = load_config(dir.path()).await.unwrap();
assert_eq!(config.provider, DnsProvider::System);
}
#[test]
fn test_config_serialization() {
let config = DnsConfig {
provider: DnsProvider::Google,
servers: vec!["8.8.8.8".into()],
doh_enabled: true,
doh_url: Some("https://dns.google/dns-query".into()),
};
let json = serde_json::to_string(&config).unwrap();
let parsed: DnsConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.provider, DnsProvider::Google);
assert_eq!(parsed.servers, vec!["8.8.8.8"]);
}
}