use clap::Parser;
use serde::{Deserialize, Serialize};
use std::net::{IpAddr, ToSocketAddrs};
use std::path::PathBuf;
use crate::error::{Error, Result};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Debug,
Info,
#[default]
Notice,
Warn,
Error,
}
impl std::fmt::Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LogLevel::Debug => write!(f, "DEBUG"),
LogLevel::Info => write!(f, "INFO"),
LogLevel::Notice => write!(f, "NOTICE"),
LogLevel::Warn => write!(f, "WARN"),
LogLevel::Error => write!(f, "ERROR"),
}
}
}
impl std::str::FromStr for LogLevel {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s.to_uppercase().as_str() {
"DEBUG" => Ok(LogLevel::Debug),
"INFO" => Ok(LogLevel::Info),
"NOTICE" => Ok(LogLevel::Notice),
"WARN" | "WARNING" => Ok(LogLevel::Warn),
"ERROR" | "ERR" => Ok(LogLevel::Error),
_ => Err(Error::Config(format!("invalid log level: {}", s))),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VanguardsConfig {
#[serde(default = "default_num_layer1_guards")]
pub num_layer1_guards: u8,
#[serde(default = "default_num_layer2_guards")]
pub num_layer2_guards: u8,
#[serde(default = "default_num_layer3_guards")]
pub num_layer3_guards: u8,
#[serde(default)]
pub layer1_lifetime_days: u16,
#[serde(default = "default_min_layer2_lifetime_hours")]
pub min_layer2_lifetime_hours: u32,
#[serde(default = "default_max_layer2_lifetime_hours")]
pub max_layer2_lifetime_hours: u32,
#[serde(default = "default_min_layer3_lifetime_hours")]
pub min_layer3_lifetime_hours: u32,
#[serde(default = "default_max_layer3_lifetime_hours")]
pub max_layer3_lifetime_hours: u32,
}
fn default_num_layer1_guards() -> u8 {
2
}
fn default_num_layer2_guards() -> u8 {
4
}
fn default_num_layer3_guards() -> u8 {
8
}
fn default_min_layer2_lifetime_hours() -> u32 {
24
}
fn default_max_layer2_lifetime_hours() -> u32 {
1080
}
fn default_min_layer3_lifetime_hours() -> u32 {
1
}
fn default_max_layer3_lifetime_hours() -> u32 {
48
}
impl Default for VanguardsConfig {
fn default() -> Self {
Self {
num_layer1_guards: default_num_layer1_guards(),
num_layer2_guards: default_num_layer2_guards(),
num_layer3_guards: default_num_layer3_guards(),
layer1_lifetime_days: 0,
min_layer2_lifetime_hours: default_min_layer2_lifetime_hours(),
max_layer2_lifetime_hours: default_max_layer2_lifetime_hours(),
min_layer3_lifetime_hours: default_min_layer3_lifetime_hours(),
max_layer3_lifetime_hours: default_max_layer3_lifetime_hours(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BandguardsConfig {
#[serde(default)]
pub circ_max_megabytes: u64,
#[serde(default = "default_circ_max_age_hours")]
pub circ_max_age_hours: u32,
#[serde(default = "default_circ_max_hsdesc_kilobytes")]
pub circ_max_hsdesc_kilobytes: u32,
#[serde(default)]
pub circ_max_serv_intro_kilobytes: u32,
#[serde(default = "default_circ_max_disconnected_secs")]
pub circ_max_disconnected_secs: u32,
#[serde(default = "default_conn_max_disconnected_secs")]
pub conn_max_disconnected_secs: u32,
}
fn default_circ_max_age_hours() -> u32 {
24
}
fn default_circ_max_hsdesc_kilobytes() -> u32 {
30
}
fn default_circ_max_disconnected_secs() -> u32 {
30
}
fn default_conn_max_disconnected_secs() -> u32 {
15
}
impl Default for BandguardsConfig {
fn default() -> Self {
Self {
circ_max_megabytes: 0,
circ_max_age_hours: default_circ_max_age_hours(),
circ_max_hsdesc_kilobytes: default_circ_max_hsdesc_kilobytes(),
circ_max_serv_intro_kilobytes: 0,
circ_max_disconnected_secs: default_circ_max_disconnected_secs(),
conn_max_disconnected_secs: default_conn_max_disconnected_secs(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RendguardConfig {
#[serde(default = "default_use_global_start_count")]
pub use_global_start_count: u32,
#[serde(default = "default_use_scale_at_count")]
pub use_scale_at_count: u32,
#[serde(default = "default_use_relay_start_count")]
pub use_relay_start_count: u32,
#[serde(default = "default_use_max_use_to_bw_ratio")]
pub use_max_use_to_bw_ratio: f64,
#[serde(default = "default_use_max_consensus_weight_churn")]
pub use_max_consensus_weight_churn: f64,
#[serde(default = "default_close_circuits_on_overuse")]
pub close_circuits_on_overuse: bool,
}
fn default_use_global_start_count() -> u32 {
1000
}
fn default_use_scale_at_count() -> u32 {
20000
}
fn default_use_relay_start_count() -> u32 {
100
}
fn default_use_max_use_to_bw_ratio() -> f64 {
5.0
}
fn default_use_max_consensus_weight_churn() -> f64 {
1.0
}
fn default_close_circuits_on_overuse() -> bool {
true
}
impl Default for RendguardConfig {
fn default() -> Self {
Self {
use_global_start_count: default_use_global_start_count(),
use_scale_at_count: default_use_scale_at_count(),
use_relay_start_count: default_use_relay_start_count(),
use_max_use_to_bw_ratio: default_use_max_use_to_bw_ratio(),
use_max_consensus_weight_churn: default_use_max_consensus_weight_churn(),
close_circuits_on_overuse: default_close_circuits_on_overuse(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct LogguardConfig {
#[serde(default = "default_protocol_warns")]
pub protocol_warns: bool,
#[serde(default = "default_dump_limit")]
pub dump_limit: usize,
#[serde(default)]
pub dump_level: LogLevel,
}
fn default_protocol_warns() -> bool {
true
}
fn default_dump_limit() -> usize {
25
}
impl Default for LogguardConfig {
fn default() -> Self {
Self {
protocol_warns: default_protocol_warns(),
dump_limit: default_dump_limit(),
dump_level: LogLevel::Notice,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Config {
#[serde(default = "default_control_ip")]
pub control_ip: String,
#[serde(default)]
pub control_port: Option<u16>,
#[serde(default)]
pub control_socket: Option<PathBuf>,
#[serde(default)]
pub control_pass: Option<String>,
#[serde(default = "default_state_file")]
pub state_file: PathBuf,
#[serde(default)]
pub loglevel: LogLevel,
#[serde(default)]
pub logfile: Option<String>,
#[serde(default)]
pub retry_limit: Option<u32>,
#[serde(default)]
pub one_shot_vanguards: bool,
#[serde(default = "default_close_circuits")]
pub close_circuits: bool,
#[serde(default = "default_enable_vanguards")]
pub enable_vanguards: bool,
#[serde(default = "default_enable_bandguards")]
pub enable_bandguards: bool,
#[serde(default = "default_enable_rendguard")]
pub enable_rendguard: bool,
#[serde(default = "default_enable_logguard")]
pub enable_logguard: bool,
#[serde(default)]
pub enable_cbtverify: bool,
#[serde(default)]
pub enable_pathverify: bool,
#[serde(default)]
pub vanguards: VanguardsConfig,
#[serde(default)]
pub bandguards: BandguardsConfig,
#[serde(default)]
pub rendguard: RendguardConfig,
#[serde(default)]
pub logguard: LogguardConfig,
}
fn default_control_ip() -> String {
"127.0.0.1".to_string()
}
fn default_state_file() -> PathBuf {
PathBuf::from("vanguards.state")
}
fn default_close_circuits() -> bool {
true
}
fn default_enable_vanguards() -> bool {
true
}
fn default_enable_bandguards() -> bool {
true
}
fn default_enable_rendguard() -> bool {
true
}
fn default_enable_logguard() -> bool {
true
}
impl Default for Config {
fn default() -> Self {
Self {
control_ip: default_control_ip(),
control_port: None,
control_socket: None,
control_pass: None,
state_file: default_state_file(),
loglevel: LogLevel::default(),
logfile: None,
retry_limit: None,
one_shot_vanguards: false,
close_circuits: default_close_circuits(),
enable_vanguards: default_enable_vanguards(),
enable_bandguards: default_enable_bandguards(),
enable_rendguard: default_enable_rendguard(),
enable_logguard: default_enable_logguard(),
enable_cbtverify: false,
enable_pathverify: false,
vanguards: VanguardsConfig::default(),
bandguards: BandguardsConfig::default(),
rendguard: RendguardConfig::default(),
logguard: LogguardConfig::default(),
}
}
}
impl Config {
pub fn from_file(path: &std::path::Path) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
toml::from_str(&content).map_err(|e| Error::Config(e.to_string()))
}
pub fn to_toml(&self) -> Result<String> {
toml::to_string_pretty(self).map_err(|e| Error::Config(e.to_string()))
}
pub fn validate(&self) -> Result<()> {
if self.vanguards.min_layer2_lifetime_hours > self.vanguards.max_layer2_lifetime_hours {
return Err(Error::Config(
"min_layer2_lifetime_hours must be <= max_layer2_lifetime_hours".to_string(),
));
}
if self.vanguards.min_layer3_lifetime_hours > self.vanguards.max_layer3_lifetime_hours {
return Err(Error::Config(
"min_layer3_lifetime_hours must be <= max_layer3_lifetime_hours".to_string(),
));
}
if self.rendguard.use_max_use_to_bw_ratio <= 0.0 {
return Err(Error::Config(
"use_max_use_to_bw_ratio must be positive".to_string(),
));
}
if self.rendguard.use_max_consensus_weight_churn < 0.0 {
return Err(Error::Config(
"use_max_consensus_weight_churn must be non-negative".to_string(),
));
}
Ok(())
}
pub fn resolve_control_ip(&mut self) -> Result<()> {
if self.control_ip.parse::<IpAddr>().is_err() {
let addr = format!("{}:0", self.control_ip)
.to_socket_addrs()
.map_err(|e| {
Error::Config(format!(
"failed to resolve hostname {}: {}",
self.control_ip, e
))
})?
.next()
.ok_or_else(|| {
Error::Config(format!(
"no addresses found for hostname {}",
self.control_ip
))
})?;
self.control_ip = addr.ip().to_string();
}
Ok(())
}
}
#[derive(Parser, Debug)]
#[command(name = "vanguards-rs")]
#[command(about = "Enhanced security for Tor hidden services")]
#[command(version)]
#[command(
long_about = "vanguards-rs provides enhanced security for Tor hidden services through \
persistent vanguard relay selection, bandwidth monitoring, rendezvous point protection, \
circuit build timeout verification, path verification, and log monitoring."
)]
pub struct CliArgs {
#[arg(long = "state", env = "VANGUARDS_STATE")]
pub state_file: Option<PathBuf>,
#[arg(long = "generate_config")]
pub generate_config: Option<PathBuf>,
#[arg(long)]
pub loglevel: Option<String>,
#[arg(long)]
pub logfile: Option<String>,
#[arg(
long = "config",
env = "VANGUARDS_CONFIG",
default_value = "vanguards.conf"
)]
pub config_file: PathBuf,
#[arg(long)]
pub control_ip: Option<String>,
#[arg(long)]
pub control_port: Option<u16>,
#[arg(long)]
pub control_socket: Option<PathBuf>,
#[arg(long)]
pub control_pass: Option<String>,
#[arg(long)]
pub retry_limit: Option<u32>,
#[arg(long)]
pub one_shot_vanguards: bool,
#[arg(long)]
pub disable_vanguards: bool,
#[arg(long)]
pub disable_bandguards: bool,
#[arg(long)]
pub disable_rendguard: bool,
#[arg(long)]
pub disable_logguard: bool,
#[arg(long)]
pub enable_cbtverify: bool,
#[arg(long)]
pub enable_pathverify: bool,
}
impl CliArgs {
pub fn apply_to(&self, config: &mut Config) {
if let Some(ref state_file) = self.state_file {
config.state_file = state_file.clone();
}
if let Some(ref loglevel) = self.loglevel {
if let Ok(level) = loglevel.parse() {
config.loglevel = level;
}
}
if let Some(ref logfile) = self.logfile {
config.logfile = Some(logfile.clone());
}
if let Some(ref control_ip) = self.control_ip {
config.control_ip = control_ip.clone();
}
if let Some(control_port) = self.control_port {
config.control_port = Some(control_port);
}
if let Some(ref control_socket) = self.control_socket {
config.control_socket = Some(control_socket.clone());
}
if let Some(ref control_pass) = self.control_pass {
config.control_pass = Some(control_pass.clone());
}
if let Some(retry_limit) = self.retry_limit {
config.retry_limit = Some(retry_limit);
}
if self.one_shot_vanguards {
config.one_shot_vanguards = true;
}
if self.disable_vanguards {
config.enable_vanguards = false;
}
if self.disable_bandguards {
config.enable_bandguards = false;
}
if self.disable_rendguard {
config.enable_rendguard = false;
}
if self.disable_logguard {
config.enable_logguard = false;
}
if self.enable_cbtverify {
config.enable_cbtverify = true;
}
if self.enable_pathverify {
config.enable_pathverify = true;
}
}
}
pub fn load_config(args: &CliArgs) -> Result<Config> {
let mut config = Config::default();
if args.config_file.exists() {
config = Config::from_file(&args.config_file)?;
}
args.apply_to(&mut config);
config.resolve_control_ip()?;
config.validate()?;
Ok(config)
}