use serde::Deserialize;
use crate::network::Ipv4Net;
const DEFAULT_DNS: &[&str] = &["1.1.1.1", "2606:4700:4700::1111"];
#[derive(Debug, Clone, Deserialize, Default)]
pub struct PelagosConfig {
#[serde(default)]
pub network: NetworkConfig,
}
impl PelagosConfig {
pub fn load() -> Self {
Self::load_from(&crate::paths::config_file())
}
pub fn load_from(path: &std::path::Path) -> Self {
let data = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(_) => return Self::default(),
};
match toml::from_str::<Self>(&data) {
Ok(cfg) => cfg,
Err(e) => {
log::warn!(
"config: failed to parse {}: {} — using defaults",
path.display(),
e
);
Self::default()
}
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct NetworkConfig {
#[serde(default = "NetworkConfig::default_subnet_str")]
pub default_subnet: String,
#[serde(default = "NetworkConfig::default_alloc_pool_str")]
pub auto_alloc_pool: String,
#[serde(default = "NetworkConfig::default_dns_list")]
pub default_dns: Vec<String>,
}
impl Default for NetworkConfig {
fn default() -> Self {
Self {
default_subnet: Self::default_subnet_str(),
auto_alloc_pool: Self::default_alloc_pool_str(),
default_dns: Self::default_dns_list(),
}
}
}
impl NetworkConfig {
fn default_subnet_str() -> String {
"172.19.0.0/24".to_string()
}
fn default_alloc_pool_str() -> String {
"10.99.0.0/16".to_string()
}
fn default_dns_list() -> Vec<String> {
DEFAULT_DNS.iter().map(|s| s.to_string()).collect()
}
pub fn effective_default_dns(&self) -> Vec<String> {
self.effective_default_dns_with_env(std::env::var("PELAGOS_DEFAULT_DNS").ok().as_deref())
}
fn effective_default_dns_with_env(&self, env_val: Option<&str>) -> Vec<String> {
match env_val {
Some("") => vec![],
Some(val) => val
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect(),
None => self.default_dns.clone(),
}
}
pub fn default_subnet_parsed(&self) -> Ipv4Net {
Ipv4Net::from_cidr(&self.default_subnet).unwrap_or_else(|e| {
log::warn!(
"config: invalid default_subnet '{}': {} — using 172.19.0.0/24",
self.default_subnet,
e
);
Ipv4Net::from_cidr("172.19.0.0/24").unwrap()
})
}
pub fn auto_alloc_pool_parsed(&self) -> Ipv4Net {
Ipv4Net::from_cidr(&self.auto_alloc_pool).unwrap_or_else(|e| {
log::warn!(
"config: invalid auto_alloc_pool '{}': {} — using 10.99.0.0/16",
self.auto_alloc_pool,
e
);
Ipv4Net::from_cidr("10.99.0.0/16").unwrap()
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config_values() {
let cfg = PelagosConfig::default();
assert_eq!(cfg.network.default_subnet, "172.19.0.0/24");
assert_eq!(cfg.network.auto_alloc_pool, "10.99.0.0/16");
assert_eq!(
cfg.network.default_dns,
vec!["1.1.1.1", "2606:4700:4700::1111"]
);
}
#[test]
fn test_parse_full_config() {
let toml = r#"
[network]
default_subnet = "10.88.0.0/24"
auto_alloc_pool = "10.200.0.0/16"
"#;
let cfg: PelagosConfig = toml::from_str(toml).unwrap();
assert_eq!(cfg.network.default_subnet, "10.88.0.0/24");
assert_eq!(cfg.network.auto_alloc_pool, "10.200.0.0/16");
}
#[test]
fn test_parse_partial_config_uses_defaults() {
let toml = "[network]\nauto_alloc_pool = \"10.200.0.0/16\"\n";
let cfg: PelagosConfig = toml::from_str(toml).unwrap();
assert_eq!(cfg.network.default_subnet, "172.19.0.0/24");
assert_eq!(cfg.network.auto_alloc_pool, "10.200.0.0/16");
}
#[test]
fn test_parse_empty_config_uses_defaults() {
let cfg: PelagosConfig = toml::from_str("").unwrap();
assert_eq!(cfg.network.default_subnet, "172.19.0.0/24");
assert_eq!(cfg.network.auto_alloc_pool, "10.99.0.0/16");
}
#[test]
fn test_default_subnet_parsed() {
let cfg = NetworkConfig::default();
let net = cfg.default_subnet_parsed();
assert_eq!(net.addr.to_string(), "172.19.0.0");
assert_eq!(net.prefix_len, 24);
}
#[test]
fn test_auto_alloc_pool_parsed() {
let cfg = NetworkConfig::default();
let pool = cfg.auto_alloc_pool_parsed();
assert_eq!(pool.addr.to_string(), "10.99.0.0");
assert_eq!(pool.prefix_len, 16);
}
#[test]
fn test_invalid_subnet_falls_back_to_default() {
let cfg = NetworkConfig {
default_subnet: "not-a-cidr".to_string(),
auto_alloc_pool: "also-bad".to_string(),
default_dns: vec![],
};
let net = cfg.default_subnet_parsed();
assert_eq!(net.addr.to_string(), "172.19.0.0");
let pool = cfg.auto_alloc_pool_parsed();
assert_eq!(pool.addr.to_string(), "10.99.0.0");
}
#[test]
fn test_default_dns_in_config_file() {
let toml = r#"
[network]
default_dns = ["9.9.9.9", "2620:fe::fe"]
"#;
let cfg: PelagosConfig = toml::from_str(toml).unwrap();
assert_eq!(cfg.network.default_dns, vec!["9.9.9.9", "2620:fe::fe"]);
}
#[test]
fn test_empty_default_dns_disables_injection() {
let toml = "[network]\ndefault_dns = []\n";
let cfg: PelagosConfig = toml::from_str(toml).unwrap();
assert!(cfg.network.effective_default_dns().is_empty());
}
#[test]
fn test_effective_default_dns_env_override() {
let cfg = NetworkConfig::default();
let dns = cfg.effective_default_dns_with_env(Some("8.8.8.8,8.8.4.4"));
assert_eq!(dns, vec!["8.8.8.8", "8.8.4.4"]);
}
#[test]
fn test_effective_default_dns_env_empty_opt_out() {
let cfg = NetworkConfig::default();
let dns = cfg.effective_default_dns_with_env(Some(""));
assert!(dns.is_empty());
}
#[test]
fn test_effective_default_dns_no_env_uses_config() {
let cfg = NetworkConfig::default();
let dns = cfg.effective_default_dns_with_env(None);
assert_eq!(dns, vec!["1.1.1.1", "2606:4700:4700::1111"]);
}
#[test]
fn test_load_missing_file_returns_defaults() {
let tmp = tempfile::tempdir().unwrap();
let absent = tmp.path().join("pelagos/config.toml");
let cfg = PelagosConfig::load_from(&absent);
assert_eq!(cfg.network.default_subnet, "172.19.0.0/24");
assert_eq!(cfg.network.auto_alloc_pool, "10.99.0.0/16");
}
#[test]
fn test_load_from_xdg_config_home() {
let tmp = tempfile::tempdir().unwrap();
let cfg_dir = tmp.path().join("pelagos");
std::fs::create_dir_all(&cfg_dir).unwrap();
let cfg_path = cfg_dir.join("config.toml");
std::fs::write(
&cfg_path,
"[network]\ndefault_subnet = \"10.77.0.0/24\"\nauto_alloc_pool = \"10.77.0.0/16\"\n",
)
.unwrap();
let cfg = PelagosConfig::load_from(&cfg_path);
assert_eq!(cfg.network.default_subnet, "10.77.0.0/24");
assert_eq!(cfg.network.auto_alloc_pool, "10.77.0.0/16");
}
}