use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::{Path, PathBuf};
use std::time::Duration;
use clap::Parser;
use serde::{Deserialize, Serialize};
use crate::crypto::Cipher;
use crate::policy::{Mode, PolicyConfig};
use crate::protocol::DEFAULT_TUN_MTU;
pub const DEFAULT_CIPHER: &str = "chacha20-poly1305";
pub const DEFAULT_NETMASK: Ipv4Addr = Ipv4Addr::new(255, 255, 255, 0);
pub const DEFAULT_DNS_LISTEN: &str = "127.0.0.1:53";
pub const DEFAULT_DNS_LOCAL: &str = "114.114.114.114:53";
pub const DEFAULT_DNS_REMOTE: &str = "8.8.8.8:53";
pub const DEFAULT_GEOIP_COUNTRY: &str = "CN";
pub const DEFAULT_CACHE_FILE_NAME: &str = "dns-cache.json";
pub const DEFAULT_DNS_TIMEOUT_MS: u64 = 3000;
pub const DEFAULT_LEASE_TTL_SECS: u64 = 120;
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("failed to read config file {path}: {source}")]
Read {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to parse config file {path}: {source}")]
Parse {
path: PathBuf,
#[source]
source: serde_json::Error,
},
#[error("missing required configuration field: {0}")]
Missing(&'static str),
#[error(transparent)]
Cipher(#[from] crate::crypto::CryptoError),
#[error(transparent)]
Policy(#[from] crate::policy::PolicyError),
#[error("invalid value for {field}: {message}")]
Invalid {
field: &'static str,
message: String,
},
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FileConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub server: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub password: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cipher: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tun_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tun_ip: Option<Ipv4Addr>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tun_netmask: Option<Ipv4Addr>,
#[serde(skip_serializing_if = "Option::is_none")]
pub peer_ip: Option<Ipv4Addr>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mtu: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub obfs: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nat: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lease_ttl_secs: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dns_listen: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dns_local: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dns_remote: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gfwlist: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub chnroute: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub geoip: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub geoip_country: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub set_dns: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prewarm: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dns_timeout_ms: Option<u64>,
}
impl FileConfig {
pub fn load(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
let path = path.as_ref();
let bytes = std::fs::read(path).map_err(|source| ConfigError::Read {
path: path.to_path_buf(),
source,
})?;
serde_json::from_slice(&bytes).map_err(|source| ConfigError::Parse {
path: path.to_path_buf(),
source,
})
}
}
#[derive(Debug, Clone)]
pub struct TunConfig {
pub name: Option<String>,
pub ip: Ipv4Addr,
pub netmask: Ipv4Addr,
pub peer_ip: Ipv4Addr,
pub mtu: u16,
}
#[derive(Debug, Clone)]
pub struct ServerConfig {
pub listen: String,
pub cipher: Cipher,
pub master_key: Vec<u8>,
pub tun: TunConfig,
pub obfs: Option<String>,
pub nat: bool,
pub lease_ttl: Duration,
}
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub server: String,
pub cipher: Cipher,
pub master_key: Vec<u8>,
pub tun: TunConfig,
pub policy: PolicyConfig,
pub obfs: Option<String>,
}
#[derive(Debug, Clone, Parser)]
#[command(
name = "shadowvpn-server",
about = "ShadowVPN server: terminates the encrypted UDP tunnel onto a TUN device."
)]
pub struct ServerArgs {
#[arg(short = 'c', long = "config")]
pub config: Option<PathBuf>,
#[arg(short = 'l', long = "listen")]
pub listen: Option<String>,
#[arg(short = 'k', long = "password")]
pub password: Option<String>,
#[arg(short = 'm', long = "cipher")]
pub cipher: Option<String>,
#[arg(long = "tun-name")]
pub tun_name: Option<String>,
#[arg(long = "tun-ip")]
pub tun_ip: Option<Ipv4Addr>,
#[arg(long = "tun-netmask")]
pub tun_netmask: Option<Ipv4Addr>,
#[arg(long = "peer-ip")]
pub peer_ip: Option<Ipv4Addr>,
#[arg(long = "mtu")]
pub mtu: Option<u16>,
#[arg(long = "nat")]
pub nat: bool,
#[arg(long = "lease-ttl-secs")]
pub lease_ttl_secs: Option<u64>,
}
#[derive(Debug, Clone, Parser)]
#[command(
name = "shadowvpn-client",
about = "ShadowVPN client: tunnels TUN traffic to the server over encrypted UDP."
)]
pub struct ClientArgs {
#[arg(short = 'c', long = "config")]
pub config: Option<PathBuf>,
#[arg(short = 's', long = "server")]
pub server: Option<String>,
#[arg(short = 'k', long = "password")]
pub password: Option<String>,
#[arg(short = 'm', long = "cipher")]
pub cipher: Option<String>,
#[arg(long = "tun-name")]
pub tun_name: Option<String>,
#[arg(long = "tun-ip")]
pub tun_ip: Option<Ipv4Addr>,
#[arg(long = "tun-netmask")]
pub tun_netmask: Option<Ipv4Addr>,
#[arg(long = "peer-ip")]
pub peer_ip: Option<Ipv4Addr>,
#[arg(long = "mtu")]
pub mtu: Option<u16>,
#[arg(long = "mode")]
pub mode: Option<String>,
#[arg(long = "dns-listen")]
pub dns_listen: Option<String>,
#[arg(long = "dns-local")]
pub dns_local: Option<String>,
#[arg(long = "dns-remote")]
pub dns_remote: Option<String>,
#[arg(long = "gfwlist")]
pub gfwlist: Option<PathBuf>,
#[arg(long = "chnroute")]
pub chnroute: Option<PathBuf>,
#[arg(long = "geoip")]
pub geoip: Option<PathBuf>,
#[arg(long = "geoip-country")]
pub geoip_country: Option<String>,
#[arg(long = "set-dns")]
pub set_dns: bool,
#[arg(long = "no-set-dns")]
pub no_set_dns: bool,
#[arg(long = "no-prewarm")]
pub no_prewarm: bool,
#[arg(long = "cache-file")]
pub cache_file: Option<String>,
#[arg(long = "no-cache-persist")]
pub no_cache_persist: bool,
}
fn load_file(config: &Option<PathBuf>) -> Result<FileConfig, ConfigError> {
match config {
Some(path) => FileConfig::load(path),
None => Ok(FileConfig::default()),
}
}
fn resolve_crypto(
cipher_name: Option<String>,
password: Option<String>,
) -> Result<(Cipher, Vec<u8>), ConfigError> {
let cipher_name = cipher_name.unwrap_or_else(|| DEFAULT_CIPHER.to_string());
let cipher = Cipher::from_name(&cipher_name)?;
let password = password.ok_or(ConfigError::Missing("password"))?;
let master_key = crate::crypto::evp_bytes_to_key(password.as_bytes(), cipher.key_len());
Ok((cipher, master_key))
}
fn default_cache_file() -> PathBuf {
std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(Path::to_path_buf))
.unwrap_or_else(|| PathBuf::from("."))
.join(DEFAULT_CACHE_FILE_NAME)
}
fn parse_dns_addr(
field: &'static str,
value: &str,
default_port: u16,
) -> Result<SocketAddr, ConfigError> {
if let Ok(addr) = value.parse::<SocketAddr>() {
return Ok(addr);
}
if let Ok(ip) = value.parse::<IpAddr>() {
return Ok(SocketAddr::new(ip, default_port));
}
Err(ConfigError::Invalid {
field,
message: format!("`{value}` is not an `ip` or `ip:port` address"),
})
}
fn resolve_policy(args: &ClientArgs, file: &FileConfig) -> Result<PolicyConfig, ConfigError> {
let mode = match args.mode.clone().or_else(|| file.mode.clone()) {
Some(name) => Mode::from_name(&name)?,
None => Mode::Full,
};
let pick = |a: &Option<String>, f: &Option<String>, default: &str| -> String {
a.clone()
.or_else(|| f.clone())
.unwrap_or_else(|| default.to_string())
};
let dns_listen = parse_dns_addr(
"dns_listen",
&pick(&args.dns_listen, &file.dns_listen, DEFAULT_DNS_LISTEN),
53,
)?;
let dns_local = parse_dns_addr(
"dns_local",
&pick(&args.dns_local, &file.dns_local, DEFAULT_DNS_LOCAL),
53,
)?;
let dns_remote = parse_dns_addr(
"dns_remote",
&pick(&args.dns_remote, &file.dns_remote, DEFAULT_DNS_REMOTE),
53,
)?;
let gfwlist = args.gfwlist.clone().or_else(|| file.gfwlist.clone());
let chnroute = args.chnroute.clone().or_else(|| file.chnroute.clone());
let geoip = args.geoip.clone().or_else(|| file.geoip.clone());
let set_dns = if args.no_set_dns {
false
} else if args.set_dns {
true
} else {
file.set_dns.unwrap_or(true)
};
let prewarm = if args.no_prewarm {
Vec::new()
} else {
file.prewarm.clone().unwrap_or_else(|| {
crate::policy::DEFAULT_PREWARM
.iter()
.map(|s| s.to_string())
.collect()
})
};
let cache_file = if args.no_cache_persist {
None
} else {
Some(
args.cache_file
.clone()
.or_else(|| file.cache_file.clone())
.map(PathBuf::from)
.unwrap_or_else(default_cache_file),
)
};
if matches!(mode, Mode::GfwList) && gfwlist.is_none() {
return Err(ConfigError::Missing("gfwlist (required by gfwlist mode)"));
}
if matches!(mode, Mode::ChinaDns) && chnroute.is_none() && geoip.is_none() {
return Err(ConfigError::Missing(
"chnroute or geoip (required by chinadns mode)",
));
}
Ok(PolicyConfig {
mode,
dns_listen,
dns_local,
dns_remote,
gfwlist,
chnroute,
geoip,
geoip_country: args
.geoip_country
.clone()
.or_else(|| file.geoip_country.clone())
.unwrap_or_else(|| DEFAULT_GEOIP_COUNTRY.to_string()),
set_dns,
prewarm,
cache_file,
dns_timeout: Duration::from_millis(file.dns_timeout_ms.unwrap_or(DEFAULT_DNS_TIMEOUT_MS)),
})
}
#[allow(clippy::too_many_arguments)]
fn resolve_tun(
name: Option<String>,
ip: Option<Ipv4Addr>,
netmask: Option<Ipv4Addr>,
peer_ip: Option<Ipv4Addr>,
mtu: Option<u16>,
) -> Result<TunConfig, ConfigError> {
Ok(TunConfig {
name,
ip: ip.ok_or(ConfigError::Missing("tun_ip"))?,
netmask: netmask.unwrap_or(DEFAULT_NETMASK),
peer_ip: peer_ip.ok_or(ConfigError::Missing("peer_ip"))?,
mtu: mtu.unwrap_or(DEFAULT_TUN_MTU),
})
}
impl ServerArgs {
pub fn resolve(self) -> Result<ServerConfig, ConfigError> {
let file = load_file(&self.config)?;
let listen = self
.listen
.or(file.server)
.ok_or(ConfigError::Missing("listen"))?;
let (cipher, master_key) =
resolve_crypto(self.cipher.or(file.cipher), self.password.or(file.password))?;
let tun = resolve_tun(
self.tun_name.or(file.tun_name),
self.tun_ip.or(file.tun_ip),
self.tun_netmask.or(file.tun_netmask),
self.peer_ip.or(file.peer_ip),
self.mtu.or(file.mtu),
)?;
let obfs = file.obfs.filter(|s| !s.is_empty() && s != "none");
let nat = self.nat || file.nat.unwrap_or(false);
let lease_ttl = Duration::from_secs(
self.lease_ttl_secs
.or(file.lease_ttl_secs)
.unwrap_or(DEFAULT_LEASE_TTL_SECS),
);
Ok(ServerConfig {
listen,
cipher,
master_key,
tun,
obfs,
nat,
lease_ttl,
})
}
}
impl ClientArgs {
pub fn resolve(self) -> Result<ClientConfig, ConfigError> {
let file = load_file(&self.config)?;
let policy = resolve_policy(&self, &file)?;
let server = self
.server
.or(file.server)
.ok_or(ConfigError::Missing("server"))?;
let (cipher, master_key) =
resolve_crypto(self.cipher.or(file.cipher), self.password.or(file.password))?;
let tun = resolve_tun(
self.tun_name.or(file.tun_name),
self.tun_ip.or(file.tun_ip),
self.tun_netmask.or(file.tun_netmask),
self.peer_ip.or(file.peer_ip),
self.mtu.or(file.mtu),
)?;
let obfs = file.obfs.filter(|s| !s.is_empty() && s != "none");
Ok(ClientConfig {
server,
cipher,
master_key,
tun,
policy,
obfs,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
impl ClientArgs {
fn empty() -> Self {
ClientArgs {
config: None,
server: None,
password: None,
cipher: None,
tun_name: None,
tun_ip: None,
tun_netmask: None,
peer_ip: None,
mtu: None,
mode: None,
dns_listen: None,
dns_local: None,
dns_remote: None,
gfwlist: None,
chnroute: None,
geoip: None,
geoip_country: None,
set_dns: false,
no_set_dns: false,
no_prewarm: false,
cache_file: None,
no_cache_persist: false,
}
}
}
#[test]
fn cli_overrides_file_and_resolves() {
let args = ServerArgs {
config: None,
listen: Some("0.0.0.0:9000".to_string()),
password: Some("test".to_string()),
cipher: Some("aes-128-gcm".to_string()),
tun_name: Some("utun9".to_string()),
tun_ip: Some(Ipv4Addr::new(10, 9, 0, 1)),
tun_netmask: None,
peer_ip: Some(Ipv4Addr::new(10, 9, 0, 2)),
mtu: None,
nat: false,
lease_ttl_secs: None,
};
let cfg = args.resolve().expect("resolve");
assert_eq!(cfg.listen, "0.0.0.0:9000");
assert_eq!(cfg.cipher, Cipher::Aes128Gcm);
assert_eq!(cfg.master_key.len(), 16);
assert_eq!(cfg.tun.netmask, DEFAULT_NETMASK);
assert_eq!(cfg.tun.mtu, DEFAULT_TUN_MTU);
assert_eq!(cfg.tun.name.as_deref(), Some("utun9"));
}
#[test]
fn missing_password_is_an_error() {
let args = ClientArgs {
config: None,
server: Some("host:1".to_string()),
password: None,
cipher: None,
tun_name: None,
tun_ip: Some(Ipv4Addr::new(10, 0, 0, 2)),
tun_netmask: None,
peer_ip: Some(Ipv4Addr::new(10, 0, 0, 1)),
mtu: None,
..ClientArgs::empty()
};
assert!(matches!(
args.resolve(),
Err(ConfigError::Missing("password"))
));
}
#[test]
fn policy_defaults_to_full_and_validates() {
let base = ClientArgs {
config: None,
server: Some("host:1".to_string()),
password: Some("pw".to_string()),
tun_ip: Some(Ipv4Addr::new(10, 0, 0, 2)),
peer_ip: Some(Ipv4Addr::new(10, 0, 0, 1)),
..ClientArgs::empty()
};
let cfg = base.clone().resolve().expect("resolve full");
assert_eq!(cfg.policy.mode, Mode::Full);
assert_eq!(cfg.policy.dns_listen.to_string(), "127.0.0.1:53");
assert!(cfg.policy.set_dns, "set_dns defaults to on");
let mut nd = base.clone();
nd.no_set_dns = true;
assert!(!nd.resolve().unwrap().policy.set_dns);
let mut sd = base.clone();
sd.set_dns = true;
assert!(sd.resolve().unwrap().policy.set_dns);
assert!(!cfg.policy.prewarm.is_empty());
let mut np = base.clone();
np.no_prewarm = true;
assert!(np.resolve().unwrap().policy.prewarm.is_empty());
assert!(cfg.policy.cache_file.is_some());
let mut nc = base.clone();
nc.no_cache_persist = true;
assert!(nc.resolve().unwrap().policy.cache_file.is_none());
let mut g = base.clone();
g.mode = Some("gfwlist".to_string());
assert!(matches!(g.resolve(), Err(ConfigError::Missing(_))));
let mut c = base.clone();
c.mode = Some("chinadns".to_string());
assert!(matches!(c.resolve(), Err(ConfigError::Missing(_))));
let mut cg = base.clone();
cg.mode = Some("chinadns".to_string());
cg.geoip = Some(PathBuf::from("/tmp/GeoLite2-Country.mmdb"));
let cfg = cg.resolve().expect("resolve chinadns+geoip");
assert_eq!(cfg.policy.mode, Mode::ChinaDns);
assert_eq!(cfg.policy.geoip_country, "CN");
let mut d = base.clone();
d.dns_local = Some("1.2.3.4".to_string());
assert_eq!(
d.resolve().unwrap().policy.dns_local.to_string(),
"1.2.3.4:53"
);
let mut m = base;
m.mode = Some("bogus".to_string());
assert!(matches!(m.resolve(), Err(ConfigError::Policy(_))));
}
#[test]
fn file_config_parses() {
let json = r#"{
"server": "1.2.3.4:8388",
"password": "pw",
"cipher": "aes-256-gcm",
"tun_ip": "10.1.0.2",
"peer_ip": "10.1.0.1"
}"#;
let fc: FileConfig = serde_json::from_str(json).expect("parse");
assert_eq!(fc.server.as_deref(), Some("1.2.3.4:8388"));
assert_eq!(fc.cipher.as_deref(), Some("aes-256-gcm"));
assert_eq!(fc.tun_ip, Some(Ipv4Addr::new(10, 1, 0, 2)));
}
#[test]
fn server_nat_flag_and_default_ttl() {
let args = ServerArgs {
config: None,
listen: Some("0.0.0.0:1".to_string()),
password: Some("pw".to_string()),
cipher: None,
tun_name: None,
tun_ip: Some(Ipv4Addr::new(10, 9, 0, 1)),
tun_netmask: None,
peer_ip: Some(Ipv4Addr::new(10, 9, 0, 2)),
mtu: None,
nat: true,
lease_ttl_secs: None,
};
let cfg = args.resolve().expect("resolve");
assert!(cfg.nat);
assert_eq!(cfg.lease_ttl, Duration::from_secs(DEFAULT_LEASE_TTL_SECS));
}
}