Magic DNS and easytier-web improvements (#856)

1. dns add macos system config
2. allow easytier-web serve dashboard and api in same port
This commit is contained in:
Sijie.Sun
2025-05-18 16:34:35 +08:00
committed by GitHub
parent 28fe6257be
commit 7c6daf7c56
7 changed files with 250 additions and 18 deletions
+2 -2
View File
@@ -23,8 +23,8 @@ cli:
en: "The port to listen for the restful server, acting as ApiHost and used by the web frontend" en: "The port to listen for the restful server, acting as ApiHost and used by the web frontend"
zh-CN: "restful 服务器的监听端口,作为 ApiHost 并被 web 前端使用" zh-CN: "restful 服务器的监听端口,作为 ApiHost 并被 web 前端使用"
web_server_port: web_server_port:
en: "The port to listen for the web dashboard server" en: "The port to listen for the web dashboard server, default is same as the api server port"
zh-CN: "web dashboard 服务器的监听端口" zh-CN: "web dashboard 服务器的监听端口, 默认为与 api 服务器端口相同"
no_web: no_web:
en: "Do not run the web dashboard server" en: "Do not run the web dashboard server"
zh-CN: "不运行 web dashboard 服务器" zh-CN: "不运行 web dashboard 服务器"
+20 -8
View File
@@ -78,10 +78,9 @@ struct Cli {
#[arg( #[arg(
long, long,
short='l', short='l',
default_value = "11210",
help = t!("cli.web_server_port").to_string(), help = t!("cli.web_server_port").to_string(),
)] )]
web_server_port: u16, web_server_port: Option<u16>,
#[cfg(feature = "embed")] #[cfg(feature = "embed")]
#[arg( #[arg(
@@ -92,9 +91,7 @@ struct Cli {
no_web: bool, no_web: bool,
} }
pub fn get_listener_by_url( pub fn get_listener_by_url(l: &url::Url) -> Result<Box<dyn TunnelListener>, Error> {
l: &url::Url,
) -> Result<Box<dyn TunnelListener>, Error> {
Ok(match l.scheme() { Ok(match l.scheme() {
"tcp" => Box::new(TcpTunnelListener::new(l.clone())), "tcp" => Box::new(TcpTunnelListener::new(l.clone())),
"udp" => Box::new(UdpTunnelListener::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 db = db::Db::new(cli.db).await.unwrap();
let listener = get_listener_by_url( 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(); .unwrap();
let mut mgr = client_manager::ClientManager::new(db.clone()); let mut mgr = client_manager::ClientManager::new(db.clone());
mgr.serve(listener).await.unwrap(); mgr.serve(listener).await.unwrap();
let mgr = Arc::new(mgr); 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( let mut restful_server = restful::RestfulServer::new(
format!("0.0.0.0:{}", cli.api_server_port).parse().unwrap(), format!("0.0.0.0:{}", cli.api_server_port).parse().unwrap(),
mgr.clone(), mgr.clone(),
db, db,
restful_also_serve_web,
) )
.await .await
.unwrap(); .unwrap();
@@ -145,13 +155,15 @@ async fn main() {
#[cfg(feature = "embed")] #[cfg(feature = "embed")]
let mut web_server = web::WebServer::new( 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 .await
.unwrap(); .unwrap();
#[cfg(feature = "embed")] #[cfg(feature = "embed")]
if !cli.no_web { if !cli.no_web && !restful_also_serve_web {
web_server.start().await.unwrap(); web_server.start().await.unwrap();
} }
+19
View File
@@ -29,6 +29,12 @@ use crate::client_manager::storage::StorageToken;
use crate::client_manager::ClientManager; use crate::client_manager::ClientManager;
use crate::db::Db; 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 { pub struct RestfulServer {
bind_addr: SocketAddr, bind_addr: SocketAddr,
client_mgr: Arc<ClientManager>, client_mgr: Arc<ClientManager>,
@@ -38,6 +44,8 @@ pub struct RestfulServer {
delete_task: Option<ScopedTask<tower_sessions::session_store::Result<()>>>, delete_task: Option<ScopedTask<tower_sessions::session_store::Result<()>>>,
network_api: NetworkApi, network_api: NetworkApi,
enable_web_embed: bool,
} }
type AppStateInner = Arc<ClientManager>; type AppStateInner = Arc<ClientManager>;
@@ -87,6 +95,7 @@ impl RestfulServer {
bind_addr: SocketAddr, bind_addr: SocketAddr,
client_mgr: Arc<ClientManager>, client_mgr: Arc<ClientManager>,
db: Db, db: Db,
enable_web_embed: bool,
) -> anyhow::Result<Self> { ) -> anyhow::Result<Self> {
assert!(client_mgr.is_running()); assert!(client_mgr.is_running());
@@ -99,6 +108,7 @@ impl RestfulServer {
serve_task: None, serve_task: None,
delete_task: None, delete_task: None,
network_api, network_api,
enable_web_embed,
}) })
} }
@@ -219,6 +229,15 @@ impl RestfulServer {
.layer(tower_http::cors::CorsLayer::very_permissive()) .layer(tower_http::cors::CorsLayer::very_permissive())
.layer(compression_layer); .layer(compression_layer);
#[cfg(feature = "embed")]
let app = if self.enable_web_embed {
use axum_embed::ServeEmbed;
let service = ServeEmbed::<Assets>::new();
app.fallback_service(service)
} else {
app
};
let task = tokio::spawn(async move { let task = tokio::spawn(async move {
axum::serve(listener, app).await.unwrap(); axum::serve(listener, app).await.unwrap();
}); });
@@ -50,6 +50,7 @@ use crate::{
use super::{ use super::{
config::{GeneralConfigBuilder, RunConfigBuilder}, config::{GeneralConfigBuilder, RunConfigBuilder},
server::Server, server::Server,
system_config::{OSConfig, SystemConfig},
MAGIC_DNS_INSTANCE_ADDR, MAGIC_DNS_INSTANCE_ADDR,
}; };
@@ -64,6 +65,8 @@ pub(super) struct MagicDnsServerInstanceData {
// zone -> (tunnel remote addr -> route) // zone -> (tunnel remote addr -> route)
route_infos: DashMap<String, MultiMap<url::Url, Route>>, route_infos: DashMap<String, MultiMap<url::Url, Route>>,
system_config: Option<Box<dyn SystemConfig>>,
} }
impl MagicDnsServerInstanceData { impl MagicDnsServerInstanceData {
@@ -128,14 +131,14 @@ impl MagicDnsServerInstanceData {
} }
} }
fn do_system_config(&self, _zone: &str) -> Result<(), anyhow::Error> { fn do_system_config(&self, zone: &str) -> Result<(), anyhow::Error> {
#[cfg(target_os = "windows")] if let Some(c) = &self.system_config {
{ c.set_dns(&OSConfig {
use super::system_config::windows::WindowsDNSManager; nameservers: vec![self.fake_ip.to_string()],
let cfg = WindowsDNSManager::new(self.tun_dev.as_ref().unwrap())?; search_domains: vec![zone.to_string()],
cfg.set_primary_dns(&[self.fake_ip.clone().into()], &[_zone.to_string()])?; match_domains: vec![zone.to_string()],
})?;
} }
Ok(()) Ok(())
} }
} }
@@ -323,6 +326,26 @@ pub struct MagicDnsServerInstance {
tun_inet: Ipv4Inet, tun_inet: Ipv4Inet,
} }
fn get_system_config(
_tun_name: Option<&str>,
) -> Result<Option<Box<dyn SystemConfig>>, 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 { impl MagicDnsServerInstance {
pub async fn new( pub async fn new(
peer_mgr: Arc<PeerManager>, peer_mgr: Arc<PeerManager>,
@@ -364,12 +387,14 @@ impl MagicDnsServerInstance {
let data = Arc::new(MagicDnsServerInstanceData { let data = Arc::new(MagicDnsServerInstanceData {
dns_server, dns_server,
tun_dev, tun_dev: tun_dev.clone(),
tun_ip: tun_inet.address(), tun_ip: tun_inet.address(),
fake_ip, fake_ip,
my_peer_id: peer_mgr.my_peer_id(), my_peer_id: peer_mgr.my_peer_id(),
route_infos: DashMap::new(), route_infos: DashMap::new(),
system_config: get_system_config(tun_dev.as_deref())?,
}); });
rpc_server rpc_server
.registry() .registry()
.register(MagicDnsServerRpcServer::new(data.clone()), ""); .register(MagicDnsServerRpcServer::new(data.clone()), "");
@@ -393,6 +418,13 @@ impl MagicDnsServerInstance {
} }
pub async fn clean_env(&self) { 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() { if !self.tun_inet.contains(&self.data.fake_ip) && self.data.tun_dev.is_some() {
let ifcfg = IfConfiger {}; let ifcfg = IfConfiger {};
let _ = ifcfg let _ = ifcfg
@@ -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<F>(&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(())
}
}
@@ -3,3 +3,18 @@ pub mod linux;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub mod windows; pub mod windows;
#[cfg(target_os = "macos")]
pub mod darwin;
#[derive(Default, Debug)]
pub struct OSConfig {
pub nameservers: Vec<String>,
pub search_domains: Vec<String>,
pub match_domains: Vec<String>,
}
pub trait SystemConfig: Send + Sync {
fn set_dns(&self, cfg: &OSConfig) -> std::io::Result<()>;
fn close(&self) -> std::io::Result<()>;
}
@@ -6,6 +6,8 @@ use winreg::RegKey;
use crate::common::ifcfg::RegistryManager; use crate::common::ifcfg::RegistryManager;
use super::{OSConfig, SystemConfig};
pub fn is_windows_10_or_better() -> io::Result<bool> { pub fn is_windows_10_or_better() -> io::Result<bool> {
let hklm = winreg::enums::HKEY_LOCAL_MACHINE; let hklm = winreg::enums::HKEY_LOCAL_MACHINE;
let key_path = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"; 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::<IpAddr>().unwrap())
.collect::<Vec<_>>(),
&cfg.match_domains,
)?;
Ok(())
}
fn close(&self) -> io::Result<()> {
Ok(())
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use cidr::Ipv4Inet; use cidr::Ipv4Inet;