use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
use crate::metrics::MetricsConfig;
use crate::multipath::{MultipathConfig, UplinkConfig};
use crate::transport::TransportConfig;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Config {
#[serde(default)]
pub server: ServerConfig,
#[serde(default)]
pub client: ClientConfig,
#[serde(default)]
pub transport: TransportConfig,
#[serde(default)]
pub multipath: MultipathConfig,
#[serde(default)]
pub metrics: MetricsConfig,
#[serde(default)]
pub logging: LoggingConfig,
}
impl Config {
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let content = std::fs::read_to_string(path.as_ref())
.map_err(|e| Error::Config(format!("Failed to read config: {e}")))?;
let config: Self = toml::from_str(&content)
.map_err(|e| Error::Config(format!("Failed to parse config: {e}")))?;
config.validate()?;
Ok(config)
}
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let content = toml::to_string_pretty(self)
.map_err(|e| Error::Config(format!("Failed to serialize config: {e}")))?;
std::fs::write(path.as_ref(), content)
.map_err(|e| Error::Config(format!("Failed to write config: {e}")))?;
Ok(())
}
pub fn validate(&self) -> Result<()> {
if self.server.enabled && self.server.listen_addrs.is_empty() {
return Err(Error::InvalidConfig(
"Server enabled but no listen addresses".into(),
));
}
if self.client.enabled && self.client.uplinks.is_empty() {
return Err(Error::InvalidConfig(
"Client enabled but no uplinks configured".into(),
));
}
Ok(())
}
pub fn default_path() -> PathBuf {
directories::ProjectDirs::from("com", "triglav", "triglav").map_or_else(
|| PathBuf::from("triglav.toml"),
|dirs| dirs.config_dir().join("config.toml"),
)
}
pub fn example() -> Self {
Self {
server: ServerConfig {
enabled: true,
listen_addrs: vec![
"0.0.0.0:7443".parse().unwrap(),
"[::]:7443".parse().unwrap(),
],
..Default::default()
},
client: ClientConfig {
enabled: true,
uplinks: vec![
UplinkConfig {
id: "primary".into(),
interface: Some("en0".into()),
remote_addr: "server.example.com:7443".parse().unwrap(),
..Default::default()
},
UplinkConfig {
id: "backup".into(),
interface: Some("en1".into()),
remote_addr: "server.example.com:7443".parse().unwrap(),
weight: 50,
..Default::default()
},
],
..Default::default()
},
..Default::default()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub listen_addrs: Vec<SocketAddr>,
#[serde(default = "default_max_connections")]
pub max_connections: usize,
#[serde(default = "default_idle_timeout", with = "humantime_serde")]
pub idle_timeout: std::time::Duration,
pub key_file: Option<PathBuf>,
#[serde(default = "default_tcp_fallback")]
pub tcp_fallback: bool,
#[serde(default)]
pub rate_limit: u32,
}
fn default_max_connections() -> usize {
10000
}
fn default_idle_timeout() -> std::time::Duration {
std::time::Duration::from_secs(300)
}
fn default_tcp_fallback() -> bool {
true
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
enabled: false,
listen_addrs: vec![],
max_connections: default_max_connections(),
idle_timeout: default_idle_timeout(),
key_file: None,
tcp_fallback: default_tcp_fallback(),
rate_limit: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientConfig {
#[serde(default)]
pub enabled: bool,
pub auth_key: Option<String>,
#[serde(default)]
pub uplinks: Vec<UplinkConfig>,
#[serde(default)]
pub auto_discover: bool,
pub socks_port: Option<u16>,
pub http_proxy_port: Option<u16>,
pub dns_server: Option<SocketAddr>,
#[serde(default = "default_reconnect_delay", with = "humantime_serde")]
pub reconnect_delay: std::time::Duration,
#[serde(default = "default_max_reconnects")]
pub max_reconnects: u32,
}
fn default_reconnect_delay() -> std::time::Duration {
std::time::Duration::from_secs(5)
}
fn default_max_reconnects() -> u32 {
10
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
enabled: false,
auth_key: None,
uplinks: vec![],
auto_discover: false,
socks_port: None,
http_proxy_port: None,
dns_server: None,
reconnect_delay: default_reconnect_delay(),
max_reconnects: default_max_reconnects(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
#[serde(default = "default_log_level")]
pub level: String,
#[serde(default = "default_log_format")]
pub format: String,
pub file: Option<PathBuf>,
#[serde(default = "default_color")]
pub color: bool,
}
fn default_log_level() -> String {
"info".into()
}
fn default_log_format() -> String {
"text".into()
}
fn default_color() -> bool {
true
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: default_log_level(),
format: default_log_format(),
file: None,
color: default_color(),
}
}
}
pub fn init_logging(config: &LoggingConfig) -> Result<()> {
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
let filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.level));
let subscriber = tracing_subscriber::registry().with(filter);
if config.format == "json" {
subscriber
.with(fmt::layer().json())
.try_init()
.map_err(|e| Error::Config(format!("Failed to init logging: {e}")))?;
} else {
subscriber
.with(fmt::layer().with_ansi(config.color))
.try_init()
.map_err(|e| Error::Config(format!("Failed to init logging: {e}")))?;
}
Ok(())
}