config from environment variables; CLI args override config file (#755)

* feat: configure through os environment variables
* feat: support CLI args overriding config file options
This commit is contained in:
L-Trump
2025-04-10 18:14:10 +08:00
committed by GitHub
parent 75f7865769
commit 44d93648ee
2 changed files with 226 additions and 124 deletions

View File

@@ -129,6 +129,7 @@ clap = { version = "4.5.30", features = [
"unicode", "unicode",
"derive", "derive",
"wrap_help", "wrap_help",
"env",
] } ] }
async-recursion = "1.0.5" async-recursion = "1.0.5"

View File

@@ -96,6 +96,7 @@ struct Cli {
#[arg( #[arg(
short = 'w', short = 'w',
long, long,
env = "ET_CONFIG_SERVER",
help = t!("core_clap.config_server").to_string() help = t!("core_clap.config_server").to_string()
)] )]
config_server: Option<String>, config_server: Option<String>,
@@ -103,27 +104,29 @@ struct Cli {
#[arg( #[arg(
short, short,
long, long,
env = "ET_CONFIG_FILE",
help = t!("core_clap.config_file").to_string() help = t!("core_clap.config_file").to_string()
)] )]
config_file: Option<PathBuf>, config_file: Option<PathBuf>,
#[arg( #[arg(
long, long,
env = "ET_NETWORK_NAME",
help = t!("core_clap.network_name").to_string(), help = t!("core_clap.network_name").to_string(),
default_value = "default"
)] )]
network_name: String, network_name: Option<String>,
#[arg( #[arg(
long, long,
env = "ET_NETWORK_SECRET",
help = t!("core_clap.network_secret").to_string(), help = t!("core_clap.network_secret").to_string(),
default_value = ""
)] )]
network_secret: String, network_secret: Option<String>,
#[arg( #[arg(
short, short,
long, long,
env = "ET_IPV4",
help = t!("core_clap.ipv4").to_string() help = t!("core_clap.ipv4").to_string()
)] )]
ipv4: Option<String>, ipv4: Option<String>,
@@ -131,13 +134,18 @@ struct Cli {
#[arg( #[arg(
short, short,
long, long,
help = t!("core_clap.dhcp").to_string() env = "ET_DHCP",
help = t!("core_clap.dhcp").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)] )]
dhcp: bool, dhcp: Option<bool>,
#[arg( #[arg(
short, short,
long, long,
env = "ET_PEERS",
value_delimiter = ',',
help = t!("core_clap.peers").to_string(), help = t!("core_clap.peers").to_string(),
num_args = 0.. num_args = 0..
)] )]
@@ -146,6 +154,7 @@ struct Cli {
#[arg( #[arg(
short, short,
long, long,
env = "ET_EXTERNAL_NODE",
help = t!("core_clap.external_node").to_string() help = t!("core_clap.external_node").to_string()
)] )]
external_node: Option<String>, external_node: Option<String>,
@@ -153,6 +162,8 @@ struct Cli {
#[arg( #[arg(
short = 'n', short = 'n',
long, long,
env = "ET_PROXY_NETWORKS",
value_delimiter = ',',
help = t!("core_clap.proxy_networks").to_string() help = t!("core_clap.proxy_networks").to_string()
)] )]
proxy_networks: Vec<String>, proxy_networks: Vec<String>,
@@ -160,14 +171,16 @@ struct Cli {
#[arg( #[arg(
short, short,
long, long,
env = "ET_RPC_PORTAL",
help = t!("core_clap.rpc_portal").to_string(), help = t!("core_clap.rpc_portal").to_string(),
default_value = "0"
)] )]
rpc_portal: String, rpc_portal: Option<String>,
#[arg( #[arg(
short, short,
long, long,
env = "ET_LISTENERS",
value_delimiter = ',',
help = t!("core_clap.listeners").to_string(), help = t!("core_clap.listeners").to_string(),
default_values_t = ["11010".to_string()], default_values_t = ["11010".to_string()],
num_args = 0.. num_args = 0..
@@ -176,6 +189,8 @@ struct Cli {
#[arg( #[arg(
long, long,
env = "ET_MAPPED_LISTENERS",
value_delimiter = ',',
help = t!("core_clap.mapped_listeners").to_string(), help = t!("core_clap.mapped_listeners").to_string(),
num_args = 0.. num_args = 0..
)] )]
@@ -183,31 +198,36 @@ struct Cli {
#[arg( #[arg(
long, long,
env = "ET_NO_LISTENER",
help = t!("core_clap.no_listener").to_string(), help = t!("core_clap.no_listener").to_string(),
default_value = "false" default_value = "false",
)] )]
no_listener: bool, no_listener: bool,
#[arg( #[arg(
long, long,
env = "ET_CONSOLE_LOG_LEVEL",
help = t!("core_clap.console_log_level").to_string() help = t!("core_clap.console_log_level").to_string()
)] )]
console_log_level: Option<String>, console_log_level: Option<String>,
#[arg( #[arg(
long, long,
env = "ET_FILE_LOG_LEVEL",
help = t!("core_clap.file_log_level").to_string() help = t!("core_clap.file_log_level").to_string()
)] )]
file_log_level: Option<String>, file_log_level: Option<String>,
#[arg( #[arg(
long, long,
env = "ET_FILE_LOG_DIR",
help = t!("core_clap.file_log_dir").to_string() help = t!("core_clap.file_log_dir").to_string()
)] )]
file_log_dir: Option<String>, file_log_dir: Option<String>,
#[arg( #[arg(
long, long,
env = "ET_HOSTNAME",
help = t!("core_clap.hostname").to_string() help = t!("core_clap.hostname").to_string()
)] )]
hostname: Option<String>, hostname: Option<String>,
@@ -215,19 +235,21 @@ struct Cli {
#[arg( #[arg(
short = 'm', short = 'm',
long, long,
env = "ET_INSTANCE_NAME",
help = t!("core_clap.instance_name").to_string(), help = t!("core_clap.instance_name").to_string(),
default_value = "default"
)] )]
instance_name: String, instance_name: Option<String>,
#[arg( #[arg(
long, long,
env = "ET_VPN_PORTAL",
help = t!("core_clap.vpn_portal").to_string() help = t!("core_clap.vpn_portal").to_string()
)] )]
vpn_portal: Option<String>, vpn_portal: Option<String>,
#[arg( #[arg(
long, long,
env = "ET_DEFAULT_PROTOCOL",
help = t!("core_clap.default_protocol").to_string() help = t!("core_clap.default_protocol").to_string()
)] )]
default_protocol: Option<String>, default_protocol: Option<String>,
@@ -235,46 +257,58 @@ struct Cli {
#[arg( #[arg(
short = 'u', short = 'u',
long, long,
env = "ET_DISABLE_ENCRYPTION",
help = t!("core_clap.disable_encryption").to_string(), help = t!("core_clap.disable_encryption").to_string(),
default_value = "false" num_args = 0..=1,
default_missing_value = "true"
)] )]
disable_encryption: bool, disable_encryption: Option<bool>,
#[arg( #[arg(
long, long,
env = "ET_MULTI_THREAD",
help = t!("core_clap.multi_thread").to_string(), help = t!("core_clap.multi_thread").to_string(),
default_value = "true" num_args = 0..=1,
default_missing_value = "true"
)] )]
multi_thread: bool, multi_thread: Option<bool>,
#[arg( #[arg(
long, long,
env = "ET_DISABLE_IPV6",
help = t!("core_clap.disable_ipv6").to_string(), help = t!("core_clap.disable_ipv6").to_string(),
default_value = "false" num_args = 0..=1,
default_missing_value = "true"
)] )]
disable_ipv6: bool, disable_ipv6: Option<bool>,
#[arg( #[arg(
long, long,
env = "ET_DEV_NAME",
help = t!("core_clap.dev_name").to_string() help = t!("core_clap.dev_name").to_string()
)] )]
dev_name: Option<String>, dev_name: Option<String>,
#[arg( #[arg(
long, long,
env = "ET_MTU",
help = t!("core_clap.mtu").to_string() help = t!("core_clap.mtu").to_string()
)] )]
mtu: Option<u16>, mtu: Option<u16>,
#[arg( #[arg(
long, long,
env = "ET_LATENCY_FIRST",
help = t!("core_clap.latency_first").to_string(), help = t!("core_clap.latency_first").to_string(),
default_value = "false" num_args = 0..=1,
default_missing_value = "true"
)] )]
latency_first: bool, latency_first: Option<bool>,
#[arg( #[arg(
long, long,
env = "ET_EXIT_NODES",
value_delimiter = ',',
help = t!("core_clap.exit_nodes").to_string(), help = t!("core_clap.exit_nodes").to_string(),
num_args = 0.. num_args = 0..
)] )]
@@ -282,34 +316,44 @@ struct Cli {
#[arg( #[arg(
long, long,
env = "ET_ENABLE_EXIT_NODE",
help = t!("core_clap.enable_exit_node").to_string(), help = t!("core_clap.enable_exit_node").to_string(),
default_value = "false" num_args = 0..=1,
default_missing_value = "true"
)] )]
enable_exit_node: bool, enable_exit_node: Option<bool>,
#[arg( #[arg(
long, long,
env = "ET_PROXY_FORWARD_BY_SYSTEM",
help = t!("core_clap.proxy_forward_by_system").to_string(), help = t!("core_clap.proxy_forward_by_system").to_string(),
default_value = "false" num_args = 0..=1,
default_missing_value = "true"
)] )]
proxy_forward_by_system: bool, proxy_forward_by_system: Option<bool>,
#[arg( #[arg(
long, long,
env = "ET_NO_TUN",
help = t!("core_clap.no_tun").to_string(), help = t!("core_clap.no_tun").to_string(),
default_value = "false" num_args = 0..=1,
default_missing_value = "true"
)] )]
no_tun: bool, no_tun: Option<bool>,
#[arg( #[arg(
long, long,
env = "ET_USE_SMOLTCP",
help = t!("core_clap.use_smoltcp").to_string(), help = t!("core_clap.use_smoltcp").to_string(),
default_value = "false" num_args = 0..=1,
default_missing_value = "true"
)] )]
use_smoltcp: bool, use_smoltcp: Option<bool>,
#[arg( #[arg(
long, long,
env = "ET_MANUAL_ROUTES",
value_delimiter = ',',
help = t!("core_clap.manual_routes").to_string(), help = t!("core_clap.manual_routes").to_string(),
num_args = 0.. num_args = 0..
)] )]
@@ -320,6 +364,8 @@ struct Cli {
// for local virtual network, will refuse relaying tun packet // for local virtual network, will refuse relaying tun packet
#[arg( #[arg(
long, long,
env = "ET_RELAY_NETWORK_WHITELIST",
value_delimiter = ',',
help = t!("core_clap.relay_network_whitelist").to_string(), help = t!("core_clap.relay_network_whitelist").to_string(),
num_args = 0.. num_args = 0..
)] )]
@@ -327,61 +373,75 @@ struct Cli {
#[arg( #[arg(
long, long,
env = "ET_DISABLE_P2P",
help = t!("core_clap.disable_p2p").to_string(), help = t!("core_clap.disable_p2p").to_string(),
default_value = "false" num_args = 0..=1,
default_missing_value = "true"
)] )]
disable_p2p: bool, disable_p2p: Option<bool>,
#[arg( #[arg(
long, long,
env = "ET_DISABLE_UDP_HOLE_PUNCHING",
help = t!("core_clap.disable_udp_hole_punching").to_string(), help = t!("core_clap.disable_udp_hole_punching").to_string(),
default_value = "false" num_args = 0..=1,
default_missing_value = "true"
)] )]
disable_udp_hole_punching: bool, disable_udp_hole_punching: Option<bool>,
#[arg( #[arg(
long, long,
env = "ET_RELAY_ALL_PEER_RPC",
help = t!("core_clap.relay_all_peer_rpc").to_string(), help = t!("core_clap.relay_all_peer_rpc").to_string(),
default_value = "false" num_args = 0..=1,
default_missing_value = "true"
)] )]
relay_all_peer_rpc: bool, relay_all_peer_rpc: Option<bool>,
#[cfg(feature = "socks5")] #[cfg(feature = "socks5")]
#[arg( #[arg(
long, long,
env = "ET_SOCKS5",
help = t!("core_clap.socks5").to_string() help = t!("core_clap.socks5").to_string()
)] )]
socks5: Option<u16>, socks5: Option<u16>,
#[arg( #[arg(
long, long,
env = "ET_COMPRESSION",
help = t!("core_clap.compression").to_string(), help = t!("core_clap.compression").to_string(),
default_value = "none",
)] )]
compression: String, compression: Option<String>,
#[arg( #[arg(
long, long,
env = "ET_BIND_DEVICE",
help = t!("core_clap.bind_device").to_string() help = t!("core_clap.bind_device").to_string()
)] )]
bind_device: Option<bool>, bind_device: Option<bool>,
#[arg( #[arg(
long, long,
env = "ET_ENABLE_KCP_PROXY",
help = t!("core_clap.enable_kcp_proxy").to_string(), help = t!("core_clap.enable_kcp_proxy").to_string(),
default_value = "false" num_args = 0..=1,
default_missing_value = "true"
)] )]
enable_kcp_proxy: bool, enable_kcp_proxy: Option<bool>,
#[arg( #[arg(
long, long,
env = "ET_DISABLE_KCP_INPUT",
help = t!("core_clap.disable_kcp_input").to_string(), help = t!("core_clap.disable_kcp_input").to_string(),
default_value = "false" num_args = 0..=1,
default_missing_value = "true"
)] )]
disable_kcp_input: bool, disable_kcp_input: Option<bool>,
#[arg( #[arg(
long, long,
env = "ET_PORT_FORWARD",
value_delimiter = ',',
help = t!("core_clap.port_forward").to_string(), help = t!("core_clap.port_forward").to_string(),
num_args = 1.. num_args = 1..
)] )]
@@ -449,25 +509,28 @@ impl TryFrom<&Cli> for TomlConfigLoader {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(cli: &Cli) -> Result<Self, Self::Error> { fn try_from(cli: &Cli) -> Result<Self, Self::Error> {
if let Some(config_file) = &cli.config_file { let cfg = if let Some(config_file) = &cli.config_file {
println!( TomlConfigLoader::new(config_file)
"NOTICE: loading config file: {:?}, will ignore all command line flags\n", .with_context(|| format!("failed to load config file: {:?}", cli.config_file))?
config_file } else {
); TomlConfigLoader::default()
return Ok(TomlConfigLoader::new(config_file) };
.with_context(|| format!("failed to load config file: {:?}", cli.config_file))?);
if cli.hostname.is_some() {
cfg.set_hostname(cli.hostname.clone());
} }
let cfg = TomlConfigLoader::default(); let old_ns = cfg.get_network_identity();
let network_name = cli.network_name.clone().unwrap_or(old_ns.network_name);
let network_secret = cli
.network_secret
.clone()
.unwrap_or(old_ns.network_secret.unwrap_or_default());
cfg.set_network_identity(NetworkIdentity::new(network_name, network_secret));
cfg.set_hostname(cli.hostname.clone()); if let Some(dhcp) = cli.dhcp {
cfg.set_dhcp(dhcp);
cfg.set_network_identity(NetworkIdentity::new( }
cli.network_name.clone(),
cli.network_secret.clone(),
));
cfg.set_dhcp(cli.dhcp);
if let Some(ipv4) = &cli.ipv4 { if let Some(ipv4) = &cli.ipv4 {
cfg.set_ipv4(Some(ipv4.parse().with_context(|| { cfg.set_ipv4(Some(ipv4.parse().with_context(|| {
@@ -475,7 +538,9 @@ impl TryFrom<&Cli> for TomlConfigLoader {
})?)) })?))
} }
let mut peers = Vec::<PeerConfig>::with_capacity(cli.peers.len()); if !cli.peers.is_empty() {
let mut peers = cfg.get_peers();
peers.reserve(peers.len() + cli.peers.len());
for p in &cli.peers { for p in &cli.peers {
peers.push(PeerConfig { peers.push(PeerConfig {
uri: p uri: p
@@ -484,14 +549,18 @@ impl TryFrom<&Cli> for TomlConfigLoader {
}); });
} }
cfg.set_peers(peers); cfg.set_peers(peers);
}
if cli.no_listener || !cli.listeners.is_empty() {
cfg.set_listeners( cfg.set_listeners(
Cli::parse_listeners(cli.no_listener, cli.listeners.clone())? Cli::parse_listeners(cli.no_listener, cli.listeners.clone())?
.into_iter() .into_iter()
.map(|s| s.parse().unwrap()) .map(|s| s.parse().unwrap())
.collect(), .collect(),
); );
}
if !cli.mapped_listeners.is_empty() {
cfg.set_mapped_listeners(Some( cfg.set_mapped_listeners(Some(
cli.mapped_listeners cli.mapped_listeners
.iter() .iter()
@@ -508,6 +577,7 @@ impl TryFrom<&Cli> for TomlConfigLoader {
}) })
.collect(), .collect(),
)); ));
}
for n in cli.proxy_networks.iter() { for n in cli.proxy_networks.iter() {
cfg.add_proxy_cidr( cfg.add_proxy_cidr(
@@ -516,10 +586,15 @@ impl TryFrom<&Cli> for TomlConfigLoader {
); );
} }
cfg.set_rpc_portal( let rpc_portal = if let Some(r) = &cli.rpc_portal {
Cli::parse_rpc_portal(cli.rpc_portal.clone()) Cli::parse_rpc_portal(r.clone())
.with_context(|| format!("failed to parse rpc portal: {}", cli.rpc_portal))?, .with_context(|| format!("failed to parse rpc portal: {}", r))?
); } else if let Some(r) = cfg.get_rpc_portal() {
r
} else {
Cli::parse_rpc_portal("0".into())?
};
cfg.set_rpc_portal(rpc_portal);
if let Some(external_nodes) = cli.external_node.as_ref() { if let Some(external_nodes) = cli.external_node.as_ref() {
let mut old_peers = cfg.get_peers(); let mut old_peers = cfg.get_peers();
@@ -537,15 +612,29 @@ impl TryFrom<&Cli> for TomlConfigLoader {
}); });
} }
if cli.file_log_dir.is_some() || cli.file_log_level.is_some() { if let Some(inst_name) = &cli.instance_name {
cfg.set_file_logger_config(FileLoggerConfig { cfg.set_inst_name(inst_name.clone());
level: cli.file_log_level.clone(),
dir: cli.file_log_dir.clone(),
file: Some(format!("easytier-{}", cli.instance_name)),
});
} }
cfg.set_inst_name(cli.instance_name.clone()); if cli.file_log_dir.is_some() || cli.file_log_level.is_some() {
let inst_name = cfg.get_inst_name();
let old_fl = cfg.get_file_logger_config();
let file_log_dir = if cli.file_log_dir.is_some() {
&cli.file_log_dir
} else {
&old_fl.dir
};
let file_log_level = if cli.file_log_level.is_some() {
&cli.file_log_level
} else {
&old_fl.level
};
cfg.set_file_logger_config(FileLoggerConfig {
level: file_log_level.clone(),
dir: file_log_dir.clone(),
file: Some(format!("easytier-{}", inst_name)),
});
}
if let Some(vpn_portal) = cli.vpn_portal.as_ref() { if let Some(vpn_portal) = cli.vpn_portal.as_ref() {
let url: url::Url = vpn_portal let url: url::Url = vpn_portal
@@ -622,44 +711,56 @@ impl TryFrom<&Cli> for TomlConfigLoader {
} }
let mut f = cfg.get_flags(); let mut f = cfg.get_flags();
if cli.default_protocol.is_some() { if let Some(default_protocol) = &cli.default_protocol {
f.default_protocol = cli.default_protocol.as_ref().unwrap().clone(); f.default_protocol = default_protocol.clone()
};
if let Some(v) = cli.disable_encryption {
f.enable_encryption = !v;
}
if let Some(v) = cli.disable_ipv6 {
f.enable_ipv6 = !v;
}
f.latency_first = cli.latency_first.unwrap_or(f.latency_first);
if let Some(dev_name) = &cli.dev_name {
f.dev_name = dev_name.clone()
} }
f.enable_encryption = !cli.disable_encryption;
f.enable_ipv6 = !cli.disable_ipv6;
f.latency_first = cli.latency_first;
f.dev_name = cli.dev_name.clone().unwrap_or_default();
if let Some(mtu) = cli.mtu { if let Some(mtu) = cli.mtu {
f.mtu = mtu as u32; f.mtu = mtu as u32;
} }
f.enable_exit_node = cli.enable_exit_node; f.enable_exit_node = cli.enable_exit_node.unwrap_or(f.enable_exit_node);
f.proxy_forward_by_system = cli.proxy_forward_by_system; f.proxy_forward_by_system = cli
f.no_tun = cli.no_tun || cfg!(not(feature = "tun")); .proxy_forward_by_system
f.use_smoltcp = cli.use_smoltcp; .unwrap_or(f.proxy_forward_by_system);
f.no_tun = cli.no_tun.unwrap_or(f.no_tun) || cfg!(not(feature = "tun"));
f.use_smoltcp = cli.use_smoltcp.unwrap_or(f.use_smoltcp);
if let Some(wl) = cli.relay_network_whitelist.as_ref() { if let Some(wl) = cli.relay_network_whitelist.as_ref() {
f.relay_network_whitelist = wl.join(" "); f.relay_network_whitelist = wl.join(" ");
} }
f.disable_p2p = cli.disable_p2p; f.disable_p2p = cli.disable_p2p.unwrap_or(f.disable_p2p);
f.disable_udp_hole_punching = cli.disable_udp_hole_punching; f.disable_udp_hole_punching = cli
f.relay_all_peer_rpc = cli.relay_all_peer_rpc; .disable_udp_hole_punching
f.multi_thread = cli.multi_thread; .unwrap_or(f.disable_udp_hole_punching);
f.data_compress_algo = match cli.compression.as_str() { f.relay_all_peer_rpc = cli.relay_all_peer_rpc.unwrap_or(f.relay_all_peer_rpc);
f.multi_thread = cli.multi_thread.unwrap_or(f.multi_thread);
if let Some(compression) = &cli.compression {
f.data_compress_algo = match compression.as_str() {
"none" => CompressionAlgoPb::None, "none" => CompressionAlgoPb::None,
"zstd" => CompressionAlgoPb::Zstd, "zstd" => CompressionAlgoPb::Zstd,
_ => panic!( _ => panic!(
"unknown compression algorithm: {}, supported: none, zstd", "unknown compression algorithm: {}, supported: none, zstd",
cli.compression compression
), ),
} }
.into(); .into();
if let Some(bind_device) = cli.bind_device {
f.bind_device = bind_device;
} }
f.enable_kcp_proxy = cli.enable_kcp_proxy; f.bind_device = cli.bind_device.unwrap_or(f.bind_device);
f.disable_kcp_input = cli.disable_kcp_input; f.enable_kcp_proxy = cli.enable_kcp_proxy.unwrap_or(f.enable_kcp_proxy);
f.disable_kcp_input = cli.disable_kcp_input.unwrap_or(f.disable_kcp_input);
cfg.set_flags(f); cfg.set_flags(f);
if !cli.exit_nodes.is_empty() {
cfg.set_exit_nodes(cli.exit_nodes.clone()); cfg.set_exit_nodes(cli.exit_nodes.clone());
}
Ok(cfg) Ok(cfg)
} }