Files
EasyTier/easytier/src/core.rs
T
KKRainbow 021f523431 refactor(core): separate portable core from native runtime (#2451)
Create easytier-core as the portable owner of configuration,
connectivity, tunnels, peer and routing state, gateways, management,
the data plane, and instance lifecycle. Keep operating-system
integration, native protocol engines, process startup, and presentation
in easytier behind explicit Host capability adapters.

Create easytier-proto to own schemas, generated RPC types, descriptors,
and feature-scoped protocol slices. Remove runtime protobuf reflection
from core while preserving unknown route-peer fields across forwarding.

Normalize instance construction through CoreInstance, CoreHostAdapters,
CoreProcessRuntime, and InstanceManager. Make the runtime config store
the only authoritative mutable configuration after startup.

Move the portable TCP/UDP data plane into core and extract a generic
OperationBroker for completion, cancellation, disposal, and capacity
accounting. Expose the session-based FFI v2 completion API and keep the
WASI guest ABI, wire schemas, and adapters with core.

Migrate CLI, GUI, web, FFI, Android JNI, OHOS, uptime, and mobile
consumers to the shared manager and core state. Add explicit user/web
config ownership and revision-aware web reconciliation.

Preserve configuration, wire, and management behavior while fixing
regressions discovered by the full platform and integration matrix:

- inherit advertised relay capabilities in foreign networks;
- refresh OSPF peer state immediately after runtime config changes;
- restore CLI GlobalCtx event output without forcing GUI logging;
- retain legacy encryption names and standalone RPC tunnel metadata;
- restore ICMP host composition and fragmented UDP handling;
- use portable 64-bit atomics on 32-bit MIPS targets; and
- retain discarded operations until late cancellation completes.

Validate the refactor across 45 GitHub checks, including Linux, macOS,
Windows, FreeBSD, web, GUI, Android, OHOS, feature profiles, and
three-node and subnet-proxy integration tests.

BREAKING CHANGE: internal Rust module paths are not preserved. Legacy
native data-plane APIs are replaced by the session-based FFI v2 API.
The dedicated Android data-plane wrapper is removed.
2026-07-26 15:41:55 +08:00

1775 lines
53 KiB
Rust

use crate::{
ShellType,
common::{
config::{
ConfigFileControl, ConfigLoader, ConsoleLoggerConfig, EncryptionAlgorithm,
FileLoggerConfig, LoggingConfigLoader, NetworkIdentity, PeerConfig, PortForwardConfig,
TomlConfigLoader, VpnPortalConfig, add_proxy_network_to_config, load_config_from_file,
load_toml_config_from_path, parse_mapped_listener_urls,
},
constants::EASYTIER_VERSION,
log,
},
instance::factory::native_cli_instance_manager,
proto::common::{CompressionAlgoPb, 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 guarden::defer;
use rust_i18n::t;
use std::{
net::{IpAddr, SocketAddr},
path::PathBuf,
process::ExitCode,
sync::{Arc, atomic::AtomicBool},
};
use strum::VariantArray;
use tokio::io::AsyncReadExt;
use crate::tunnel::IpScheme;
#[cfg(feature = "jemalloc-prof")]
use jemalloc_ctl::{Access as _, AsName as _, epoch, stats};
#[cfg(target_os = "windows")]
windows_service::define_windows_service!(ffi_service_main, win_service_main);
fn set_prof_active(_active: bool) {
#[cfg(feature = "jemalloc-prof")]
{
const PROF_ACTIVE: &[u8] = b"prof.active\0";
let name = PROF_ACTIVE.name();
name.write(_active).expect("Should succeed to set prof");
}
}
#[cfg(feature = "jemalloc-prof")]
fn get_dump_profile_path(cur_allocated: usize, suffix: &str) -> String {
format!(
"profile-{}-{}.{}",
cur_allocated,
chrono::Local::now().format("%Y-%m-%d-%H-%M-%S"),
suffix
)
}
fn dump_profile(_cur_allocated: usize) {
#[cfg(feature = "jemalloc-prof")]
{
const PROF_DUMP: &[u8] = b"prof.dump\0";
static mut PROF_DUMP_FILE_NAME: [u8; 128] = [0; 128];
let file_name_str = get_dump_profile_path(_cur_allocated, "out");
// copy file name to PROF_DUMP
let file_name = file_name_str.as_bytes();
let len = file_name.len();
if len > 127 {
panic!("file name too long");
}
unsafe {
PROF_DUMP_FILE_NAME[..len].copy_from_slice(file_name);
// set the last byte to 0
PROF_DUMP_FILE_NAME[len] = 0;
let name = PROF_DUMP.name();
name.write(&PROF_DUMP_FILE_NAME[..len + 1])
.expect("Should succeed to dump profile");
println!("dump profile to: {}", file_name_str);
}
}
}
#[derive(Parser, Debug)]
#[command(name = "easytier-core", author, version = EASYTIER_VERSION , about, long_about = None)]
struct Cli {
#[arg(
short = 'w',
long,
env = "ET_CONFIG_SERVER",
help = t!("core_clap.config_server").to_string()
)]
config_server: Option<String>,
#[arg(
long,
env = "ET_MACHINE_ID",
help = t!("core_clap.machine_id").to_string()
)]
machine_id: Option<String>,
#[arg(
short,
long,
env = "ET_CONFIG_FILE",
value_delimiter = ',',
help = t!("core_clap.config_file").to_string(),
num_args = 1..,
)]
config_file: Option<Vec<PathBuf>>,
#[arg(
long,
env = "ET_CONFIG_DIR",
help = t!("core_clap.config_dir").to_string()
)]
config_dir: Option<PathBuf>,
#[command(flatten)]
network_options: NetworkOptions,
#[command(flatten)]
logging_options: LoggingOptions,
#[command(flatten)]
rpc_portal_options: RpcPortalOptions,
#[clap(long, help = t!("core_clap.generate_completions").to_string())]
gen_autocomplete: Option<ShellType>,
#[clap(long, help = t!("core_clap.check_config").to_string())]
check_config: bool,
#[clap(long, help = t!("core_clap.daemon").to_string())]
daemon: bool,
#[clap(long, help = t!("core_clap.disable_env_parsing").to_string())]
disable_env_parsing: bool,
}
#[derive(Parser, Debug, Default, PartialEq, Eq)]
struct NetworkOptions {
#[arg(
long,
env = "ET_NETWORK_NAME",
help = t!("core_clap.network_name").to_string(),
)]
network_name: Option<String>,
#[arg(
long,
env = "ET_NETWORK_SECRET",
help = t!("core_clap.network_secret").to_string(),
)]
network_secret: Option<String>,
#[arg(
short,
long,
env = "ET_IPV4",
help = t!("core_clap.ipv4").to_string()
)]
ipv4: Option<String>,
#[arg(
long,
env = "ET_IPV6",
help = t!("core_clap.ipv6").to_string()
)]
ipv6: Option<String>,
#[arg(
long,
env = "ET_IPV6_PUBLIC_ADDR_PROVIDER",
help = t!("core_clap.ipv6_public_addr_provider").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
ipv6_public_addr_provider: Option<bool>,
#[arg(
long,
env = "ET_IPV6_PUBLIC_ADDR_AUTO",
help = t!("core_clap.ipv6_public_addr_auto").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
ipv6_public_addr_auto: Option<bool>,
#[arg(
long,
env = "ET_IPV6_PUBLIC_ADDR_PREFIX",
help = t!("core_clap.ipv6_public_addr_prefix").to_string()
)]
ipv6_public_addr_prefix: Option<String>,
#[arg(
short,
long,
env = "ET_DHCP",
help = t!("core_clap.dhcp").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
dhcp: Option<bool>,
#[arg(
short,
long,
env = "ET_PEERS",
value_delimiter = ',',
help = t!("core_clap.peers").to_string(),
num_args = 0..
)]
peers: Vec<String>,
#[arg(
short,
long,
env = "ET_EXTERNAL_NODE",
help = t!("core_clap.external_node").to_string()
)]
external_node: Option<String>,
#[arg(
short = 'n',
long,
env = "ET_PROXY_NETWORKS",
value_delimiter = ',',
help = t!("core_clap.proxy_networks").to_string()
)]
proxy_networks: Vec<String>,
#[arg(
short,
long,
env = "ET_LISTENERS",
value_delimiter = ',',
help = t!("core_clap.listeners").to_string(),
num_args = 0..
)]
listeners: Vec<String>,
#[arg(
long,
env = "ET_MAPPED_LISTENERS",
value_delimiter = ',',
help = t!("core_clap.mapped_listeners").to_string(),
num_args = 0..
)]
mapped_listeners: Vec<String>,
#[arg(
long,
env = "ET_NO_LISTENER",
help = t!("core_clap.no_listener").to_string(),
default_value = "false",
)]
no_listener: bool,
#[arg(
long,
env = "ET_HOSTNAME",
help = t!("core_clap.hostname").to_string()
)]
hostname: Option<String>,
#[arg(
short = 'm',
long,
env = "ET_INSTANCE_NAME",
help = t!("core_clap.instance_name").to_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>,
#[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",
value_delimiter = ',',
help = t!("core_clap.exit_nodes").to_string(),
num_args = 0..
)]
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",
value_delimiter = ',',
help = t!("core_clap.manual_routes").to_string(),
num_args = 0..
)]
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,
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(),
)]
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",
value_delimiter = ',',
help = t!("core_clap.port_forward").to_string(),
num_args = 1..
)]
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 = ',',
help = t!("core_clap.tcp_whitelist").to_string(),
num_args = 0..
)]
tcp_whitelist: Vec<String>,
#[arg(
long,
value_delimiter = ',',
help = t!("core_clap.udp_whitelist").to_string(),
num_args = 0..
)]
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",
value_delimiter = ',',
help = t!("core_clap.stun_servers").to_string(),
num_args = 0..
)]
stun_servers: Option<Vec<String>>,
#[arg(
long,
env = "ET_STUN_SERVERS_V6",
value_delimiter = ',',
help = t!("core_clap.stun_servers_v6").to_string(),
num_args = 0..
)]
stun_servers_v6: Option<Vec<String>>,
#[arg(
long,
env = "ET_SECURE_MODE",
help = t!("core_clap.secure_mode").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
secure_mode: Option<bool>,
#[arg(
long,
env = "ET_LOCAL_PRIVATE_KEY",
help = t!("core_clap.local_private_key").to_string()
)]
local_private_key: Option<String>,
#[arg(
long,
env = "ET_LOCAL_PUBLIC_KEY",
help = t!("core_clap.local_public_key").to_string()
)]
local_public_key: Option<String>,
#[arg(
long,
env = "ET_CREDENTIAL",
help = t!("core_clap.credential").to_string()
)]
credential: Option<String>,
#[arg(
long,
env = "ET_CREDENTIAL_FILE",
help = t!("core_clap.credential_file").to_string()
)]
credential_file: Option<PathBuf>,
}
#[derive(Parser, Debug)]
struct LoggingOptions {
#[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_FILE_LOG_SIZE",
help = t!("core_clap.file_log_size_mb").to_string()
)]
file_log_size: Option<u64>,
#[arg(
long,
env = "ET_FILE_LOG_COUNT",
help = t!("core_clap.file_log_count").to_string()
)]
file_log_count: Option<usize>,
}
#[derive(Parser, Debug)]
struct RpcPortalOptions {
#[arg(
short,
long,
env = "ET_RPC_PORTAL",
help = t!("core_clap.rpc_portal").to_string(),
)]
rpc_portal: Option<String>,
#[arg(
long,
env = "ET_RPC_PORTAL_WHITELIST",
value_delimiter = ',',
help = t!("core_clap.rpc_portal_whitelist").to_string(),
)]
rpc_portal_whitelist: Option<Vec<IpCidr>>,
}
impl Cli {
fn gen_listeners(addr: SocketAddr) -> impl Iterator<Item = String> {
let dynamic = addr.port() == 0;
IpScheme::VARIANTS.iter().map(move |proto| {
let mut addr = addr;
if !dynamic {
addr.set_port(addr.port() + proto.port_offset());
}
format!("{}://{}", proto, addr)
})
}
fn parse_listeners(no_listener: bool, listeners: Vec<String>) -> anyhow::Result<Vec<String>> {
if no_listener || listeners.is_empty() {
return Ok(vec![]);
}
let mut parsed = vec![];
for l in listeners.into_iter() {
if let Ok(port) = l.parse::<u16>() {
parsed.extend(Self::gen_listeners(SocketAddr::new(
"0.0.0.0".parse()?,
port,
)));
continue;
}
if let Ok(ip) = l.trim_matches(|c| c == '[' || c == ']').parse::<IpAddr>() {
parsed.extend(Self::gen_listeners(SocketAddr::new(ip, 11010)));
continue;
}
if let Ok(addr) = l.parse::<SocketAddr>() {
parsed.extend(Self::gen_listeners(addr));
continue;
}
let (scheme, rest) = l.split_once(':').unwrap_or((&l, ""));
let Ok(scheme) = scheme.parse::<IpScheme>() else {
anyhow::bail!("invalid listener: {}", l);
};
if rest.is_empty() {
parsed.push(format!(
"{}://0.0.0.0:{}",
scheme,
11010 + scheme.port_offset()
));
continue;
}
if let Ok(port) = rest.parse::<u16>() {
parsed.push(format!("{}://0.0.0.0:{}", scheme, port));
continue;
}
if !l.parse::<url::Url>()?.has_authority() {
anyhow::bail!("invalid listener: {}", l);
}
parsed.push(l);
}
Ok(parsed)
}
}
impl NetworkOptions {
fn can_merge(
&self,
cfg: &TomlConfigLoader,
source: ConfigFileSource,
explicit_config_file_count: usize,
config_dir_file_count: usize,
) -> bool {
if (*self) == NetworkOptions::default() {
return false;
}
if source == ConfigFileSource::CliConfigFile
&& explicit_config_file_count == 1
&& config_dir_file_count == 0
{
return true;
}
let Some(network_name) = &self.network_name else {
return false;
};
if source == ConfigFileSource::ConfigDir {
return cfg.get_network_identity().network_name == *network_name;
}
cfg.get_network_identity().network_name == *network_name
}
fn merge_into(&self, cfg: &TomlConfigLoader) -> anyhow::Result<()> {
if self.hostname.is_some() {
cfg.set_hostname(self.hostname.clone());
}
let old_ns = cfg.get_network_identity();
let network_name = self
.network_name
.clone()
.unwrap_or_else(|| old_ns.network_name.clone());
if self.credential.is_some() {
// Credential mode: no network_secret, authenticate via credential keypair
cfg.set_network_identity(NetworkIdentity::new_credential(network_name));
} else if let Some(network_secret) = &self.network_secret {
cfg.set_network_identity(NetworkIdentity::new(network_name, network_secret.clone()));
} else if let Some(network_secret) = old_ns.network_secret {
cfg.set_network_identity(NetworkIdentity::new(network_name, network_secret));
} else {
cfg.set_network_identity(NetworkIdentity::new_credential(network_name));
}
if let Some(dhcp) = self.dhcp {
cfg.set_dhcp(dhcp);
}
if let Some(ipv4) = &self.ipv4 {
cfg.set_ipv4(Some(ipv4.parse().with_context(|| {
format!("failed to parse ipv4 address: {}", ipv4)
})?))
}
if let Some(ipv6) = &self.ipv6 {
cfg.set_ipv6(Some(ipv6.parse().with_context(|| {
format!("failed to parse ipv6 address: {}", ipv6)
})?))
}
if let Some(enabled) = self.ipv6_public_addr_provider {
cfg.set_ipv6_public_addr_provider(enabled);
}
if let Some(enabled) = self.ipv6_public_addr_auto {
cfg.set_ipv6_public_addr_auto(enabled);
}
if let Some(prefix) = &self.ipv6_public_addr_prefix {
cfg.set_ipv6_public_addr_prefix(Some(prefix.parse().with_context(|| {
format!("failed to parse ipv6 public address prefix: {}", prefix)
})?));
}
if !self.peers.is_empty() {
let mut peers = cfg.get_peers();
peers.reserve(peers.len() + self.peers.len());
for p in &self.peers {
peers.push(PeerConfig {
uri: p
.parse()
.with_context(|| format!("failed to parse peer uri: {}", p))?,
peer_public_key: None,
});
}
cfg.set_peers(peers);
}
if self.no_listener || !self.listeners.is_empty() {
cfg.set_listeners(
Cli::parse_listeners(self.no_listener, self.listeners.clone())
.with_context(|| format!("failed to parse listeners: {:?}", self.listeners))?
.into_iter()
.map(|s| s.parse().unwrap())
.collect(),
);
} else if cfg.get_listeners().is_none() {
cfg.set_listeners(
Cli::parse_listeners(false, vec!["11010".to_string()])?
.into_iter()
.map(|s| s.parse().unwrap())
.collect(),
);
}
if !self.mapped_listeners.is_empty() {
cfg.set_mapped_listeners(Some(parse_mapped_listener_urls(&self.mapped_listeners)?));
}
for n in self.proxy_networks.iter() {
add_proxy_network_to_config(n, cfg)?;
}
if let Some(external_nodes) = self.external_node.as_ref() {
let mut old_peers = cfg.get_peers();
old_peers.push(PeerConfig {
uri: external_nodes.parse().with_context(|| {
format!("failed to parse external node uri: {}", external_nodes)
})?,
peer_public_key: None,
});
cfg.set_peers(old_peers);
}
if let Some(inst_name) = &self.instance_name {
cfg.set_inst_name(inst_name.clone());
}
if let Some(vpn_portal) = self.vpn_portal.as_ref() {
let url: url::Url = vpn_portal
.parse()
.with_context(|| format!("failed to parse vpn portal url: {}", vpn_portal))?;
let host = url
.host_str()
.ok_or_else(|| anyhow::anyhow!("vpn portal url missing host"))?;
let port = url
.port()
.ok_or_else(|| anyhow::anyhow!("vpn portal url missing port"))?;
let client_cidr = url.path()[1..].parse().with_context(|| {
format!("failed to parse vpn portal client cidr: {}", url.path())
})?;
let wireguard_listen: SocketAddr = format!("{}:{}", host, port).parse().unwrap();
cfg.set_vpn_portal_config(VpnPortalConfig {
wireguard_listen,
client_cidr,
});
}
if let Some(manual_routes) = self.manual_routes.as_ref() {
let mut routes = Vec::<cidr::Ipv4Cidr>::with_capacity(manual_routes.len());
for r in manual_routes {
routes.push(
r.parse()
.with_context(|| format!("failed to parse route: {}", r))?,
);
}
cfg.set_routes(Some(routes));
}
#[cfg(feature = "socks5")]
if let Some(socks5_proxy) = self.socks5 {
cfg.set_socks5_portal(Some(
format!("socks5://0.0.0.0:{}", socks5_proxy)
.parse()
.unwrap(),
));
}
for port_forward in self.port_forward.iter() {
let example_str = ", example: udp://0.0.0.0:12345/10.126.126.1:12345";
let bind_addr = format!(
"{}:{}",
port_forward.host_str().expect("local bind host is missing"),
port_forward.port().expect("local bind port is missing")
)
.parse()
.unwrap_or_else(|_| panic!("failed to parse local bind addr {}", example_str));
let dst_addr = port_forward
.path_segments()
.unwrap_or_else(|| panic!("remote destination addr is missing {}", example_str))
.next()
.unwrap_or_else(|| panic!("remote destination addr is missing {}", example_str))
.to_string()
.parse()
.unwrap_or_else(|_| {
panic!("failed to parse remote destination addr {}", example_str)
});
let port_forward_item = PortForwardConfig {
bind_addr,
dst_addr,
proto: port_forward.scheme().to_string(),
};
let mut old = cfg.get_port_forwards();
old.push(port_forward_item);
cfg.set_port_forwards(old);
}
if let Some(ref credential_file) = self.credential_file {
cfg.set_credential_file(Some(credential_file.clone()));
}
if let Some(ref credential_secret) = self.credential {
// --credential implies --secure-mode and sets the credential private key
let c = SecureModeConfig {
enabled: true,
local_private_key: Some(credential_secret.clone()),
local_public_key: None,
};
cfg.set_secure_mode(Some(normalize_secure_mode_config(c)?));
} else if let Some(secure_mode) = self.secure_mode
&& secure_mode
{
let c = SecureModeConfig {
enabled: secure_mode,
local_private_key: self.local_private_key.clone(),
local_public_key: self.local_public_key.clone(),
};
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);
if !self.exit_nodes.is_empty() {
cfg.set_exit_nodes(self.exit_nodes.clone());
}
let mut old_tcp_whitelist = cfg.get_tcp_whitelist();
old_tcp_whitelist.extend(self.tcp_whitelist.clone());
cfg.set_tcp_whitelist(old_tcp_whitelist);
let mut old_udp_whitelist = cfg.get_udp_whitelist();
old_udp_whitelist.extend(self.udp_whitelist.clone());
cfg.set_udp_whitelist(old_udp_whitelist);
if let Some(stun_servers) = &self.stun_servers {
let mut old_stun_servers = cfg.get_stun_servers().unwrap_or_default();
old_stun_servers.extend(stun_servers.iter().cloned());
cfg.set_stun_servers(Some(old_stun_servers));
}
if let Some(stun_servers_v6) = &self.stun_servers_v6 {
let mut old_stun_servers_v6 = cfg.get_stun_servers_v6().unwrap_or_default();
old_stun_servers_v6.extend(stun_servers_v6.iter().cloned());
cfg.set_stun_servers_v6(Some(old_stun_servers_v6));
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ConfigFileSource {
CliConfigFile,
ConfigDir,
}
impl LoggingConfigLoader for &LoggingOptions {
fn get_console_logger_config(&self) -> ConsoleLoggerConfig {
ConsoleLoggerConfig {
level: self.console_log_level.clone(),
}
}
fn get_file_logger_config(&self) -> FileLoggerConfig {
FileLoggerConfig {
level: self.file_log_level.clone(),
dir: self.file_log_dir.clone(),
file: None,
size_mb: self.file_log_size,
count: self.file_log_count,
}
}
}
#[cfg(target_os = "windows")]
fn win_service_set_work_dir(service_name: &std::ffi::OsString) -> anyhow::Result<()> {
use crate::common::constants::WIN_SERVICE_WORK_DIR_REG_KEY;
use winreg::{RegKey, enums::*};
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let key = hklm.open_subkey_with_flags(WIN_SERVICE_WORK_DIR_REG_KEY, KEY_READ)?;
let dir_pat_str = key.get_value::<std::ffi::OsString, _>(service_name)?;
let dir_path = std::fs::canonicalize(dir_pat_str)?;
std::env::set_current_dir(dir_path)?;
Ok(())
}
#[cfg(target_os = "windows")]
fn win_service_event_loop(
stop_notify: std::sync::Arc<tokio::sync::Notify>,
cli: Cli,
status_handle: windows_service::service_control_handler::ServiceStatusHandle,
) {
use std::time::Duration;
use tokio::runtime::Runtime;
use windows_service::service::*;
let normal_status = ServiceStatus {
service_type: ServiceType::OWN_PROCESS,
current_state: ServiceState::Running,
controls_accepted: ServiceControlAccept::STOP,
exit_code: ServiceExitCode::Win32(0),
checkpoint: 0,
wait_hint: Duration::default(),
process_id: None,
};
let error_status = ServiceStatus {
service_type: ServiceType::OWN_PROCESS,
current_state: ServiceState::Stopped,
controls_accepted: ServiceControlAccept::empty(),
exit_code: ServiceExitCode::ServiceSpecific(1u32),
checkpoint: 0,
wait_hint: Duration::default(),
process_id: None,
};
std::thread::spawn(move || {
let rt = Runtime::new().unwrap();
rt.block_on(async move {
tokio::select! {
res = run_main(cli) => {
match res {
Ok(_) => {
status_handle.set_service_status(normal_status).unwrap();
std::process::exit(0);
}
Err(error) => {
status_handle.set_service_status(error_status).unwrap();
log::error!(?error);
}
}
},
_ = stop_notify.notified() => {
_ = status_handle.set_service_status(normal_status);
std::process::exit(0);
}
}
});
});
}
fn parse_cli() -> Cli {
let mut cli = Cli::parse();
// 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());
}
if let Some(stun_servers_v6) = &mut cli.network_options.stun_servers_v6 {
stun_servers_v6.retain(|s| !s.trim().is_empty());
}
cli
}
#[cfg(target_os = "windows")]
fn win_service_main(arg: Vec<std::ffi::OsString>) {
use std::{sync::Arc, time::Duration};
use tokio::sync::Notify;
use windows_service::{service::*, service_control_handler::*};
_ = win_service_set_work_dir(&arg[0]);
let cli = parse_cli();
let stop_notify_send = Arc::new(Notify::new());
let stop_notify_recv = Arc::clone(&stop_notify_send);
let event_handler = move |control_event| -> ServiceControlHandlerResult {
match control_event {
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
ServiceControl::Stop => {
stop_notify_send.notify_one();
ServiceControlHandlerResult::NoError
}
_ => ServiceControlHandlerResult::NotImplemented,
}
};
let status_handle = register(String::new(), event_handler).expect("register service fail");
let next_status = ServiceStatus {
service_type: ServiceType::OWN_PROCESS,
current_state: ServiceState::Running,
controls_accepted: ServiceControlAccept::STOP,
exit_code: ServiceExitCode::Win32(0),
checkpoint: 0,
wait_hint: Duration::default(),
process_id: None,
};
status_handle
.set_service_status(next_status)
.expect("set service status fail");
win_service_event_loop(stop_notify_recv, cli, status_handle);
}
async fn run_main(cli: Cli) -> anyhow::Result<()> {
defer!(dump_profile(0););
log::init(&cli.logging_options, true)?;
let manager = Arc::new(native_cli_instance_manager().with_config_path(cli.config_dir.clone()));
let _rpc_server = ApiRpcServer::new(
cli.rpc_portal_options.rpc_portal,
cli.rpc_portal_options.rpc_portal_whitelist,
manager.clone(),
)?
.serve()
.await?;
let _web_client = if let Some(config_server_url_s) = cli.config_server.as_ref() {
let wc = web_client::run_web_client(
config_server_url_s,
crate::common::MachineIdOptions {
explicit_machine_id: cli.machine_id.clone(),
state_dir: None,
},
cli.network_options.hostname.clone(),
cli.network_options.secure_mode.unwrap_or(false),
manager.clone(),
None,
)
.await
.inspect(|_| {
log::info!(
server = config_server_url_s,
"Web client started successfully...",
);
log::info!("Official config website: https://easytier.cn/web");
})?;
Some(wc)
} else {
None
};
let _daemon_guard = if cli.daemon {
Some(manager.register_daemon())
} else {
None
};
let explicit_config_file_count = cli.config_file.as_ref().map_or(0, |files| files.len());
let mut config_dir_file_count = 0;
let mut config_files = if let Some(v) = cli.config_file {
v.iter()
.cloned()
.map(|path| (path, ConfigFileSource::CliConfigFile))
.collect()
} else {
vec![]
};
if let Some(config_dir) = cli.config_dir.as_ref() {
if !config_dir.is_dir() {
anyhow::bail!("config_dir {} is not a directory", config_dir.display());
}
for entry in std::fs::read_dir(config_dir)? {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
let Some(ext) = path.extension() else {
continue;
};
if ext != "toml" {
continue;
}
config_dir_file_count += 1;
config_files.push((path, ConfigFileSource::ConfigDir));
}
}
let config_file_count = config_files.len();
let mut crate_cli_network = {
if cli.daemon {
false
} else if config_file_count == 0 && cli.config_server.is_none() {
true
} else {
cli.network_options.network_name.is_some()
}
};
for (config_file, source) in config_files {
let (cfg, mut control) = load_config_from_file(
&config_file,
cli.config_dir.as_ref(),
cli.disable_env_parsing,
)
.await?;
if cli.network_options.can_merge(
&cfg,
source,
explicit_config_file_count,
config_dir_file_count,
) {
cli.network_options
.merge_into(&cfg)
.with_context(|| format!("failed to merge config from cli: {:?}", config_file))?;
crate_cli_network = false;
control.set_read_only(true);
control.set_no_delete(true);
}
log::info!(
"\
Starting easytier from config file {:?}({:?}) with config:\n\
############### TOML ###############\n\
{}\n\
-----------------------------------\n\
",
config_file,
control.permission,
cfg.dump()
);
manager.run_network_instance(cfg, control)?;
}
if crate_cli_network {
let cfg = TomlConfigLoader::default();
cli.network_options
.merge_into(&cfg)
.with_context(|| "failed to create config from cli".to_string())?;
log::info!(
"\
Starting easytier from cli with config:\n\
############### TOML ###############\n\
{}\n\
-----------------------------------\n\
",
cfg.dump()
);
manager.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)?;
}
#[cfg(unix)]
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
#[cfg(unix)]
let sigterm = sigterm.recv();
#[cfg(not(unix))]
let sigterm = std::future::pending::<()>();
tokio::select! {
_ = manager.wait() => {
let infos = manager.collect_network_infos().await?;
if infos
.into_values()
.filter_map(|info| info.error_msg).next().is_some() {
return Err(anyhow::anyhow!("some instances stopped with errors"));
}
}
_ = tokio::signal::ctrl_c() => {
log::info!("ctrl-c received, exiting...");
}
_ = sigterm, if cfg!(unix) => {
log::warn!("terminate signal received, exiting...");
}
}
Ok(())
}
fn memory_monitor(_force_dump: Arc<AtomicBool>) {
#[cfg(feature = "jemalloc-prof")]
{
let mut last_peak_size = 0;
let e = epoch::mib().unwrap();
let allocated_stats = stats::allocated::mib().unwrap();
loop {
e.advance().unwrap();
let new_heap_size = allocated_stats.read().unwrap();
log::debug!("heap size: {} bytes", new_heap_size);
// dump every 75MB
if (last_peak_size > 0
&& new_heap_size > last_peak_size
&& new_heap_size - last_peak_size > 10 * 1024 * 1024)
|| _force_dump.load(std::sync::atomic::Ordering::Relaxed)
{
log::debug!(
"heap size increased: {} bytes",
new_heap_size - last_peak_size,
);
dump_profile(new_heap_size);
last_peak_size = new_heap_size;
if _force_dump.load(std::sync::atomic::Ordering::Relaxed) {
// also dump whole jemalloc stats
use jemalloc_ctl::stats_print::stats_print;
let tmp_file = get_dump_profile_path(new_heap_size, "stats");
let mut file = std::fs::File::create(tmp_file).unwrap();
let _ = stats_print(&mut file, Default::default());
_force_dump.store(false, std::sync::atomic::Ordering::Relaxed);
}
}
if last_peak_size == 0 {
last_peak_size = new_heap_size;
}
std::thread::sleep(std::time::Duration::from_secs(5));
}
}
}
pub async fn main() -> ExitCode {
let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
rust_i18n::set_locale(&locale);
setup_panic_handler();
#[cfg(target_os = "windows")]
match windows_service::service_dispatcher::start(String::new(), ffi_service_main) {
Ok(_) => std::thread::park(),
Err(e) => {
let should_panic = if let windows_service::Error::Winapi(ref io_error) = e {
io_error.raw_os_error() != Some(0x427) // ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
} else {
true
};
if should_panic {
panic!("SCM start an error: {}", e);
}
}
};
set_prof_active(true);
// register a signal handler to set force dump when signal usr1 is received
let force_dump = Arc::new(AtomicBool::new(false));
#[cfg(all(feature = "jemalloc-prof", not(target_os = "windows")))]
{
let force_dump_clone = force_dump.clone();
let mut sigusr1 =
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::user_defined1()).unwrap();
tokio::task::spawn(async move {
while sigusr1.recv().await.is_some() {
force_dump_clone.store(true, std::sync::atomic::Ordering::Relaxed);
}
});
}
let _monitor = std::thread::spawn(move || memory_monitor(force_dump));
let cli = parse_cli();
if let Some(shell) = cli.gen_autocomplete {
let mut cmd = Cli::command();
if let Some(shell) = shell.to_shell() {
crate::print_completions(shell, &mut cmd, "easytier-core");
} else {
// Handle Nushell
crate::print_nushell_completions(&mut cmd, "easytier-core");
}
return ExitCode::SUCCESS;
}
// Verify configurations
if cli.check_config {
if let Err(error) = validate_config(&cli).await {
log::error!(%error, "Config validation failed");
return ExitCode::FAILURE;
} else {
return ExitCode::SUCCESS;
}
}
let mut ret_code = 0;
if let Err(error) = run_main(cli).await {
log::error!(%error);
ret_code = 1;
}
log::info!("Stopping easytier...");
set_prof_active(false);
ExitCode::from(ret_code)
}
async fn validate_config(cli: &Cli) -> anyhow::Result<()> {
// Check if a config file is provided
let config_files = cli
.config_file
.as_ref()
.ok_or_else(|| anyhow::anyhow!("--config-file is required when using --check-config"))?;
for config_file in config_files {
if config_file == &PathBuf::from("-") {
let mut stdin = String::new();
_ = tokio::io::stdin()
.read_to_string(&mut stdin)
.await
.context("failed to read config from stdin")?;
TomlConfigLoader::new_from_str_with_source("stdin", stdin.as_str())?;
} else {
load_toml_config_from_path(config_file)?;
};
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_listeners() {
type IpSchemeMap = fn(&IpScheme) -> String;
let cases: [(&str, IpSchemeMap); _] = [
("0", |s| format!("{}://0.0.0.0:0", s)),
("11010", |s| {
format!("{}://0.0.0.0:{}", s, 11010 + s.port_offset())
}),
("1.1.1.1", |s| {
format!("{}://1.1.1.1:{}", s, 11010 + s.port_offset())
}),
("1.1.1.1:50000", |s| {
format!("{}://1.1.1.1:{}", s, 50000 + s.port_offset())
}),
("[::1]", |s| {
format!("{}://[::1]:{}", s, 11010 + s.port_offset())
}),
("[::1]:50000", |s| {
format!("{}://[::1]:{}", s, 50000 + s.port_offset())
}),
];
for (input, output) in cases {
assert_eq!(
Cli::parse_listeners(false, vec![input.to_string()]).unwrap(),
IpScheme::VARIANTS.iter().map(output).collect::<Vec<_>>()
);
}
let input = cases.iter().map(|(i, _)| i.to_string()).collect::<Vec<_>>();
let output = cases
.iter()
.flat_map(|(_, o)| IpScheme::VARIANTS.iter().map(o))
.collect::<Vec<_>>();
assert_eq!(Cli::parse_listeners(false, input).unwrap(), output);
let cases: [(IpSchemeMap, IpSchemeMap); _] = [
(
|s| format!("{}", s),
|s| format!("{}://0.0.0.0:{}", s, 11010 + s.port_offset()),
),
(
|s| format!("{}:50000", s),
|s| format!("{}://0.0.0.0:50000", s),
),
(
|s| format!("{}://1.1.1.1:50000", s),
|s| format!("{}://1.1.1.1:50000", s),
),
];
for (input, output) in cases {
assert_eq!(
Cli::parse_listeners(
false,
IpScheme::VARIANTS.iter().map(input).collect::<Vec<_>>(),
)
.unwrap(),
IpScheme::VARIANTS.iter().map(output).collect::<Vec<_>>()
);
}
let input = cases
.iter()
.flat_map(|(i, _)| IpScheme::VARIANTS.iter().map(i))
.collect::<Vec<_>>();
let output = cases
.iter()
.flat_map(|(_, o)| IpScheme::VARIANTS.iter().map(o))
.collect::<Vec<_>>();
assert_eq!(Cli::parse_listeners(false, input).unwrap(), output);
let cases = ["tcp://[::1", "xxx", "tcp:/abc", "tcp:abc"];
for input in cases {
assert!(
Cli::parse_listeners(false, vec![input.to_string()]).is_err(),
"input: {}",
input
);
}
}
#[test]
fn test_network_options_merge_preserves_credential_identity() {
let cfg = TomlConfigLoader::new_from_str(
r#"
[network_identity]
network_name = "credential-network"
network_secret = ""
[secure_mode]
enabled = true
"#,
)
.unwrap();
assert_eq!(cfg.get_network_identity().network_secret, None);
NetworkOptions {
hostname: Some("override-host".to_string()),
..Default::default()
}
.merge_into(&cfg)
.unwrap();
let identity = cfg.get_network_identity();
assert_eq!(identity.network_name, "credential-network");
assert_eq!(identity.network_secret, None);
assert_eq!(identity.network_secret_digest, None);
assert_eq!(cfg.get_hostname(), "override-host");
}
}