use crate::common::config::{ ConfigFileControl, ConfigSource, PortForwardConfig, parse_mapped_listener_urls, process_secure_mode_cfg, }; use crate::proto::api::{self, manage}; use crate::proto::rpc_types::controller::BaseController; use crate::rpc_service::InstanceRpcService; use crate::{ common::{ config::{ ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader, VpnPortalConfig, gen_default_flags, }, constants::EASYTIER_VERSION, global_ctx::{EventBusSubscriber, GlobalCtxEvent}, }, instance::instance::Instance, proto::api::instance::list_peer_route_pair, }; use anyhow::Context; use chrono::{DateTime, Local}; use std::{ collections::VecDeque, net::SocketAddr, sync::{Arc, Mutex, RwLock, atomic::AtomicBool}, }; use tokio::{ sync::{broadcast, mpsc}, task::JoinSet, }; pub type MyNodeInfo = crate::proto::api::manage::MyNodeInfo; type ArcMutApiService = Arc>>>; type TunFd = Option; #[derive(serde::Serialize, Clone)] pub struct Event { time: DateTime, event: GlobalCtxEvent, } struct EasyTierData { events: RwLock>, tun_fd: (mpsc::Sender, Mutex>>), event_subscriber: RwLock>, instance_stop_notifier: Arc, } impl Default for EasyTierData { fn default() -> Self { let (tx, _) = broadcast::channel(16); let (sender, receiver) = mpsc::channel(16); Self { event_subscriber: RwLock::new(tx), events: RwLock::new(VecDeque::new()), tun_fd: (sender, Mutex::new(Some(receiver))), instance_stop_notifier: Arc::new(tokio::sync::Notify::new()), } } } pub struct EasyTierLauncher { instance_alive: Arc, stop_flag: Arc, thread_handle: Option>, api_service: ArcMutApiService, running_cfg: String, error_msg: Arc>>, data: Arc, } impl EasyTierLauncher { pub fn new() -> Self { let instance_alive = Arc::new(AtomicBool::new(false)); Self { instance_alive, thread_handle: None, api_service: Arc::new(RwLock::new(None)), error_msg: Arc::new(RwLock::new(None)), running_cfg: String::new(), stop_flag: Arc::new(AtomicBool::new(false)), data: Arc::new(EasyTierData::default()), } } async fn handle_easytier_event(event: GlobalCtxEvent, data: &EasyTierData) { let mut events = data.events.write().unwrap(); let _ = data.event_subscriber.read().unwrap().send(event.clone()); events.push_front(Event { time: chrono::Local::now(), event, }); if events.len() > 20 { events.pop_back(); } } #[cfg(mobile)] async fn run_routine_for_mobile( instance: &Instance, data: &EasyTierData, tasks: &mut JoinSet<()>, ) { let global_ctx = instance.get_global_ctx(); let peer_mgr = instance.get_peer_manager(); let nic_ctx = instance.get_nic_ctx(); let peer_packet_receiver = instance.get_peer_packet_receiver(); let mut tun_fd_receiver = data.tun_fd.1.lock().unwrap().take().unwrap(); tasks.spawn(async move { loop { let Some(tun_fd) = tun_fd_receiver.recv().await.flatten() else { return; }; let res = Instance::setup_nic_ctx_for_mobile( nic_ctx.clone(), global_ctx.clone(), peer_mgr.clone(), peer_packet_receiver.clone(), tun_fd, ) .await; } }); } async fn easytier_routine( cfg: TomlConfigLoader, stop_signal: Arc, api_service: ArcMutApiService, data: Arc, ) -> Result<(), anyhow::Error> { let mut instance = Instance::new(cfg); let mut tasks = JoinSet::new(); // Subscribe to global context events let global_ctx = instance.get_global_ctx(); let data_c = data.clone(); tasks.spawn(async move { let mut receiver = global_ctx.subscribe(); loop { match receiver.recv().await { Ok(event) => { Self::handle_easytier_event(event.clone(), &data_c).await; } Err(broadcast::error::RecvError::Closed) => { break; } Err(broadcast::error::RecvError::Lagged(_)) => { // do nothing currently receiver = receiver.resubscribe(); } } } }); #[cfg(mobile)] Self::run_routine_for_mobile(&instance, &data, &mut tasks).await; instance.run().await?; api_service .write() .unwrap() .replace(Arc::new(instance.get_api_rpc_service())); drop(api_service); stop_signal.notified().await; tasks.abort_all(); drop(tasks); instance.clear_resources().await; drop(instance); Ok(()) } pub fn start(&mut self, cfg_generator: F) where F: FnOnce() -> Result + Send + Sync, { let error_msg = self.error_msg.clone(); let cfg = match cfg_generator() { Err(e) => { error_msg.write().unwrap().replace(e.to_string()); return; } Ok(cfg) => cfg, }; self.running_cfg = cfg.dump(); let stop_flag = self.stop_flag.clone(); let instance_alive = self.instance_alive.clone(); instance_alive.store(true, std::sync::atomic::Ordering::Relaxed); let data = self.data.clone(); let api_service = self.api_service.clone(); self.thread_handle = Some(std::thread::spawn(move || { let rt = if cfg.get_flags().multi_thread { let worker_threads = 2.max(cfg.get_flags().multi_thread_count as usize); tokio::runtime::Builder::new_multi_thread() .worker_threads(worker_threads) .enable_all() .build() } else { tokio::runtime::Builder::new_current_thread() .enable_all() .build() } .unwrap(); let stop_notifier = Arc::new(tokio::sync::Notify::new()); let stop_notifier_clone = stop_notifier.clone(); rt.spawn(async move { while !stop_flag.load(std::sync::atomic::Ordering::Relaxed) { tokio::time::sleep(std::time::Duration::from_millis(100)).await; } stop_notifier_clone.notify_one(); }); let notifier = data.instance_stop_notifier.clone(); let ret = rt.block_on(Self::easytier_routine( cfg, stop_notifier, api_service, data, )); if let Err(e) = ret { error_msg.write().unwrap().replace(format!("{:?}", e)); } instance_alive.store(false, std::sync::atomic::Ordering::Relaxed); notifier.notify_one(); rt.shutdown_background(); })); } pub fn error_msg(&self) -> Option { self.error_msg.read().unwrap().clone() } pub fn running(&self) -> bool { self.instance_alive .load(std::sync::atomic::Ordering::Relaxed) } pub fn get_events(&self) -> Vec { let events = self.data.events.read().unwrap(); events.iter().cloned().collect() } pub fn get_api_service(&self) -> Option> { match self.api_service.read() { Ok(guard) => guard.clone(), Err(e) => { tracing::error!("Failed to acquire read lock for api_service: {:?}", e); None } } } } impl Default for EasyTierLauncher { fn default() -> Self { Self::new() } } impl Drop for EasyTierLauncher { fn drop(&mut self) { self.stop_flag .store(true, std::sync::atomic::Ordering::Relaxed); if let Some(handle) = self.thread_handle.take() && let Err(e) = handle.join() { println!("Error when joining thread: {:?}", e); } } } pub type NetworkInstanceRunningInfo = crate::proto::api::manage::NetworkInstanceRunningInfo; pub struct NetworkInstance { config: TomlConfigLoader, launcher: Option, config_file_control: ConfigFileControl, } impl NetworkInstance { pub fn new(config: TomlConfigLoader, config_file_control: ConfigFileControl) -> Self { Self { config, launcher: None, config_file_control, } } pub fn is_easytier_running(&self) -> bool { self.launcher.is_some() && self.launcher.as_ref().unwrap().running() } pub async fn get_running_info(&self) -> anyhow::Result { let launcher = self.launcher.as_ref().ok_or_else(|| { anyhow::anyhow!("instance is not running, please start the instance first") })?; let api_service = self.get_api_service().ok_or_else(|| { anyhow::anyhow!("failed to get api service, instance may not be running") })?; let ctrl = BaseController::default(); let peers = api_service .get_peer_manage_service() .list_peer(ctrl.clone(), api::instance::ListPeerRequest::default()) .await? .peer_infos; let my_info = api_service .get_peer_manage_service() .show_node_info(ctrl.clone(), api::instance::ShowNodeInfoRequest::default()) .await? .node_info .ok_or_else(|| anyhow::anyhow!("failed to get my node info"))?; let vpn_portal_cfg = api_service .get_vpn_portal_service() .get_vpn_portal_info( ctrl.clone(), api::instance::GetVpnPortalInfoRequest::default(), ) .await? .vpn_portal_info .map(|i| i.client_config); let routes = api_service .get_peer_manage_service() .list_route(ctrl.clone(), api::instance::ListRouteRequest::default()) .await? .routes; let peer_route_pairs = list_peer_route_pair(peers.clone(), routes.clone()); let foreign_network_summary = api_service .get_peer_manage_service() .get_foreign_network_summary( ctrl.clone(), api::instance::GetForeignNetworkSummaryRequest::default(), ) .await? .summary; let dev_name = api_service .get_config_service() .get_config(ctrl.clone(), api::config::GetConfigRequest::default()) .await? .config .ok_or_else(|| anyhow::anyhow!("failed to get config"))? .dev_name .unwrap_or_else(|| "".to_string()); Ok(NetworkInstanceRunningInfo { dev_name, my_node_info: Some(MyNodeInfo { virtual_ipv4: my_info .ipv4_addr .parse::() .ok() .map(Into::into), hostname: my_info.hostname, version: EASYTIER_VERSION.to_string(), ips: my_info.ip_list, stun_info: my_info.stun_info, listeners: my_info .listeners .into_iter() .map(|s| s.parse::().unwrap().into()) .collect(), vpn_portal_cfg, peer_id: my_info.peer_id, }), events: launcher .get_events() .iter() .map(|e| serde_json::to_string(e).unwrap()) .collect(), routes, peers, peer_route_pairs, running: launcher.running(), error_msg: launcher.error_msg(), foreign_network_summary, }) } pub fn get_inst_name(&self) -> String { self.config.get_inst_name() } pub fn get_network_name(&self) -> String { self.config.get_network_identity().network_name } pub fn get_tun_fd_sender(&self) -> Option> { self.launcher .as_ref() .map(|launcher| launcher.data.tun_fd.0.clone()) } pub fn start(&mut self) -> Result { if self.is_easytier_running() { return Ok(self.subscribe_event().unwrap()); } let launcher = EasyTierLauncher::new(); self.launcher = Some(launcher); let ev = self.subscribe_event().unwrap(); self.launcher .as_mut() .unwrap() .start(|| Ok(self.config.clone())); Ok(ev) } pub fn subscribe_event(&self) -> Option> { self.launcher .as_ref() .map(|launcher| launcher.data.event_subscriber.read().unwrap().subscribe()) } pub fn get_stop_notifier(&self) -> Option> { self.launcher .as_ref() .map(|launcher| launcher.data.instance_stop_notifier.clone()) } pub fn get_config_file_control(&self) -> &ConfigFileControl { &self.config_file_control } pub fn get_network_config_source(&self) -> ConfigSource { self.config.get_network_config_source() } pub fn get_latest_error_msg(&self) -> Option { if let Some(launcher) = self.launcher.as_ref() { launcher.error_msg.read().unwrap().clone() } else { None } } pub fn get_api_service(&self) -> Option> { self.launcher .as_ref() .and_then(|launcher| launcher.get_api_service()) } } pub fn add_proxy_network_to_config( proxy_network: &str, cfg: &TomlConfigLoader, ) -> Result<(), anyhow::Error> { let parts: Vec<&str> = proxy_network.split("->").collect(); let real_cidr = parts[0] .parse() .with_context(|| format!("failed to parse proxy network: {}", parts[0]))?; if parts.len() > 2 { return Err(anyhow::anyhow!( "invalid proxy network format: {}, support format: or ->, example: 10.0.0.0/24 or 10.0.0.0/24->192.168.0.0/24", proxy_network )); } let mapped_cidr = if parts.len() == 2 { Some( parts[1] .parse() .with_context(|| format!("failed to parse mapped network: {}", parts[1]))?, ) } else { None }; cfg.add_proxy_cidr(real_cidr, mapped_cidr)?; Ok(()) } pub type NetworkingMethod = crate::proto::api::manage::NetworkingMethod; pub type NetworkConfig = crate::proto::api::manage::NetworkConfig; impl NetworkConfig { pub fn gen_config(&self) -> Result { let cfg = TomlConfigLoader::default(); cfg.set_id( self.instance_id .clone() .unwrap_or(uuid::Uuid::new_v4().to_string()) .parse() .with_context(|| format!("failed to parse instance id: {:?}", self.instance_id))?, ); cfg.set_hostname(self.hostname.clone()); cfg.set_dhcp(self.dhcp.unwrap_or_default()); cfg.set_inst_name(self.network_name.clone().unwrap_or_default()); // The web UI does not expose credential inputs directly, but imported/saved // NetworkConfig objects still need to preserve credential-mode instances via // secure_mode.local_private_key + empty network_secret. let credential_secret = if self.network_secret.is_some() { None } else { self.secure_mode .as_ref() .and_then(|mode| mode.local_private_key.clone()) .filter(|s| !s.is_empty()) }; if credential_secret.is_some() { cfg.set_network_identity(NetworkIdentity::new_credential( self.network_name.clone().unwrap_or_default(), )); } else { cfg.set_network_identity(NetworkIdentity::new( self.network_name.clone().unwrap_or_default(), self.network_secret.clone().unwrap_or_default(), )); } if !cfg.get_dhcp() { let virtual_ipv4 = self.virtual_ipv4.clone().unwrap_or_default(); if !virtual_ipv4.is_empty() { let ip = format!("{}/{}", virtual_ipv4, self.network_length.unwrap_or(24)) .parse() .with_context(|| { format!( "failed to parse ipv4 inet address: {}, {:?}", virtual_ipv4, self.network_length ) })?; cfg.set_ipv4(Some(ip)); } } match NetworkingMethod::try_from(self.networking_method.unwrap_or_default()) .unwrap_or_default() { NetworkingMethod::PublicServer => { let public_server_url = self.public_server_url.clone().unwrap_or_default(); cfg.set_peers(vec![PeerConfig { uri: public_server_url.parse().with_context(|| { format!("failed to parse public server uri: {}", public_server_url) })?, peer_public_key: None, }]); } NetworkingMethod::Manual => { let mut peers = vec![]; for peer_url in self.peer_urls.iter() { if peer_url.is_empty() { continue; } peers.push(PeerConfig { uri: peer_url .parse() .with_context(|| format!("failed to parse peer uri: {}", peer_url))?, peer_public_key: None, }); } if !peers.is_empty() { cfg.set_peers(peers); } } NetworkingMethod::Standalone => {} } let mut listener_urls = vec![]; for listener_url in self.listener_urls.iter() { if listener_url.is_empty() { continue; } listener_urls.push( listener_url .parse() .with_context(|| format!("failed to parse listener uri: {}", listener_url))?, ); } cfg.set_listeners(listener_urls); for n in self.proxy_cidrs.iter() { add_proxy_network_to_config(n, &cfg)?; } if !self.port_forwards.is_empty() { cfg.set_port_forwards( self.port_forwards .iter() .filter(|pf| !pf.bind_ip.is_empty() && !pf.dst_ip.is_empty()) .filter_map(|pf| { let bind_addr = format!("{}:{}", pf.bind_ip, pf.bind_port).parse::(); let dst_addr = format!("{}:{}", pf.dst_ip, pf.dst_port).parse::(); match (bind_addr, dst_addr) { (Ok(bind_addr), Ok(dst_addr)) => Some(PortForwardConfig { bind_addr, dst_addr, proto: pf.proto.clone(), }), _ => None, } }) .collect::>(), ); } if self.enable_vpn_portal.unwrap_or_default() { let cidr = format!( "{}/{}", self.vpn_portal_client_network_addr .clone() .unwrap_or_default(), self.vpn_portal_client_network_len.unwrap_or(24) ); cfg.set_vpn_portal_config(VpnPortalConfig { client_cidr: cidr .parse() .with_context(|| format!("failed to parse vpn portal client cidr: {}", cidr))?, wireguard_listen: format!( "0.0.0.0:{}", self.vpn_portal_listen_port.unwrap_or_default() ) .parse() .with_context(|| { format!( "failed to parse vpn portal wireguard listen port. {:?}", self.vpn_portal_listen_port ) })?, }); } if self.enable_manual_routes.unwrap_or_default() { let mut routes = Vec::::with_capacity(self.routes.len()); for route in self.routes.iter() { routes.push( route .parse() .with_context(|| format!("failed to parse route: {}", route))?, ); } cfg.set_routes(Some(routes)); } if !self.exit_nodes.is_empty() { let mut exit_nodes = Vec::::with_capacity(self.exit_nodes.len()); for node in self.exit_nodes.iter() { exit_nodes.push( node.parse() .with_context(|| format!("failed to parse exit node: {}", node))?, ); } cfg.set_exit_nodes(exit_nodes); } if self.enable_socks5.unwrap_or_default() && let Some(socks5_port) = self.socks5_port { cfg.set_socks5_portal(Some( format!("socks5://0.0.0.0:{}", socks5_port).parse().unwrap(), )); } if !self.mapped_listeners.is_empty() { let mapped_listeners = parse_mapped_listener_urls(&self.mapped_listeners)?; cfg.set_mapped_listeners(Some(mapped_listeners)); } if let Some(credential_file) = self .credential_file .as_ref() .filter(|path| !path.is_empty()) { cfg.set_credential_file(Some(credential_file.into())); } if let Some(credential_secret) = credential_secret { cfg.set_secure_mode(Some(process_secure_mode_cfg( crate::proto::common::SecureModeConfig { enabled: true, local_private_key: Some(credential_secret), local_public_key: None, }, )?)); } else { cfg.set_secure_mode( self.secure_mode .clone() .map(process_secure_mode_cfg) .transpose()?, ); } let mut flags = gen_default_flags(); if let Some(latency_first) = self.latency_first { flags.latency_first = latency_first; } if let Some(dev_name) = self.dev_name.clone() { flags.dev_name = dev_name; } if let Some(use_smoltcp) = self.use_smoltcp { flags.use_smoltcp = use_smoltcp; } if let Some(ipv6_public_addr_provider) = self.ipv6_public_addr_provider { cfg.set_ipv6_public_addr_provider(ipv6_public_addr_provider); } if let Some(ipv6_public_addr_auto) = self.ipv6_public_addr_auto { cfg.set_ipv6_public_addr_auto(ipv6_public_addr_auto); } if let Some(ipv6_public_addr_prefix) = self .ipv6_public_addr_prefix .as_ref() .filter(|prefix| !prefix.is_empty()) { cfg.set_ipv6_public_addr_prefix(Some(ipv6_public_addr_prefix.parse().with_context( || format!("failed to parse ipv6 public address prefix: {ipv6_public_addr_prefix}"), )?)); } if let Some(disable_ipv6) = self.disable_ipv6 { flags.enable_ipv6 = !disable_ipv6; } if let Some(enable_kcp_proxy) = self.enable_kcp_proxy { flags.enable_kcp_proxy = enable_kcp_proxy; } if let Some(disable_kcp_input) = self.disable_kcp_input { flags.disable_kcp_input = disable_kcp_input; } if let Some(enable_quic_proxy) = self.enable_quic_proxy { flags.enable_quic_proxy = enable_quic_proxy; } if let Some(disable_quic_input) = self.disable_quic_input { flags.disable_quic_input = disable_quic_input; } if let Some(disable_p2p) = self.disable_p2p { flags.disable_p2p = disable_p2p; } if let Some(p2p_only) = self.p2p_only { flags.p2p_only = p2p_only; } if let Some(lazy_p2p) = self.lazy_p2p { flags.lazy_p2p = lazy_p2p; } if let Some(bind_device) = self.bind_device { flags.bind_device = bind_device; } if let Some(no_tun) = self.no_tun { flags.no_tun = no_tun; } if let Some(enable_exit_node) = self.enable_exit_node { flags.enable_exit_node = enable_exit_node; } if let Some(relay_all_peer_rpc) = self.relay_all_peer_rpc { flags.relay_all_peer_rpc = relay_all_peer_rpc; } if let Some(need_p2p) = self.need_p2p { flags.need_p2p = need_p2p; } if let Some(multi_thread) = self.multi_thread { flags.multi_thread = multi_thread; } if let Some(proxy_forward_by_system) = self.proxy_forward_by_system { flags.proxy_forward_by_system = proxy_forward_by_system; } if let Some(disable_encryption) = self.disable_encryption { flags.enable_encryption = !disable_encryption; } if self.enable_relay_network_whitelist.unwrap_or_default() { if !self.relay_network_whitelist.is_empty() { flags.relay_network_whitelist = self.relay_network_whitelist.join(" "); } else { flags.relay_network_whitelist = "".to_string(); } } if let Some(disable_tcp_hole_punching) = self.disable_tcp_hole_punching { flags.disable_tcp_hole_punching = disable_tcp_hole_punching; } if let Some(disable_udp_hole_punching) = self.disable_udp_hole_punching { flags.disable_udp_hole_punching = disable_udp_hole_punching; } if let Some(disable_upnp) = self.disable_upnp { flags.disable_upnp = disable_upnp; } if let Some(disable_sym_hole_punching) = self.disable_sym_hole_punching { flags.disable_sym_hole_punching = disable_sym_hole_punching; } if let Some(enable_magic_dns) = self.enable_magic_dns { flags.accept_dns = enable_magic_dns; } if let Some(mtu) = self.mtu { flags.mtu = mtu as u32; } if let Some(instance_recv_bps_limit) = self.instance_recv_bps_limit { flags.instance_recv_bps_limit = instance_recv_bps_limit; } if let Some(enable_private_mode) = self.enable_private_mode { flags.private_mode = enable_private_mode; } if let Some(encryption_algorithm) = self.encryption_algorithm.clone() { flags.encryption_algorithm = encryption_algorithm; } if let Some(acl) = self.acl.as_ref() && !acl.is_empty() { cfg.set_acl(Some(acl.clone())); } if let Some(data_compress_algo) = self.data_compress_algo { if data_compress_algo < 1 { flags.data_compress_algo = 1; } else { flags.data_compress_algo = data_compress_algo } } cfg.set_flags(flags); Ok(cfg) } pub fn new_from_config(config: impl ConfigLoader) -> Result { let default_config = TomlConfigLoader::default(); let mut result = Self { ..Default::default() }; result.instance_id = Some(config.get_id().to_string()); if config.get_hostname() != default_config.get_hostname() { result.hostname = Some(config.get_hostname()); } result.dhcp = Some(config.get_dhcp()); let network_identity = config.get_network_identity(); result.network_name = Some(network_identity.network_name.clone()); result.network_secret = network_identity.network_secret; if let Some(ipv4) = config.get_ipv4() { result.virtual_ipv4 = Some(ipv4.address().to_string()); result.network_length = Some(ipv4.network_length() as i32); } if config.get_ipv6_public_addr_provider() != default_config.get_ipv6_public_addr_provider() { result.ipv6_public_addr_provider = Some(config.get_ipv6_public_addr_provider()); } if config.get_ipv6_public_addr_auto() != default_config.get_ipv6_public_addr_auto() { result.ipv6_public_addr_auto = Some(config.get_ipv6_public_addr_auto()); } result.ipv6_public_addr_prefix = config .get_ipv6_public_addr_prefix() .map(|prefix| prefix.to_string()); let peers = config.get_peers(); result.networking_method = Some(NetworkingMethod::Manual as i32); if !peers.is_empty() { result.peer_urls = peers.iter().map(|p| p.uri.to_string()).collect(); } result.listener_urls = config .get_listeners() .unwrap_or_default() .iter() .map(|l| l.to_string()) .collect(); result.proxy_cidrs = config .get_proxy_cidrs() .iter() .map(|c| { if let Some(mapped) = c.mapped_cidr { format!("{}->{}", c.cidr, mapped) } else { c.cidr.to_string() } }) .collect(); let port_forwards = config.get_port_forwards(); if !port_forwards.is_empty() { result.port_forwards = port_forwards .iter() .map(|f| manage::PortForwardConfig { proto: f.proto.clone(), bind_ip: f.bind_addr.ip().to_string(), bind_port: f.bind_addr.port() as u32, dst_ip: f.dst_addr.ip().to_string(), dst_port: f.dst_addr.port() as u32, }) .collect(); } if let Some(vpn_config) = config.get_vpn_portal_config() { result.enable_vpn_portal = Some(true); let cidr = vpn_config.client_cidr; result.vpn_portal_client_network_addr = Some(cidr.first_address().to_string()); result.vpn_portal_client_network_len = Some(cidr.network_length() as i32); result.vpn_portal_listen_port = Some(vpn_config.wireguard_listen.port() as i32); } if let Some(routes) = config.get_routes() && !routes.is_empty() { result.enable_manual_routes = Some(true); result.routes = routes.iter().map(|r| r.to_string()).collect(); } let exit_nodes = config.get_exit_nodes(); if !exit_nodes.is_empty() { result.exit_nodes = exit_nodes.iter().map(|n| n.to_string()).collect(); } if let Some(socks5_portal) = config.get_socks5_portal() { result.enable_socks5 = Some(true); result.socks5_port = socks5_portal.port().map(|p| p as i32); } let mapped_listeners = config.get_mapped_listeners(); if !mapped_listeners.is_empty() { result.mapped_listeners = mapped_listeners.iter().map(|l| l.to_string()).collect(); } result.secure_mode = config.get_secure_mode(); result.credential_file = config .get_credential_file() .map(|path| path.to_string_lossy().into_owned()); let flags = config.get_flags(); result.latency_first = Some(flags.latency_first); result.dev_name = Some(flags.dev_name.clone()); result.use_smoltcp = Some(flags.use_smoltcp); result.disable_ipv6 = Some(!flags.enable_ipv6); result.enable_kcp_proxy = Some(flags.enable_kcp_proxy); result.disable_kcp_input = Some(flags.disable_kcp_input); result.enable_quic_proxy = Some(flags.enable_quic_proxy); result.disable_quic_input = Some(flags.disable_quic_input); result.disable_p2p = Some(flags.disable_p2p); result.p2p_only = Some(flags.p2p_only); result.lazy_p2p = Some(flags.lazy_p2p); result.bind_device = Some(flags.bind_device); result.no_tun = Some(flags.no_tun); result.enable_exit_node = Some(flags.enable_exit_node); result.relay_all_peer_rpc = Some(flags.relay_all_peer_rpc); result.need_p2p = Some(flags.need_p2p); result.multi_thread = Some(flags.multi_thread); result.proxy_forward_by_system = Some(flags.proxy_forward_by_system); result.disable_encryption = Some(!flags.enable_encryption); result.disable_tcp_hole_punching = Some(flags.disable_tcp_hole_punching); result.disable_udp_hole_punching = Some(flags.disable_udp_hole_punching); result.disable_upnp = Some(flags.disable_upnp); result.disable_sym_hole_punching = Some(flags.disable_sym_hole_punching); result.enable_magic_dns = Some(flags.accept_dns); result.mtu = Some(flags.mtu as i32); result.instance_recv_bps_limit = (flags.instance_recv_bps_limit != u64::MAX).then_some(flags.instance_recv_bps_limit); result.enable_private_mode = Some(flags.private_mode); result.acl = config.get_acl(); if flags.relay_network_whitelist == "*" { result.enable_relay_network_whitelist = Some(false); } else { result.enable_relay_network_whitelist = Some(true); if flags.relay_network_whitelist.is_empty() { result.relay_network_whitelist = vec![]; } else { result.relay_network_whitelist = flags .relay_network_whitelist .split_whitespace() .map(|s| s.to_string()) .collect(); } } Ok(result) } } #[cfg(test)] mod tests { use crate::{ common::config::{ConfigLoader, process_secure_mode_cfg}, proto::common::SecureModeConfig, }; use base64::prelude::{BASE64_STANDARD, Engine as _}; use rand::Rng; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; fn gen_default_config() -> crate::common::config::TomlConfigLoader { let config = crate::common::config::TomlConfigLoader::default(); config.set_id(uuid::Uuid::new_v4()); config.set_dhcp(false); config.set_inst_name("default".to_string()); config.set_listeners(vec![]); config } #[test] fn test_network_config_conversion_basic() -> Result<(), anyhow::Error> { let config = gen_default_config(); let network_config = super::NetworkConfig::new_from_config(&config)?; let generated_config = network_config.gen_config()?; let config_str = config.dump(); let generated_config_str = generated_config.dump(); assert_eq!( config_str, generated_config_str, "Generated config does not match original config:\nOriginal:\n{}\n\nGenerated:\n{}\nNetwork Config: {}\n", config_str, generated_config_str, serde_json::to_string(&network_config).unwrap() ); Ok(()) } #[test] fn test_network_config_conversion_random() -> Result<(), anyhow::Error> { let mut rng = rand::thread_rng(); for _ in 0..100 { let config = gen_default_config(); config.set_id(uuid::Uuid::new_v4()); config.set_dhcp(rng.gen_bool(0.5)); if rng.gen_bool(0.7) { let hostname = format!("host-{}", rng.r#gen::()); config.set_hostname(Some(hostname)); } config.set_network_identity(crate::common::config::NetworkIdentity::new( format!("network-{}", rng.r#gen::()), format!("secret-{}", rng.r#gen::()), )); config.set_inst_name(config.get_network_identity().network_name.clone()); if !config.get_dhcp() { let addr = Ipv4Addr::new( rng.gen_range(1..254), rng.gen_range(0..255), rng.gen_range(0..255), rng.gen_range(1..254), ); let prefix_len = rng.gen_range(1..31); let ipv4 = format!("{}/{}", addr, prefix_len).parse().unwrap(); config.set_ipv4(Some(ipv4)); } let peer_count = rng.gen_range(0..3); let mut peers = Vec::new(); for _ in 0..peer_count { let port = rng.gen_range(10000..60000); let protocol = if rng.gen_bool(0.5) { "tcp" } else { "udp" }; let uri = format!("{}://127.0.0.1:{}", protocol, port) .parse() .unwrap(); peers.push(crate::common::config::PeerConfig { uri, peer_public_key: None, }); } config.set_peers(peers); if rng.gen_bool(0.7) { let listener_count = rng.gen_range(0..3); let mut listeners = Vec::new(); for _ in 0..listener_count { let port = rng.gen_range(10000..60000); let protocol = if rng.gen_bool(0.5) { "tcp" } else { "udp" }; listeners.push(format!("{}://0.0.0.0:{}", protocol, port).parse().unwrap()); } config.set_listeners(listeners); } if rng.gen_bool(0.6) { let proxy_count = rng.gen_range(0..3); for _ in 0..proxy_count { let network = format!( "{}.{}.{}.0/{}", rng.gen_range(1..254), rng.gen_range(0..255), rng.gen_range(0..255), rng.gen_range(24..30) ) .parse::() .unwrap(); let mapped_network = if rng.gen_bool(0.5) { Some( format!( "{}.{}.{}.0/{}", rng.gen_range(1..254), rng.gen_range(0..255), rng.gen_range(0..255), network.network_length() ) .parse::() .unwrap(), ) } else { None }; config.add_proxy_cidr(network, mapped_network).unwrap(); } } if rng.gen_bool(0.5) { let vpn_network = format!( "{}.{}.{}.0/{}", rng.gen_range(10..173), rng.gen_range(0..255), rng.gen_range(0..255), rng.gen_range(24..30) ); let vpn_port = rng.gen_range(10000..60000); config.set_vpn_portal_config(crate::common::config::VpnPortalConfig { client_cidr: vpn_network.parse().unwrap(), wireguard_listen: format!("0.0.0.0:{}", vpn_port).parse().unwrap(), }); } if rng.gen_bool(0.6) { let route_count = rng.gen_range(1..3); let mut routes = Vec::new(); for _ in 0..route_count { let route = format!( "{}.{}.{}.0/{}", rng.gen_range(1..254), rng.gen_range(0..255), rng.gen_range(0..255), rng.gen_range(24..30) ); routes.push(route.parse().unwrap()); } config.set_routes(Some(routes)); } if rng.gen_bool(0.4) { let node_count = rng.gen_range(1..3); let mut nodes = Vec::new(); for _ in 0..node_count { let ip = Ipv4Addr::new( rng.gen_range(1..254), rng.gen_range(0..255), rng.gen_range(0..255), rng.gen_range(1..254), ); nodes.push(IpAddr::V4(ip)); // gen ipv6 let ip = Ipv6Addr::new( rng.gen_range(0..65535), rng.gen_range(0..65535), rng.gen_range(0..65535), rng.gen_range(0..65535), rng.gen_range(0..65535), rng.gen_range(0..65535), rng.gen_range(0..65535), rng.gen_range(0..65535), ); nodes.push(IpAddr::V6(ip)); } config.set_exit_nodes(nodes); } if rng.gen_bool(0.5) { let socks5_port = rng.gen_range(10000..60000); config.set_socks5_portal(Some( format!("socks5://0.0.0.0:{}", socks5_port).parse().unwrap(), )); } if rng.gen_bool(0.4) { let count = rng.gen_range(1..3); let mut mapped_listeners = Vec::new(); for _ in 0..count { let port = rng.gen_range(10000..60000); mapped_listeners.push(format!("tcp://0.0.0.0:{}", port).parse().unwrap()); } config.set_mapped_listeners(Some(mapped_listeners)); } if rng.gen_bool(0.3) { config.set_secure_mode(Some(SecureModeConfig { enabled: true, local_private_key: None, local_public_key: None, })); } if rng.gen_bool(0.9) { let mut flags = crate::common::config::gen_default_flags(); flags.latency_first = rng.gen_bool(0.5); flags.dev_name = format!("etun{}", rng.gen_range(0..10)); flags.use_smoltcp = rng.gen_bool(0.3); flags.enable_ipv6 = rng.gen_bool(0.8); flags.enable_kcp_proxy = rng.gen_bool(0.5); flags.disable_kcp_input = rng.gen_bool(0.3); flags.enable_quic_proxy = rng.gen_bool(0.5); flags.disable_quic_input = rng.gen_bool(0.3); flags.disable_p2p = rng.gen_bool(0.2); flags.p2p_only = rng.gen_bool(0.2); flags.lazy_p2p = rng.gen_bool(0.3); flags.bind_device = rng.gen_bool(0.3); flags.no_tun = rng.gen_bool(0.1); flags.enable_exit_node = rng.gen_bool(0.4); flags.relay_all_peer_rpc = rng.gen_bool(0.5); flags.need_p2p = rng.gen_bool(0.3); flags.multi_thread = rng.gen_bool(0.7); flags.proxy_forward_by_system = rng.gen_bool(0.3); flags.enable_encryption = rng.gen_bool(0.8); flags.disable_tcp_hole_punching = rng.gen_bool(0.2); flags.disable_udp_hole_punching = rng.gen_bool(0.2); flags.disable_upnp = rng.gen_bool(0.2); flags.accept_dns = rng.gen_bool(0.6); flags.mtu = rng.gen_range(1200..1500); flags.private_mode = rng.gen_bool(0.3); if rng.gen_bool(0.4) { flags.relay_network_whitelist = (0..rng.gen_range(1..3)) .map(|_| { format!( "{}.{}.0.0/16", rng.gen_range(10..192), rng.gen_range(0..255) ) }) .collect::>() .join(" "); } config.set_flags(flags); } if let Some(secure_mode) = config.get_secure_mode() { config.set_secure_mode(Some(process_secure_mode_cfg(secure_mode)?)); } let network_config = super::NetworkConfig::new_from_config(&config)?; let generated_config = network_config.gen_config()?; generated_config.set_peers(generated_config.get_peers()); // Ensure peers field is not None let config_str = config.dump(); let generated_config_str = generated_config.dump(); assert_eq!( config_str, generated_config_str, "Generated config does not match original config:\nOriginal:\n{}\n\nGenerated:\n{}\nNetwork Config: {}\n", config_str, generated_config_str, serde_json::to_string(&network_config).unwrap() ); } Ok(()) } #[test] fn test_network_config_conversion_credential_mode() -> Result<(), anyhow::Error> { let private_key = x25519_dalek::StaticSecret::from([7u8; 32]); let public_key = x25519_dalek::PublicKey::from(&private_key); let credential_secret = BASE64_STANDARD.encode(private_key.as_bytes()); let credential_file = "/tmp/easytier-credentials.json".to_string(); let config = gen_default_config(); config.set_network_identity(crate::common::config::NetworkIdentity::new_credential( "credential-net".to_string(), )); config.set_inst_name("credential-net".to_string()); config.set_credential_file(Some(credential_file.clone().into())); config.set_secure_mode(Some(SecureModeConfig { enabled: true, local_private_key: Some(credential_secret.clone()), local_public_key: Some(BASE64_STANDARD.encode(public_key.as_bytes())), })); let network_config = super::NetworkConfig::new_from_config(&config)?; assert_eq!( network_config.credential_file.as_deref(), Some(credential_file.as_str()) ); assert_eq!(network_config.network_secret, None); assert_eq!( network_config .secure_mode .as_ref() .and_then(|mode| mode.local_private_key.as_deref()), Some(credential_secret.as_str()) ); let generated_config = network_config.gen_config()?; assert_eq!( generated_config.get_network_identity().network_secret, None, "credential mode should not be converted back into network_secret mode" ); assert_eq!( generated_config .get_credential_file() .map(|path| path.to_string_lossy().into_owned()), Some(credential_file) ); assert_eq!( generated_config .get_secure_mode() .and_then(|mode| mode.local_private_key), Some(credential_secret) ); Ok(()) } }