mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-10-15 17:16:14 -08:00
refactor(config): preserve explicit flags with FlagsPatch and derive CLI arguments from schema (#2585)
This PR refactors EasyTier's configuration flags system across protobuf definitions, TOML/API persistence, runtime reconciliation, and the CLI. It introduces a synthesized FlagsPatch protobuf message with optionize and pbjson support to track field presence reliably, replaces manual CLI flag boilerplate with dynamic schema-driven clap argument generation, and establishes Flags::resolve(patch) as the single source of truth for runtime defaults. Co-authored-by: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
018750a785
commit
250c2ad8d6
52 files changed
+1909
-1405
No files matched your search
@@ -30,12 +30,6 @@ pub fn parse_mapped_listener_urls(
|
||||
.parse_urls(mapped_listeners)
|
||||
}
|
||||
|
||||
pub fn parse_encryption_algorithm(value: &str) -> Result<EncryptionAlgorithm, String> {
|
||||
value
|
||||
.parse()
|
||||
.map_err(|_| format!("'{value}' is not a valid encryption algorithm"))
|
||||
}
|
||||
|
||||
pub fn load_toml_config_from_path(path: &PathBuf) -> Result<TomlConfigLoader, anyhow::Error> {
|
||||
let config = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("failed to read config file: {}", path.display()))?;
|
||||
|
||||
+260
-440
@@ -2,9 +2,9 @@ use crate::{
|
||||
ShellType,
|
||||
common::{
|
||||
config::{
|
||||
ConfigFileControl, ConfigLoader, ConsoleLoggerConfig, EncryptionAlgorithm,
|
||||
FileLoggerConfig, LoggingConfigLoader, NetworkIdentity, PeerConfig, PortForwardConfig,
|
||||
TomlConfigLoader, VpnPortalClientConfig, VpnPortalConfig, add_proxy_network_to_config,
|
||||
ConfigFileControl, ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig,
|
||||
LoggingConfigLoader, NetworkIdentity, PeerConfig, PortForwardConfig, TomlConfigLoader,
|
||||
VpnPortalClientConfig, VpnPortalConfig, add_proxy_network_to_config,
|
||||
load_config_from_file, load_toml_config_from_path,
|
||||
load_toml_config_from_str_with_source, parse_mapped_listener_urls,
|
||||
},
|
||||
@@ -12,17 +12,20 @@ use crate::{
|
||||
log,
|
||||
},
|
||||
instance::factory::native_cli_instance_manager,
|
||||
proto::common::{CompressionAlgoPb, SecureModeConfig},
|
||||
proto::common::{CompressionAlgoPb, Flags, FlagsPatch, SecureModeConfig},
|
||||
rpc_service::ApiRpcServer,
|
||||
utils::panic::setup_panic_handler,
|
||||
web_client,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use cidr::IpCidr;
|
||||
use clap::{CommandFactory, Parser};
|
||||
use easytier_core::config::normalize_secure_mode_config;
|
||||
use clap::{Arg, ArgMatches, CommandFactory, FromArgMatches, Parser};
|
||||
use easytier_core::config::{EncryptionAlgorithm, normalize_secure_mode_config};
|
||||
use guarden::defer;
|
||||
use prost_types::field_descriptor_proto::Type;
|
||||
use rust_i18n::t;
|
||||
use serde_json::{Value, json};
|
||||
use std::str::{FromStr, ParseBoolError};
|
||||
use std::{
|
||||
net::{IpAddr, SocketAddr},
|
||||
path::PathBuf,
|
||||
@@ -35,6 +38,7 @@ use tokio::io::AsyncReadExt;
|
||||
use crate::tunnel::IpScheme;
|
||||
#[cfg(feature = "jemalloc-prof")]
|
||||
use jemalloc_ctl::{Access as _, AsName as _, epoch, stats};
|
||||
use serde::Serialize;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
windows_service::define_windows_service!(ffi_service_main, win_service_main);
|
||||
@@ -149,6 +153,10 @@ struct Cli {
|
||||
|
||||
#[derive(Parser, Debug, Default, PartialEq, Eq)]
|
||||
struct NetworkOptions {
|
||||
/// Filled in from the flag arguments the schema implies.
|
||||
#[arg(skip)]
|
||||
flags: FlagsPatch,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_NETWORK_NAME",
|
||||
@@ -312,79 +320,6 @@ struct NetworkOptions {
|
||||
)]
|
||||
vpn_portal_client_groups: Vec<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_DEFAULT_PROTOCOL",
|
||||
help = t!("core_clap.default_protocol").to_string()
|
||||
)]
|
||||
default_protocol: Option<String>,
|
||||
|
||||
#[arg(
|
||||
short = 'u',
|
||||
long,
|
||||
env = "ET_DISABLE_ENCRYPTION",
|
||||
help = t!("core_clap.disable_encryption").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
disable_encryption: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_ENCRYPTION_ALGORITHM",
|
||||
help = t!("core_clap.encryption_algorithm").to_string(),
|
||||
value_parser = crate::common::config::parse_encryption_algorithm,
|
||||
)]
|
||||
encryption_algorithm: Option<EncryptionAlgorithm>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_MULTI_THREAD",
|
||||
help = t!("core_clap.multi_thread").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
multi_thread: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_MULTI_THREAD_COUNT",
|
||||
help = t!("core_clap.multi_thread_count").to_string(),
|
||||
)]
|
||||
multi_thread_count: Option<u32>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_DISABLE_IPV6",
|
||||
help = t!("core_clap.disable_ipv6").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
disable_ipv6: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_DEV_NAME",
|
||||
help = t!("core_clap.dev_name").to_string()
|
||||
)]
|
||||
dev_name: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_MTU",
|
||||
help = t!("core_clap.mtu").to_string()
|
||||
)]
|
||||
mtu: Option<u16>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_LATENCY_FIRST",
|
||||
help = t!("core_clap.latency_first").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
latency_first: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_EXIT_NODES",
|
||||
@@ -394,42 +329,6 @@ struct NetworkOptions {
|
||||
)]
|
||||
exit_nodes: Vec<IpAddr>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_ENABLE_EXIT_NODE",
|
||||
help = t!("core_clap.enable_exit_node").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
enable_exit_node: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_PROXY_FORWARD_BY_SYSTEM",
|
||||
help = t!("core_clap.proxy_forward_by_system").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
proxy_forward_by_system: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_NO_TUN",
|
||||
help = t!("core_clap.no_tun").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
no_tun: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_USE_SMOLTCP",
|
||||
help = t!("core_clap.use_smoltcp").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
use_smoltcp: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_MANUAL_ROUTES",
|
||||
@@ -439,108 +338,6 @@ struct NetworkOptions {
|
||||
)]
|
||||
manual_routes: Option<Vec<String>>,
|
||||
|
||||
// if not in relay_network_whitelist:
|
||||
// for foreign virtual network, will refuse the incoming connection
|
||||
// for local virtual network, will refuse to relay tun packets
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_RELAY_NETWORK_WHITELIST",
|
||||
value_delimiter = ',',
|
||||
help = t!("core_clap.relay_network_whitelist").to_string(),
|
||||
num_args = 0..
|
||||
)]
|
||||
relay_network_whitelist: Option<Vec<String>>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_P2P_ONLY",
|
||||
help = t!("core_clap.p2p_only").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
p2p_only: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_LAZY_P2P",
|
||||
help = t!("core_clap.lazy_p2p").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
lazy_p2p: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_DISABLE_P2P",
|
||||
help = t!("core_clap.disable_p2p").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
disable_p2p: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_DISABLE_UDP_HOLE_PUNCHING",
|
||||
help = t!("core_clap.disable_udp_hole_punching").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
disable_udp_hole_punching: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_DISABLE_TCP_HOLE_PUNCHING",
|
||||
help = t!("core_clap.disable_tcp_hole_punching").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
disable_tcp_hole_punching: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_DISABLE_SYM_HOLE_PUNCHING",
|
||||
help = t!("core_clap.disable_sym_hole_punching").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
disable_sym_hole_punching: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_DISABLE_UPNP",
|
||||
help = t!("core_clap.disable_upnp").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
disable_upnp: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_ENABLE_UDP_BROADCAST_RELAY",
|
||||
help = t!("core_clap.enable_udp_broadcast_relay").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
enable_udp_broadcast_relay: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_RELAY_ALL_PEER_RPC",
|
||||
help = t!("core_clap.relay_all_peer_rpc").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
relay_all_peer_rpc: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_NEED_P2P",
|
||||
help = t!("core_clap.need_p2p").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
need_p2p: Option<bool>,
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
#[arg(
|
||||
long,
|
||||
@@ -549,67 +346,6 @@ struct NetworkOptions {
|
||||
)]
|
||||
socks5: Option<u16>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_COMPRESSION",
|
||||
help = t!("core_clap.compression").to_string(),
|
||||
)]
|
||||
compression: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_BIND_DEVICE",
|
||||
help = t!("core_clap.bind_device").to_string()
|
||||
)]
|
||||
bind_device: Option<bool>,
|
||||
|
||||
// SO_MARK (fwmark) is a Linux-family kernel feature. Gate the flag out
|
||||
// entirely on other targets so users on Windows/macOS/BSD don't see a
|
||||
// `--socket-mark` they can't act on.
|
||||
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_SOCKET_MARK",
|
||||
help = t!("core_clap.socket_mark").to_string()
|
||||
)]
|
||||
socket_mark: Option<u32>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_ENABLE_KCP_PROXY",
|
||||
help = t!("core_clap.enable_kcp_proxy").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
enable_kcp_proxy: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_DISABLE_KCP_INPUT",
|
||||
help = t!("core_clap.disable_kcp_input").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
disable_kcp_input: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_ENABLE_QUIC_PROXY",
|
||||
help = t!("core_clap.enable_quic_proxy").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
enable_quic_proxy: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_DISABLE_QUIC_INPUT",
|
||||
help = t!("core_clap.disable_quic_input").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
disable_quic_input: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_PORT_FORWARD",
|
||||
@@ -619,40 +355,6 @@ struct NetworkOptions {
|
||||
)]
|
||||
port_forward: Vec<url::Url>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_ACCEPT_DNS",
|
||||
help = t!("core_clap.accept_dns").to_string(),
|
||||
)]
|
||||
accept_dns: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long = "tld-dns-zone",
|
||||
env = "ET_TLD_DNS_ZONE",
|
||||
help = t!("core_clap.tld_dns_zone").to_string())]
|
||||
tld_dns_zone: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_PRIVATE_MODE",
|
||||
help = t!("core_clap.private_mode").to_string(),
|
||||
)]
|
||||
private_mode: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_FOREIGN_RELAY_BPS_LIMIT",
|
||||
help = t!("core_clap.foreign_relay_bps_limit").to_string(),
|
||||
)]
|
||||
foreign_relay_bps_limit: Option<u64>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_INSTANCE_RECV_BPS_LIMIT",
|
||||
help = t!("core_clap.instance_recv_bps_limit").to_string(),
|
||||
)]
|
||||
instance_recv_bps_limit: Option<u64>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
value_delimiter = ',',
|
||||
@@ -669,42 +371,6 @@ struct NetworkOptions {
|
||||
)]
|
||||
udp_whitelist: Vec<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_DISABLE_RELAY_KCP",
|
||||
help = t!("core_clap.disable_relay_kcp").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
disable_relay_kcp: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_DISABLE_RELAY_QUIC",
|
||||
help = t!("core_clap.disable_relay_quic").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
disable_relay_quic: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_ENABLE_RELAY_FOREIGN_NETWORK_KCP",
|
||||
help = t!("core_clap.enable_relay_foreign_network_kcp").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
enable_relay_foreign_network_kcp: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_ENABLE_RELAY_FOREIGN_NETWORK_QUIC",
|
||||
help = t!("core_clap.enable_relay_foreign_network_quic").to_string(),
|
||||
num_args = 0..=1,
|
||||
default_missing_value = "true"
|
||||
)]
|
||||
enable_relay_foreign_network_quic: Option<bool>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_STUN_SERVERS",
|
||||
@@ -1237,96 +903,12 @@ impl NetworkOptions {
|
||||
cfg.set_secure_mode(Some(normalize_secure_mode_config(c)?));
|
||||
}
|
||||
|
||||
let mut f = cfg.get_flags();
|
||||
if let Some(default_protocol) = &self.default_protocol {
|
||||
f.default_protocol = default_protocol.clone()
|
||||
};
|
||||
if let Some(v) = self.disable_encryption {
|
||||
f.enable_encryption = !v;
|
||||
}
|
||||
if let Some(algorithm) = &self.encryption_algorithm {
|
||||
f.encryption_algorithm = algorithm.to_string();
|
||||
}
|
||||
if let Some(v) = self.disable_ipv6 {
|
||||
f.enable_ipv6 = !v;
|
||||
}
|
||||
f.latency_first = self.latency_first.unwrap_or(f.latency_first);
|
||||
if let Some(dev_name) = &self.dev_name {
|
||||
f.dev_name = dev_name.clone()
|
||||
}
|
||||
if let Some(mtu) = self.mtu {
|
||||
f.mtu = mtu as u32;
|
||||
}
|
||||
f.enable_exit_node = self.enable_exit_node.unwrap_or(f.enable_exit_node);
|
||||
f.proxy_forward_by_system = self
|
||||
.proxy_forward_by_system
|
||||
.unwrap_or(f.proxy_forward_by_system);
|
||||
f.no_tun = self.no_tun.unwrap_or(f.no_tun) || cfg!(not(feature = "tun"));
|
||||
f.use_smoltcp = self.use_smoltcp.unwrap_or(f.use_smoltcp);
|
||||
if let Some(wl) = self.relay_network_whitelist.as_ref() {
|
||||
f.relay_network_whitelist = wl.join(" ");
|
||||
}
|
||||
f.disable_p2p = self.disable_p2p.unwrap_or(f.disable_p2p);
|
||||
f.p2p_only = self.p2p_only.unwrap_or(f.p2p_only);
|
||||
f.lazy_p2p = self.lazy_p2p.unwrap_or(f.lazy_p2p);
|
||||
f.disable_tcp_hole_punching = self
|
||||
.disable_tcp_hole_punching
|
||||
.unwrap_or(f.disable_tcp_hole_punching);
|
||||
f.disable_udp_hole_punching = self
|
||||
.disable_udp_hole_punching
|
||||
.unwrap_or(f.disable_udp_hole_punching);
|
||||
f.relay_all_peer_rpc = self.relay_all_peer_rpc.unwrap_or(f.relay_all_peer_rpc);
|
||||
f.need_p2p = self.need_p2p.unwrap_or(f.need_p2p);
|
||||
f.multi_thread = self.multi_thread.unwrap_or(f.multi_thread);
|
||||
if let Some(compression) = &self.compression {
|
||||
f.data_compress_algo = match compression.as_str() {
|
||||
"none" => CompressionAlgoPb::None,
|
||||
"zstd" => CompressionAlgoPb::Zstd,
|
||||
_ => panic!(
|
||||
"unknown compression algorithm: {}, supported: none, zstd",
|
||||
compression
|
||||
),
|
||||
}
|
||||
.into();
|
||||
}
|
||||
f.bind_device = self.bind_device.unwrap_or(f.bind_device);
|
||||
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
||||
{
|
||||
f.socket_mark = self.socket_mark.or(f.socket_mark);
|
||||
}
|
||||
f.enable_kcp_proxy = self.enable_kcp_proxy.unwrap_or(f.enable_kcp_proxy);
|
||||
f.disable_kcp_input = self.disable_kcp_input.unwrap_or(f.disable_kcp_input);
|
||||
f.enable_quic_proxy = self.enable_quic_proxy.unwrap_or(f.enable_quic_proxy);
|
||||
f.disable_quic_input = self.disable_quic_input.unwrap_or(f.disable_quic_input);
|
||||
f.accept_dns = self.accept_dns.unwrap_or(f.accept_dns);
|
||||
f.private_mode = self.private_mode.unwrap_or(f.private_mode);
|
||||
f.foreign_relay_bps_limit = self
|
||||
.foreign_relay_bps_limit
|
||||
.unwrap_or(f.foreign_relay_bps_limit);
|
||||
f.instance_recv_bps_limit = self
|
||||
.instance_recv_bps_limit
|
||||
.unwrap_or(f.instance_recv_bps_limit);
|
||||
f.multi_thread_count = self.multi_thread_count.unwrap_or(f.multi_thread_count);
|
||||
f.disable_relay_kcp = self.disable_relay_kcp.unwrap_or(f.disable_relay_kcp);
|
||||
f.disable_relay_quic = self.disable_relay_quic.unwrap_or(f.disable_relay_quic);
|
||||
f.enable_relay_foreign_network_kcp = self
|
||||
.enable_relay_foreign_network_kcp
|
||||
.unwrap_or(f.enable_relay_foreign_network_kcp);
|
||||
f.enable_relay_foreign_network_quic = self
|
||||
.enable_relay_foreign_network_quic
|
||||
.unwrap_or(f.enable_relay_foreign_network_quic);
|
||||
f.disable_sym_hole_punching = self
|
||||
.disable_sym_hole_punching
|
||||
.unwrap_or(f.disable_sym_hole_punching);
|
||||
f.disable_upnp = self.disable_upnp.unwrap_or(f.disable_upnp);
|
||||
f.enable_udp_broadcast_relay = self
|
||||
.enable_udp_broadcast_relay
|
||||
.unwrap_or(f.enable_udp_broadcast_relay);
|
||||
// Configure tld_dns_zone: use provided value if set
|
||||
if let Some(tld_dns_zone) = &self.tld_dns_zone {
|
||||
f.tld_dns_zone = tld_dns_zone.clone();
|
||||
}
|
||||
cfg.set_flags(f);
|
||||
cfg.patch_flags(self.flags.clone());
|
||||
#[cfg(not(feature = "tun"))]
|
||||
cfg.patch_flags(FlagsPatch {
|
||||
no_tun: Some(true),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
if !self.exit_nodes.is_empty() {
|
||||
cfg.set_exit_nodes(self.exit_nodes.clone());
|
||||
@@ -1471,8 +1053,96 @@ fn win_service_event_loop(
|
||||
});
|
||||
}
|
||||
|
||||
fn flags_from(matches: &ArgMatches) -> Result<FlagsPatch, clap::Error> {
|
||||
let given = Flags::flags()
|
||||
.iter()
|
||||
.filter_map(|field| {
|
||||
let name = field.name();
|
||||
let value = if let Ok(Some(values)) = matches.try_get_many::<String>(name) {
|
||||
json!(values.map(|s| s.as_str()).collect::<Vec<_>>().join(" "))
|
||||
} else if let Ok(Some(value)) = matches.try_get_one::<Value>(name) {
|
||||
value.clone()
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
Some((name.to_owned(), value))
|
||||
})
|
||||
.collect::<serde_json::Map<_, _>>();
|
||||
serde_json::from_value(Value::Object(given)).map_err(|error| {
|
||||
clap::Error::raw(
|
||||
clap::error::ErrorKind::ValueValidation,
|
||||
format!("{error:#}"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn cli_command() -> clap::Command {
|
||||
fn arg(flag: &str, spelling: &str) -> Arg {
|
||||
let name = spelling.to_uppercase();
|
||||
Arg::new(flag.to_owned())
|
||||
.long(spelling.replace('_', "-"))
|
||||
.env(format!("ET_{name}"))
|
||||
.value_name(name)
|
||||
.help(t!(format!("core_clap.{spelling}")).to_string())
|
||||
}
|
||||
|
||||
fn parsed<T: FromStr + Serialize>(value: &str) -> Result<Value, T::Err> {
|
||||
value.parse::<T>().map(|value| json!(value))
|
||||
}
|
||||
|
||||
fn negated(value: &str) -> Result<Value, ParseBoolError> {
|
||||
value.parse::<bool>().map(|value| json!(!value))
|
||||
}
|
||||
|
||||
Cli::command().args(Flags::flags().iter().map(|field| {
|
||||
let name = field.name();
|
||||
match name {
|
||||
// The two the command line states in the negative, which can be given
|
||||
// bare like any other boolean flag.
|
||||
"enable_encryption" => arg(name, "disable_encryption")
|
||||
.short('u')
|
||||
.num_args(0..=1)
|
||||
.default_missing_value("true")
|
||||
.value_parser(negated),
|
||||
"enable_ipv6" => arg(name, "disable_ipv6")
|
||||
.num_args(0..=1)
|
||||
.default_missing_value("true")
|
||||
.value_parser(negated),
|
||||
// The command line names the algorithm instead of spelling the field.
|
||||
// The values are the schema's own, which the patch reads.
|
||||
"data_compress_algo" => {
|
||||
arg(name, "compression").value_parser(parsed::<CompressionAlgoPb>)
|
||||
}
|
||||
"encryption_algorithm" => arg(name, name).value_parser(parsed::<EncryptionAlgorithm>),
|
||||
"mtu" => arg(name, name).value_parser(parsed::<u16>),
|
||||
"relay_network_whitelist" => arg(name, name)
|
||||
.value_delimiter(',')
|
||||
.num_args(0..)
|
||||
.action(clap::ArgAction::Append),
|
||||
_ => {
|
||||
let arg = arg(name, name);
|
||||
match field.r#type() {
|
||||
// A boolean flag can be given bare, and bare means enabled.
|
||||
Type::Bool => arg
|
||||
.num_args(0..=1)
|
||||
.default_missing_value("true")
|
||||
.value_parser(parsed::<bool>),
|
||||
Type::Uint32 => arg.value_parser(parsed::<u32>),
|
||||
Type::Uint64 => arg.value_parser(parsed::<u64>),
|
||||
// An enum flag takes the name the schema spells, and the patch's
|
||||
// own serde rejects anything the enum does not declare.
|
||||
Type::String | Type::Enum => arg.value_parser(parsed::<String>),
|
||||
other => panic!("{name}: no command line for {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn parse_cli() -> Cli {
|
||||
let mut cli = Cli::parse();
|
||||
let matches = cli_command().get_matches();
|
||||
let mut cli = Cli::from_arg_matches(&matches).unwrap_or_else(|error| error.exit());
|
||||
cli.network_options.flags = flags_from(&matches).unwrap_or_else(|error| error.exit());
|
||||
// for --stun-servers="", we want vec![], but clap will give vec![""], hack for that
|
||||
if let Some(stun_servers) = &mut cli.network_options.stun_servers {
|
||||
stun_servers.retain(|s| !s.trim().is_empty());
|
||||
@@ -1778,7 +1448,7 @@ pub async fn main() -> ExitCode {
|
||||
let cli = parse_cli();
|
||||
|
||||
if let Some(shell) = cli.gen_autocomplete {
|
||||
let mut cmd = Cli::command();
|
||||
let mut cmd = cli_command();
|
||||
if let Some(shell) = shell.to_shell() {
|
||||
crate::print_completions(shell, &mut cmd, "easytier-core");
|
||||
} else {
|
||||
@@ -1841,6 +1511,12 @@ async fn validate_config(cli: &Cli) -> anyhow::Result<()> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::proto::common::CompressionAlgoPb;
|
||||
|
||||
/// Parses the flags the way the binary does.
|
||||
fn parse_flags(argv: &[&str]) -> Result<FlagsPatch, clap::Error> {
|
||||
flags_from(&cli_command().try_get_matches_from(argv)?)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_listeners() {
|
||||
@@ -1954,6 +1630,150 @@ enabled = true
|
||||
assert_eq!(cfg.get_hostname(), "override-host");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_flags_from_schema_preserves_presence() {
|
||||
assert_eq!(parse_flags(&["easytier"]).unwrap(), FlagsPatch::default());
|
||||
let patch = parse_flags(&[
|
||||
"easytier",
|
||||
"--disable-encryption",
|
||||
"false",
|
||||
"--latency-first",
|
||||
"false",
|
||||
"--mtu",
|
||||
"0",
|
||||
"--compression",
|
||||
"Zstd",
|
||||
"--socket-mark",
|
||||
"42",
|
||||
"--disable-relay-data",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(patch.enable_encryption, Some(true));
|
||||
assert_eq!(patch.latency_first, Some(false));
|
||||
assert_eq!(patch.mtu, Some(0));
|
||||
assert_eq!(
|
||||
patch.data_compress_algo,
|
||||
Some(CompressionAlgoPb::Zstd as i32)
|
||||
);
|
||||
assert_eq!(patch.socket_mark, Some(42));
|
||||
assert_eq!(patch.disable_relay_data, Some(true));
|
||||
assert_eq!(patch.enable_ipv6, None);
|
||||
|
||||
// A negated flag can be given bare, and bare means the value it states.
|
||||
let patch = parse_flags(&["easytier", "--disable-ipv6"]).unwrap();
|
||||
assert_eq!(patch.enable_ipv6, Some(false));
|
||||
|
||||
// Compression tests: case-insensitive, accepts None/none/zstd/Zstd/ZSTD, rejects invalid
|
||||
let patch = parse_flags(&["easytier", "--compression", "none"]).unwrap();
|
||||
assert_eq!(
|
||||
patch.data_compress_algo,
|
||||
Some(CompressionAlgoPb::None as i32)
|
||||
);
|
||||
let patch = parse_flags(&["easytier", "--compression", "zstd"]).unwrap();
|
||||
assert_eq!(
|
||||
patch.data_compress_algo,
|
||||
Some(CompressionAlgoPb::Zstd as i32)
|
||||
);
|
||||
let patch = parse_flags(&["easytier", "--compression", "ZSTD"]).unwrap();
|
||||
assert_eq!(
|
||||
patch.data_compress_algo,
|
||||
Some(CompressionAlgoPb::Zstd as i32)
|
||||
);
|
||||
assert!(parse_flags(&["easytier", "--compression", "invalid"]).is_err());
|
||||
assert!(parse_flags(&["easytier", "--compression", "lz4"]).is_err());
|
||||
|
||||
// Encryption algorithm tests: case-insensitive, aliases, rejects invalid
|
||||
let patch = parse_flags(&["easytier", "--encryption-algorithm", "aes-gcm"]).unwrap();
|
||||
assert_eq!(patch.encryption_algorithm, Some("aes-gcm".to_string()));
|
||||
let patch = parse_flags(&["easytier", "--encryption-algorithm", "AES-GCM"]).unwrap();
|
||||
assert_eq!(patch.encryption_algorithm, Some("aes-gcm".to_string()));
|
||||
let patch =
|
||||
parse_flags(&["easytier", "--encryption-algorithm", "openssl-aes-gcm"]).unwrap();
|
||||
assert_eq!(patch.encryption_algorithm, Some("aes-gcm".to_string()));
|
||||
let patch =
|
||||
parse_flags(&["easytier", "--encryption-algorithm", "chacha20-poly1305"]).unwrap();
|
||||
assert_eq!(patch.encryption_algorithm, Some("chacha20".to_string()));
|
||||
let patch = parse_flags(&["easytier", "--encryption-algorithm", "xor"]).unwrap();
|
||||
assert_eq!(patch.encryption_algorithm, Some("xor".to_string()));
|
||||
assert!(parse_flags(&["easytier", "--encryption-algorithm", "rot13"]).is_err());
|
||||
assert!(parse_flags(&["easytier", "--encryption-algorithm", "des"]).is_err());
|
||||
|
||||
// Deprecated flag is not accepted.
|
||||
assert!(parse_flags(&["easytier", "--quic-listen-port", "1234"]).is_err());
|
||||
|
||||
// MTU tests: u16 range validation
|
||||
let patch = parse_flags(&["easytier", "--mtu", "1400"]).unwrap();
|
||||
assert_eq!(patch.mtu, Some(1400));
|
||||
let patch = parse_flags(&["easytier", "--mtu", "65535"]).unwrap();
|
||||
assert_eq!(patch.mtu, Some(65535));
|
||||
assert!(parse_flags(&["easytier", "--mtu", "65536"]).is_err());
|
||||
assert!(parse_flags(&["easytier", "--mtu", "70000"]).is_err());
|
||||
|
||||
// Relay network whitelist tests: comma, multiple values, bare clearing
|
||||
let patch = parse_flags(&["easytier", "--relay-network-whitelist", "net1,net2"]).unwrap();
|
||||
assert_eq!(patch.relay_network_whitelist, Some("net1 net2".to_string()));
|
||||
let patch =
|
||||
parse_flags(&["easytier", "--relay-network-whitelist", "net1", "net2"]).unwrap();
|
||||
assert_eq!(patch.relay_network_whitelist, Some("net1 net2".to_string()));
|
||||
let patch = parse_flags(&[
|
||||
"easytier",
|
||||
"--relay-network-whitelist",
|
||||
"net1",
|
||||
"--relay-network-whitelist",
|
||||
"net2",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(patch.relay_network_whitelist, Some("net1 net2".to_string()));
|
||||
let patch = parse_flags(&["easytier", "--relay-network-whitelist"]).unwrap();
|
||||
assert_eq!(patch.relay_network_whitelist, Some("".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_options_patch_preserves_omitted_flags_and_applies_zero_values() {
|
||||
let cfg = TomlConfigLoader::new_from_str(
|
||||
r#"[flags]
|
||||
default_protocol = "udp"
|
||||
latency_first = true
|
||||
mtu = 1400
|
||||
socket_mark = 42
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
NetworkOptions::default().merge_into(&cfg).unwrap();
|
||||
assert_eq!(cfg.get_flags().socket_mark, Some(42));
|
||||
assert_eq!(cfg.get_flags_patch().enable_encryption, None);
|
||||
assert_eq!(cfg.get_flags_patch().multi_thread, None);
|
||||
|
||||
NetworkOptions {
|
||||
flags: FlagsPatch {
|
||||
latency_first: Some(false),
|
||||
mtu: Some(0),
|
||||
relay_network_whitelist: Some(String::new()),
|
||||
enable_encryption: Some(false),
|
||||
data_compress_algo: Some(CompressionAlgoPb::Zstd.into()),
|
||||
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
||||
socket_mark: Some(0),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
.merge_into(&cfg)
|
||||
.unwrap();
|
||||
|
||||
let flags = cfg.get_flags();
|
||||
assert_eq!(flags.default_protocol, "udp");
|
||||
assert!(!flags.latency_first);
|
||||
assert_eq!(flags.mtu, 0);
|
||||
assert_eq!(flags.relay_network_whitelist, "");
|
||||
assert!(!flags.enable_encryption);
|
||||
assert_eq!(flags.data_compress_algo, CompressionAlgoPb::Zstd as i32);
|
||||
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
||||
assert_eq!(flags.socket_mark, Some(0));
|
||||
#[cfg(not(feature = "tun"))]
|
||||
assert!(flags.no_tun);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secure_mode_cli_flag_preserves_config_file_keypair() {
|
||||
use base64::{Engine as _, prelude::BASE64_STANDARD};
|
||||
|
||||
Reference in new issue
Block a user