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

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