diff --git a/easytier-web/locales/app.yml b/easytier-web/locales/app.yml index bca40a7..5d9dbff 100644 --- a/easytier-web/locales/app.yml +++ b/easytier-web/locales/app.yml @@ -23,8 +23,8 @@ cli: en: "The port to listen for the restful server, acting as ApiHost and used by the web frontend" zh-CN: "restful 服务器的监听端口,作为 ApiHost 并被 web 前端使用" web_server_port: - en: "The port to listen for the web dashboard server" - zh-CN: "web dashboard 服务器的监听端口" + en: "The port to listen for the web dashboard server, default is same as the api server port" + zh-CN: "web dashboard 服务器的监听端口, 默认为与 api 服务器端口相同" no_web: en: "Do not run the web dashboard server" zh-CN: "不运行 web dashboard 服务器" \ No newline at end of file diff --git a/easytier-web/src/main.rs b/easytier-web/src/main.rs index 0a993d2..949a1eb 100644 --- a/easytier-web/src/main.rs +++ b/easytier-web/src/main.rs @@ -78,10 +78,9 @@ struct Cli { #[arg( long, short='l', - default_value = "11210", help = t!("cli.web_server_port").to_string(), )] - web_server_port: u16, + web_server_port: Option, #[cfg(feature = "embed")] #[arg( @@ -92,9 +91,7 @@ struct Cli { no_web: bool, } -pub fn get_listener_by_url( - l: &url::Url, -) -> Result, Error> { +pub fn get_listener_by_url(l: &url::Url) -> Result, Error> { Ok(match l.scheme() { "tcp" => Box::new(TcpTunnelListener::new(l.clone())), "udp" => Box::new(UdpTunnelListener::new(l.clone())), @@ -126,17 +123,30 @@ async fn main() { let db = db::Db::new(cli.db).await.unwrap(); let listener = get_listener_by_url( - &format!("{}://0.0.0.0:{}", cli.config_server_protocol, cli.config_server_port).parse().unwrap(), + &format!( + "{}://0.0.0.0:{}", + cli.config_server_protocol, cli.config_server_port + ) + .parse() + .unwrap(), ) .unwrap(); let mut mgr = client_manager::ClientManager::new(db.clone()); mgr.serve(listener).await.unwrap(); let mgr = Arc::new(mgr); + #[cfg(feature = "embed")] + let restful_also_serve_web = !cli.no_web + && (cli.web_server_port.is_none() || cli.web_server_port == Some(cli.api_server_port)); + + #[cfg(not(feature = "embed"))] + let restful_also_serve_web = false; + let mut restful_server = restful::RestfulServer::new( format!("0.0.0.0:{}", cli.api_server_port).parse().unwrap(), mgr.clone(), db, + restful_also_serve_web, ) .await .unwrap(); @@ -145,13 +155,15 @@ async fn main() { #[cfg(feature = "embed")] let mut web_server = web::WebServer::new( - format!("0.0.0.0:{}", cli.web_server_port).parse().unwrap() + format!("0.0.0.0:{}", cli.web_server_port.unwrap_or(0)) + .parse() + .unwrap(), ) .await .unwrap(); #[cfg(feature = "embed")] - if !cli.no_web { + if !cli.no_web && !restful_also_serve_web { web_server.start().await.unwrap(); } diff --git a/easytier-web/src/restful/mod.rs b/easytier-web/src/restful/mod.rs index 3169d0f..98543d4 100644 --- a/easytier-web/src/restful/mod.rs +++ b/easytier-web/src/restful/mod.rs @@ -29,6 +29,12 @@ use crate::client_manager::storage::StorageToken; use crate::client_manager::ClientManager; use crate::db::Db; +/// Embed assets for web dashboard, build frontend first +#[cfg(feature = "embed")] +#[derive(rust_embed::RustEmbed, Clone)] +#[folder = "frontend/dist/"] +struct Assets; + pub struct RestfulServer { bind_addr: SocketAddr, client_mgr: Arc, @@ -38,6 +44,8 @@ pub struct RestfulServer { delete_task: Option>>, network_api: NetworkApi, + + enable_web_embed: bool, } type AppStateInner = Arc; @@ -87,6 +95,7 @@ impl RestfulServer { bind_addr: SocketAddr, client_mgr: Arc, db: Db, + enable_web_embed: bool, ) -> anyhow::Result { assert!(client_mgr.is_running()); @@ -99,6 +108,7 @@ impl RestfulServer { serve_task: None, delete_task: None, network_api, + enable_web_embed, }) } @@ -219,6 +229,15 @@ impl RestfulServer { .layer(tower_http::cors::CorsLayer::very_permissive()) .layer(compression_layer); + #[cfg(feature = "embed")] + let app = if self.enable_web_embed { + use axum_embed::ServeEmbed; + let service = ServeEmbed::::new(); + app.fallback_service(service) + } else { + app + }; + let task = tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); diff --git a/easytier/src/instance/dns_server/server_instance.rs b/easytier/src/instance/dns_server/server_instance.rs index 36ab916..f769549 100644 --- a/easytier/src/instance/dns_server/server_instance.rs +++ b/easytier/src/instance/dns_server/server_instance.rs @@ -50,6 +50,7 @@ use crate::{ use super::{ config::{GeneralConfigBuilder, RunConfigBuilder}, server::Server, + system_config::{OSConfig, SystemConfig}, MAGIC_DNS_INSTANCE_ADDR, }; @@ -64,6 +65,8 @@ pub(super) struct MagicDnsServerInstanceData { // zone -> (tunnel remote addr -> route) route_infos: DashMap>, + + system_config: Option>, } impl MagicDnsServerInstanceData { @@ -128,14 +131,14 @@ impl MagicDnsServerInstanceData { } } - fn do_system_config(&self, _zone: &str) -> Result<(), anyhow::Error> { - #[cfg(target_os = "windows")] - { - use super::system_config::windows::WindowsDNSManager; - let cfg = WindowsDNSManager::new(self.tun_dev.as_ref().unwrap())?; - cfg.set_primary_dns(&[self.fake_ip.clone().into()], &[_zone.to_string()])?; + fn do_system_config(&self, zone: &str) -> Result<(), anyhow::Error> { + if let Some(c) = &self.system_config { + c.set_dns(&OSConfig { + nameservers: vec![self.fake_ip.to_string()], + search_domains: vec![zone.to_string()], + match_domains: vec![zone.to_string()], + })?; } - Ok(()) } } @@ -323,6 +326,26 @@ pub struct MagicDnsServerInstance { tun_inet: Ipv4Inet, } +fn get_system_config( + _tun_name: Option<&str>, +) -> Result>, anyhow::Error> { + #[cfg(target_os = "windows")] + { + use super::system_config::windows::WindowsDNSManager; + let tun_name = _tun_name.ok_or_else(|| anyhow::anyhow!("No tun name"))?; + return Ok(Some(Box::new(WindowsDNSManager::new(tun_name)?))); + } + + #[cfg(target_os = "macos")] + { + use super::system_config::darwin::DarwinConfigurator; + return Ok(Some(Box::new(DarwinConfigurator::new()))); + } + + #[allow(unreachable_code)] + Ok(None) +} + impl MagicDnsServerInstance { pub async fn new( peer_mgr: Arc, @@ -364,12 +387,14 @@ impl MagicDnsServerInstance { let data = Arc::new(MagicDnsServerInstanceData { dns_server, - tun_dev, + tun_dev: tun_dev.clone(), tun_ip: tun_inet.address(), fake_ip, my_peer_id: peer_mgr.my_peer_id(), route_infos: DashMap::new(), + system_config: get_system_config(tun_dev.as_deref())?, }); + rpc_server .registry() .register(MagicDnsServerRpcServer::new(data.clone()), ""); @@ -393,6 +418,13 @@ impl MagicDnsServerInstance { } pub async fn clean_env(&self) { + if let Some(configer) = &self.data.system_config { + let ret = configer.close(); + if let Err(e) = ret { + tracing::error!("Failed to close system config: {:?}", e); + } + } + if !self.tun_inet.contains(&self.data.fake_ip) && self.data.tun_dev.is_some() { let ifcfg = IfConfiger {}; let _ = ifcfg diff --git a/easytier/src/instance/dns_server/system_config/darwin.rs b/easytier/src/instance/dns_server/system_config/darwin.rs new file mode 100644 index 0000000..8217480 --- /dev/null +++ b/easytier/src/instance/dns_server/system_config/darwin.rs @@ -0,0 +1,135 @@ +use std::{ + collections::HashSet, + fs::{self, OpenOptions}, + io::{self, Write}, + os::unix::fs::PermissionsExt, + path::Path, +}; + +use super::{OSConfig, SystemConfig}; + +const MAC_RESOLVER_FILE_HEADER: &str = "# Added by easytier\n"; +const ETC_RESOLVER: &str = "/etc/resolver"; +const ETC_RESOLV_CONF: &str = "/etc/resolv.conf"; + +pub struct DarwinConfigurator {} + +impl DarwinConfigurator { + pub fn new() -> Self { + DarwinConfigurator {} + } + + pub fn do_close(&self) -> io::Result<()> { + self.remove_resolver_files(|_| true) + } + + pub fn supports_split_dns(&self) -> bool { + true + } + + pub fn do_set_dns(&self, cfg: &OSConfig) -> io::Result<()> { + fs::create_dir_all(ETC_RESOLVER)?; + let mut keep = HashSet::new(); + + // 写 search.easytier 文件 + if !cfg.search_domains.is_empty() { + let search_file = "search.easytier"; + keep.insert(search_file.to_string()); + let mut content = String::from(MAC_RESOLVER_FILE_HEADER); + content.push_str("search"); + for domain in &cfg.search_domains { + content.push(' '); + content.push_str(domain.trim_end_matches('.')); + } + content.push('\n'); + Self::write_resolver_file(search_file, &content)?; + } + + // 写 match_domains 文件 + let mut ns_content = String::from(MAC_RESOLVER_FILE_HEADER); + for ns in &cfg.nameservers { + ns_content.push_str(&format!("nameserver {}\n", ns)); + } + for domain in &cfg.match_domains { + let file_base = domain.trim_end_matches('.'); + keep.insert(file_base.to_string()); + Self::write_resolver_file(file_base, &ns_content)?; + } + // 删除未保留的 resolver 文件 + self.remove_resolver_files(|domain| !keep.contains(domain))?; + + Ok(()) + } + + fn write_resolver_file(file_name: &str, content: &str) -> io::Result<()> { + let path = Path::new(ETC_RESOLVER).join(file_name); + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(&path)?; + file.set_permissions(fs::Permissions::from_mode(0o644))?; + file.write_all(content.as_bytes())?; + Ok(()) + } + + fn remove_resolver_files(&self, should_delete: F) -> io::Result<()> + where + F: Fn(&str) -> bool, + { + let entries = match fs::read_dir(ETC_RESOLVER) { + Ok(e) => e, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()), + Err(e) => return Err(e), + }; + for entry in entries { + let entry = entry?; + let file_type = entry.file_type()?; + if !file_type.is_file() { + continue; + } + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + if !should_delete(&name_str) { + continue; + } + let full_path = entry.path(); + let content = fs::read_to_string(&full_path)?; + if !content.starts_with(MAC_RESOLVER_FILE_HEADER) { + continue; + } + fs::remove_file(&full_path)?; + } + Ok(()) + } +} + +impl SystemConfig for DarwinConfigurator { + fn set_dns(&self, cfg: &OSConfig) -> io::Result<()> { + self.do_set_dns(cfg) + } + + fn close(&self) -> io::Result<()> { + self.do_close() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn set_dns_test() -> io::Result<()> { + let config = OSConfig { + nameservers: vec!["8.8.8.8".into()], + search_domains: vec!["example.com".into()], + match_domains: vec!["test.local".into()], + }; + let configurator = DarwinConfigurator::new(); + + configurator.set_dns(&config)?; + configurator.close()?; + + Ok(()) + } +} diff --git a/easytier/src/instance/dns_server/system_config/mod.rs b/easytier/src/instance/dns_server/system_config/mod.rs index 51b8bc8..c4a735a 100644 --- a/easytier/src/instance/dns_server/system_config/mod.rs +++ b/easytier/src/instance/dns_server/system_config/mod.rs @@ -3,3 +3,18 @@ pub mod linux; #[cfg(target_os = "windows")] pub mod windows; + +#[cfg(target_os = "macos")] +pub mod darwin; + +#[derive(Default, Debug)] +pub struct OSConfig { + pub nameservers: Vec, + pub search_domains: Vec, + pub match_domains: Vec, +} + +pub trait SystemConfig: Send + Sync { + fn set_dns(&self, cfg: &OSConfig) -> std::io::Result<()>; + fn close(&self) -> std::io::Result<()>; +} diff --git a/easytier/src/instance/dns_server/system_config/windows.rs b/easytier/src/instance/dns_server/system_config/windows.rs index b8fc251..f626cae 100644 --- a/easytier/src/instance/dns_server/system_config/windows.rs +++ b/easytier/src/instance/dns_server/system_config/windows.rs @@ -6,6 +6,8 @@ use winreg::RegKey; use crate::common::ifcfg::RegistryManager; +use super::{OSConfig, SystemConfig}; + pub fn is_windows_10_or_better() -> io::Result { let hklm = winreg::enums::HKEY_LOCAL_MACHINE; let key_path = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"; @@ -150,6 +152,23 @@ impl WindowsDNSManager { } } +impl SystemConfig for WindowsDNSManager { + fn set_dns(&self, cfg: &OSConfig) -> io::Result<()> { + self.set_primary_dns( + &cfg.nameservers + .iter() + .map(|s| s.parse::().unwrap()) + .collect::>(), + &cfg.match_domains, + )?; + Ok(()) + } + + fn close(&self) -> io::Result<()> { + Ok(()) + } +} + #[cfg(test)] mod tests { use cidr::Ipv4Inet;