use anyhow::anyhow; use clap::Parser; use ipnet::Ipv4Net; use serde::{Deserialize, Serialize}; use std::net::Ipv4Addr; use std::path::{Path, PathBuf}; use vnt_core::context::config::Config; use vnt_core::nat::NetInput; use vnt_core::tls::verifier::CertValidationMode; use vnt_core::tunnel_core::server::transport::config::ProtocolAddress; use vnt_ipc as vnt_core; use vnt_ipc::port_mapping::PortMapping; #[derive(Debug, Deserialize, Serialize, Default)] pub struct FileConfig { pub server: Option>, pub network_code: Option, pub ip: Option, pub no_punch: Option, pub rtx: Option, pub compress: Option, pub fec: Option, pub input: Option>, pub output: Option>, pub no_nat: Option, pub no_tun: Option, pub mtu: Option, pub ctrl_port: Option, pub port_mapping: Option>, pub allow_mapping: Option, pub device_id: Option, pub device_name: Option, pub tun_name: Option, pub outbound_interface: Option, pub password: Option, pub cert_mode: Option, pub udp_stun: Option>, pub tcp_stun: Option>, pub tunnel_port: Option, } impl FileConfig { pub fn load(path: &Path) -> anyhow::Result { let content = std::fs::read_to_string(path)?; Ok(toml::from_str(&content)?) } pub fn save(&self, path: &Path) -> anyhow::Result<()> { let content = toml::to_string_pretty(self)?; std::fs::write(path, content)?; Ok(()) } pub fn to_server_addr(&self) -> anyhow::Result> { if let Some(server_raw) = &self.server { let mut server_addr = Vec::with_capacity(server_raw.len()); for x in server_raw { server_addr.push( x.parse::() .map_err(|e| anyhow!("invalid server address '{}': {}", x, e))?, ) } Ok(server_addr) } else { Ok(Vec::new()) } } pub fn to_port_mapping(&self) -> anyhow::Result> { if let Some(port_mapping_raw) = &self.port_mapping { let mut port_mapping = Vec::with_capacity(port_mapping_raw.len()); for x in port_mapping_raw { port_mapping.push( x.parse::() .map_err(|e| anyhow!("invalid port_mapping '{}': {}", x, e))?, ) } Ok(port_mapping) } else { Ok(Vec::new()) } } } #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] pub struct Args { /// 服务器地址 例如 `-s quic://127.0.0.1:29872`, 支持quic/tcp/wss/dynamic #[clap(short, long)] pub server: Vec, /// 网络编号,相同编号的会组同一个局域网 #[clap(short, long)] pub network_code: Option, #[clap(short = 'k', long, hide = true)] pub token: Option, /// 自定义虚拟IP #[clap(long)] pub ip: Option, /// 启用加密,设置加密密码 #[clap(short, long)] pub password: Option, /// 启用quic优化传输 #[clap(long)] pub rtx: bool, /// 启用压缩 (LZ4) #[clap(short = 'z', long)] pub compress: bool, /// 启用 FEC 前向纠错,损失一定带宽来提升网络稳定性 #[clap(long)] pub fec: bool, /// 入栈监听网段 #[clap(short, long)] pub input: Vec, /// 出栈允许网段 #[clap(short, long)] pub output: Vec, /// 自定义设备名称 #[clap(long, alias = "name")] pub device_name: Option, /// 设备id #[clap(long, alias = "id")] pub device_id: Option, /// 关闭打洞 #[clap(long)] pub no_punch: bool, /// 服务端证书验证 #[clap(long)] pub cert_mode: Option, /// 虚拟网卡名称 #[clap(long)] pub tun_name: Option, /// 绑定对外通信 Socket 的出口网卡名称 #[clap(long)] pub outbound_interface: Option, /// 关闭内置子网NAT #[clap(long)] pub no_nat: bool, /// 禁用tun,禁用后只能充当流量出口或者进行端口映射,无需管理员权限 #[clap(long)] pub no_tun: bool, /// 端口映射,格式为:协议://本地监听地址-目标虚拟IP-目标映射地址 #[clap(long)] pub port_mapping: Vec, /// 是否允许作为端口映射出口,开启后其他设备才可使用本设备的ip为"目标虚拟IP" #[clap(long)] pub allow_mapping: bool, /// 设置mtu #[clap(long)] pub mtu: Option, /// 控制端口,设置0时禁用控制服务 #[clap(long)] pub ctrl_port: Option, /// 隧道端口,用于P2P通信 #[clap(long)] pub tunnel_port: Option, /// 读取配置文件 #[arg(long)] pub conf: Option, /// 输出配置文件示例 #[clap(long)] pub conf_example: bool, } impl Args { pub fn parse_compatible() -> Self { let mut args = Args::parse(); if args.network_code.is_none() { args.network_code = args.token.clone(); } args } } pub struct CtrlConfig { pub ctrl_port: Option, } pub fn build_config_from_args_and_file( args: Option, file: Option, ) -> anyhow::Result<(Config, CtrlConfig)> { match (args, file) { (Some(args), Some(file)) => build_from_args_and_file(args, file), (Some(args), None) => build_from_args_only(args), (None, Some(file)) => build_from_file_only(file), (None, None) => Err(anyhow!("neither args nor config file provided")), } } fn build_from_args_and_file(args: Args, file: FileConfig) -> anyhow::Result<(Config, CtrlConfig)> { let server_addr = if args.server.is_empty() { file.to_server_addr()? } else { args.server }; let port_mapping = if args.port_mapping.is_empty() { file.to_port_mapping()? } else { args.port_mapping }; let network_code = args .network_code .or_else(|| file.network_code.clone()) .ok_or_else(|| anyhow!("network_code is required"))?; let cert_mode = args .cert_mode .or_else(|| file.cert_mode.as_deref().and_then(|s| s.parse().ok())) .unwrap_or(CertValidationMode::InsecureSkipVerification); let device_id = match args.device_id.or_else(|| file.device_id.clone()) { Some(id) => id, None => vnt_core::utils::device_id::get_device_id()?, }; let input = if args.input.is_empty() { file.input.unwrap_or_default() } else { args.input }; let output = if args.output.is_empty() { file.output.unwrap_or_default() } else { args.output }; let mut udp_stun = file.udp_stun.unwrap_or_default(); for x in udp_stun.iter_mut() { if !x.contains(':') { x.push_str(":3478"); } } let mut tcp_stun = file.tcp_stun.unwrap_or_default(); for x in tcp_stun.iter_mut() { if !x.contains(':') { x.push_str(":3478"); } } let config = Config { server_addr, network_code, ip: args.ip.or(file.ip), no_punch: args.no_punch || file.no_punch.unwrap_or(false), rtx: args.rtx || file.rtx.unwrap_or(false), compress: args.compress || file.compress.unwrap_or(false), fec: args.fec || file.fec.unwrap_or(false), device_id, device_name: args .device_name .or_else(|| file.device_name.clone()) .unwrap_or_else(default_hostname), tun_name: args.tun_name.or_else(|| file.tun_name.clone()), outbound_interface: args .outbound_interface .or_else(|| file.outbound_interface.clone()), password: args.password.or_else(|| file.password.clone()), cert_mode, input, output, no_nat: args.no_nat || file.no_nat.unwrap_or(false), no_tun: args.no_tun || file.no_tun.unwrap_or(false), mtu: args.mtu.or(file.mtu), port_mapping, allow_port_mapping: args.allow_mapping || file.allow_mapping.unwrap_or(false), udp_stun, tcp_stun, tunnel_port: args.tunnel_port.or(file.tunnel_port), }; let ctrl_config = CtrlConfig { ctrl_port: args.ctrl_port.or(file.ctrl_port), }; Ok((config, ctrl_config)) } fn build_from_args_only(args: Args) -> anyhow::Result<(Config, CtrlConfig)> { let device_id = match args.device_id { Some(id) => id, None => vnt_core::utils::device_id::get_device_id()?, }; let config = Config { server_addr: args.server, network_code: args .network_code .ok_or_else(|| anyhow!("network_code is required"))?, ip: args.ip, no_punch: args.no_punch, rtx: args.rtx, input: args.input, compress: args.compress, fec: args.fec, device_id, device_name: args.device_name.unwrap_or_else(default_hostname), tun_name: args.tun_name, outbound_interface: args.outbound_interface, password: args.password, cert_mode: args .cert_mode .unwrap_or(CertValidationMode::InsecureSkipVerification), output: args.output, no_nat: args.no_nat, no_tun: args.no_tun, mtu: args.mtu, port_mapping: args.port_mapping, allow_port_mapping: args.allow_mapping, tunnel_port: args.tunnel_port, ..Default::default() }; let ctrl_config = CtrlConfig { ctrl_port: args.ctrl_port, }; Ok((config, ctrl_config)) } fn build_from_file_only(file: FileConfig) -> anyhow::Result<(Config, CtrlConfig)> { let server_addr = file.to_server_addr()?; let port_mapping = file.to_port_mapping()?; let cert_mode = file .cert_mode .as_deref() .and_then(|s| s.parse().ok()) .unwrap_or(CertValidationMode::InsecureSkipVerification); let device_id = match file.device_id.clone() { Some(id) => id, None => vnt_core::utils::device_id::get_device_id()?, }; let mut udp_stun = file.udp_stun.unwrap_or_default(); for x in udp_stun.iter_mut() { if !x.contains(':') { x.push_str(":3478"); } } let mut tcp_stun = file.tcp_stun.unwrap_or_default(); for x in tcp_stun.iter_mut() { if !x.contains(':') { x.push_str(":3478"); } } let config = Config { server_addr, network_code: file .network_code .ok_or_else(|| anyhow!("network_code is required"))?, ip: file.ip, no_punch: file.no_punch.unwrap_or(false), rtx: file.rtx.unwrap_or(false), input: file.input.unwrap_or_default(), compress: file.compress.unwrap_or(false), fec: file.fec.unwrap_or(false), device_id, device_name: file.device_name.clone().unwrap_or_else(default_hostname), tun_name: file.tun_name.clone(), outbound_interface: file.outbound_interface.clone(), password: file.password.clone(), cert_mode, output: file.output.unwrap_or_default(), no_nat: file.no_nat.unwrap_or(false), no_tun: file.no_tun.unwrap_or(false), mtu: file.mtu, port_mapping, allow_port_mapping: file.allow_mapping.unwrap_or(false), udp_stun, tcp_stun, tunnel_port: file.tunnel_port, }; let ctrl_config = CtrlConfig { ctrl_port: file.ctrl_port, }; Ok((config, ctrl_config)) } fn default_hostname() -> String { hostname::get() .ok() .and_then(|v| v.into_string().ok()) .unwrap_or_default() } impl FileConfig { pub fn print_example(path: Option<&Path>) -> anyhow::Result<()> { const VERSION: &str = env!("CARGO_PKG_VERSION"); let example = format!( r#"# ================================== # VNT 配置文件示例(程序版本 v{version}) # ================================== # --- 网络配置 --- # 网络编号,相同网络编号的会组在同一个虚拟网 (必填) network_code = "your_network_code" # 服务器地址列表(支持 quic / tcp / wss / dynamic) (必填) # dynamic 协议使用dns txt解析记录值 server = ["quic://1.2.3.4:29872"] # ===简单使用以下参数可以不动=== # 自定义虚拟 IP (可选) # ip = "10.10.0.2" # 是否启用quic优化传输 (默认 false,设置为true时开启) # rtx = false # 是否启用 FEC 前向纠错,损失一定带宽来提升网络稳定性(默认 false,设置为true时开启) # fec = false # 是否关闭 P2P 打洞 (默认 false,设置为true时关闭) # no_punch = false # 是否启用 LZ4 压缩 (默认 false,设置为true时开启) # compress = false # 入栈监听网段 (逗号分隔的 CIDR 和目标 IP),用于点对网,将指定网段的流量发送到目标节点 # input = ["192.168.0.0/24,10.26.0.2", "192.168.1.0/24,10.26.0.3"] # 出栈允许网段,用于点对网,允许指定网段的转发 # output = ["0.0.0.0/0"] # 是否关闭内置子网NAT,关闭(设为true)后需要配置网卡转发,否则无法使用点对网。通常关闭内置子网NAT,使用系统的网卡转发,点对网性能会更好 # no_nat = false # 是否关闭TUN虚拟网卡,关闭(设为true)后只能充当流量出口或者进行端口映射,关闭后无需管理员权限 # no_tun = false # 端口映射,格式为:协议://本地监听地址-目标虚拟IP-目标映射地址 # 端口映射用于在本地监听指定端口,并将收到的网络流量经由指定虚拟节点转发到目标地址,从而实现跨网络或内网服务访问 # 例如 port_mapping = ["tcp://0.0.0.0:81-10.0.0.2-10.0.0.2:80"] # tcp://0.0.0.0:81-10.0.0.2-10.0.0.2:80 则表示将本地tcp的81端口的数据转发到10.0.0.2:80 # tcp://0.0.0.0:81-10.0.0.2-192.168.1.10:80 则表示将本地tcp的81端口的数据经过10.0.0.2转到192.168.1.10:80 # tcp://0.0.0.0:81-10.0.0.2-anyonehost:80 则表示将本地tcp的81端口的数据经过10.0.0.2转到anyonehost:80 # port_mapping = [] # 是否允许作为端口映射出口,开启(设置为true)后其他设备才可使用本设备的ip为"目标虚拟IP" # 开启后虚拟网络其他设备可以使用此设备当跳板访问其他网络 # allow_mapping = false # 控制服务的 tcp 端口 # ctrl_port = 11233 # 隧道端口,用于P2P通信 (默认为0,自动分配) # tunnel_port = 0 # MTU 设置 # mtu = 1400 # --- 设备配置 --- # 设备名称 (可选,默认读取本机 hostname) # device_name = "my-device" # 设备 ID (可选,不填自动生成,不同设备ID不能相同) # device_id = "device-id-xxxx" # 虚拟网卡名称 # tun_name = "vnt-tun" # 绑定对外通信 Socket 的出口网卡名称(用于服务端通信、P2P 打洞及转发流量) # outbound_interface = "Ethernet" # --- 安全配置 --- # 加密密码 (可选) # password = "123456" # 证书校验方式: # skip 跳过验证(默认) # standard 使用系统证书验证 # finger 使用证书指纹验证,服务端启动时日志会输出指纹, # 例如 finger:3bdd8675606837cdf95d5e13445606315762315a78555f9da652940a25feaec1 # cert_mode = "skip" # --- 其他配置 --- # 自定义stun地址,分别用于udp打洞和tcp打洞,需要单独配置,不设置则用默认stun # udp_stun = ["stun.chat.bilibili.com"] # tcp_stun = ["stun.nextcloud.com:443"] "#, version = VERSION ); println!("--- 示例配置文件内容 ---\n{}", example); if let Some(p) = path { std::fs::write(p, &example)?; println!("示例配置文件已写入 {}", p.display()); } Ok(()) } } #[cfg(test)] mod tests { use super::*; /// 纯参数模式下 --tunnel-port 不能被静默丢弃 #[test] fn test_args_only_keeps_tunnel_port() { let args = Args::try_parse_from([ "vnt", "-s", "quic://127.0.0.1:29872", "-n", "test-net", "--tunnel-port", "12345", "--outbound-interface", "Ethernet", ]) .unwrap(); let (config, _) = build_from_args_only(args).unwrap(); assert_eq!(config.tunnel_port, Some(12345)); assert_eq!(config.outbound_interface.as_deref(), Some("Ethernet")); } }