Files
Easytier/easytier-web/src/main.rs
T
KKRainbow e6ac31fb20 feat(web): add webhook-managed machine access and multi-instance CLI support (#1989)
* feat: add webhook-managed access and multi-instance CLI support
* fix(foreign): verify credential of foreign credential peer
2026-03-15 12:08:50 +08:00

362 lines
10 KiB
Rust

#![allow(dead_code)]
#[macro_use]
extern crate rust_i18n;
use std::net::IpAddr;
use std::sync::Arc;
use clap::Parser;
use easytier::tunnel::websocket::WSTunnelListener;
use easytier::{
common::{
config::{ConsoleLoggerConfig, FileLoggerConfig, LoggingConfigLoader},
constants::EASYTIER_VERSION,
error::Error,
log,
network::{local_ipv4, local_ipv6},
},
tunnel::{tcp::TcpTunnelListener, udp::UdpTunnelListener, TunnelListener},
utils::setup_panic_handler,
};
use mimalloc::MiMalloc;
mod client_manager;
mod db;
mod migrator;
mod restful;
mod webhook;
#[cfg(feature = "embed")]
mod web;
#[global_allocator]
static GLOBAL_MIMALLOC: MiMalloc = MiMalloc;
rust_i18n::i18n!("locales", fallback = "en");
#[derive(Parser, Debug)]
#[command(name = "easytier-web", author, version = EASYTIER_VERSION , about, long_about = None)]
struct Cli {
#[arg(short, long, default_value = "et.db", help = t!("cli.db").to_string())]
db: String,
#[arg(
long,
help = t!("cli.console_log_level").to_string(),
)]
console_log_level: Option<String>,
#[arg(
long,
help = t!("cli.file_log_level").to_string(),
)]
file_log_level: Option<String>,
#[arg(
long,
help = t!("cli.file_log_dir").to_string(),
)]
file_log_dir: Option<String>,
#[arg(
long,
short='c',
default_value = "22020",
help = t!("cli.config_server_port").to_string(),
)]
config_server_port: u16,
#[arg(
long,
short='p',
default_value = "udp",
help = t!("cli.config_server_protocol").to_string(),
)]
config_server_protocol: String,
#[arg(
long,
short='a',
default_value = "11211",
help = t!("cli.api_server_port").to_string(),
)]
api_server_port: u16,
#[arg(
long,
default_value = "0.0.0.0",
help = t!("cli.api_server_addr").to_string(),
)]
api_server_addr: IpAddr,
#[arg(
long,
help = t!("cli.geoip_db").to_string(),
)]
geoip_db: Option<String>,
#[cfg(feature = "embed")]
#[arg(
long,
short='l',
help = t!("cli.web_server_port").to_string(),
)]
web_server_port: Option<u16>,
#[cfg(feature = "embed")]
#[arg(
long,
default_value = "0.0.0.0",
help = t!("cli.web_server_addr").to_string(),
)]
web_server_addr: IpAddr,
#[cfg(feature = "embed")]
#[arg(
long,
help = t!("cli.no_web").to_string(),
default_value = "false"
)]
no_web: bool,
#[cfg(feature = "embed")]
#[arg(
long,
help = t!("cli.api_host").to_string()
)]
api_host: Option<url::Url>,
#[command(flatten)]
feature_flags: FeatureFlags,
#[command(flatten)]
oidc: restful::oidc::OidcOptions,
#[command(flatten)]
webhook: WebhookOptions,
}
#[derive(Debug, Clone, Default, clap::Args)]
pub struct WebhookOptions {
/// Base URL of the webhook endpoint for token validation and event delivery.
/// When set, incoming tokens are validated via this webhook before local fallback.
#[arg(long)]
pub webhook_url: Option<String>,
/// Shared secret used to authenticate outbound webhook calls.
#[arg(long)]
pub webhook_secret: Option<String>,
/// Token for X-Internal-Auth header. When set, API requests with this header
/// bypass session authentication.
#[arg(long)]
pub internal_auth_token: Option<String>,
/// Stable identifier for this easytier-web instance when routing webhook callbacks.
#[arg(long)]
pub web_instance_id: Option<String>,
/// Reachable base URL for this easytier-web instance's internal REST API.
#[arg(long)]
pub web_instance_api_base_url: Option<String>,
}
#[derive(Debug, Clone, Default, clap::Args)]
pub struct FeatureFlags {
/// Whether user registration via the web UI is disabled.
#[arg(long, default_value = "false", help = t!("cli.disable_registration").to_string())]
pub disable_registration: bool,
/// Whether to auto-create users when they connect via heartbeat with an unknown token.
#[arg(long, default_value = "false", help = t!("cli.allow_auto_create_user").to_string())]
pub allow_auto_create_user: bool,
}
impl LoggingConfigLoader for &Cli {
fn get_console_logger_config(&self) -> ConsoleLoggerConfig {
ConsoleLoggerConfig {
level: self.console_log_level.clone(),
}
}
fn get_file_logger_config(&self) -> FileLoggerConfig {
FileLoggerConfig {
dir: self.file_log_dir.clone(),
level: self.file_log_level.clone(),
file: None,
size_mb: None,
count: None,
}
}
}
pub fn get_listener_by_url(l: &url::Url) -> Result<Box<dyn TunnelListener>, Error> {
Ok(match l.scheme() {
"tcp" => Box::new(TcpTunnelListener::new(l.clone())),
"udp" => Box::new(UdpTunnelListener::new(l.clone())),
"ws" => Box::new(WSTunnelListener::new(l.clone())),
_ => {
return Err(Error::InvalidUrl(l.to_string()));
}
})
}
async fn get_dual_stack_listener(
protocol: &str,
port: u16,
) -> Result<
(
Option<Box<dyn TunnelListener>>,
Option<Box<dyn TunnelListener>>,
),
Error,
> {
let is_protocol_support_dual_stack =
protocol.trim().to_lowercase() == "tcp" || protocol.trim().to_lowercase() == "udp";
let v6_listener = if is_protocol_support_dual_stack && local_ipv6().await.is_ok() {
get_listener_by_url(&format!("{}://[::0]:{}", protocol, port).parse().unwrap()).ok()
} else {
None
};
let v4_listener = if local_ipv4().await.is_ok() {
get_listener_by_url(&format!("{}://0.0.0.0:{}", protocol, port).parse().unwrap()).ok()
} else {
None
};
Ok((v6_listener, v4_listener))
}
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() {
let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
rust_i18n::set_locale(&locale);
setup_panic_handler();
let cli = Cli::parse();
log::init(&cli, false).unwrap();
// Validate OIDC configuration: check split-deploy specific requirements
// Basic OIDC parameter validation is handled in OidcConfig::from_params
if cli.oidc.any_param_provided() {
let is_split_deploy = {
#[cfg(feature = "embed")]
{
let embed_split_by_port = cli.web_server_port.is_some()
&& cli.web_server_port != Some(cli.api_server_port);
cli.no_web || embed_split_by_port
}
#[cfg(not(feature = "embed"))]
{
true
}
};
if is_split_deploy && cli.oidc.oidc_frontend_base_url.is_none() {
eprintln!("Error: --oidc-frontend-base-url is required in split-deploy mode");
eprintln!(
"When frontend and API are deployed separately, you must specify the frontend URL"
);
eprintln!("Example: --oidc-frontend-base-url http://your-frontend-domain.com");
std::process::exit(1);
}
}
// let db = db::Db::new(":memory:").await.unwrap();
let db = db::Db::new(cli.db).await.unwrap();
let feature_flags = Arc::new(cli.feature_flags);
let webhook_config = Arc::new(webhook::WebhookConfig::new(
cli.webhook.webhook_url,
cli.webhook.webhook_secret,
cli.webhook.internal_auth_token,
cli.webhook.web_instance_id,
cli.webhook.web_instance_api_base_url,
));
let mut mgr = client_manager::ClientManager::new(
db.clone(),
cli.geoip_db,
feature_flags.clone(),
webhook_config.clone(),
);
let (v6_listener, v4_listener) =
get_dual_stack_listener(&cli.config_server_protocol, cli.config_server_port)
.await
.unwrap();
if v4_listener.is_none() && v6_listener.is_none() {
panic!("Listen to both IPv4 and IPv6 failed");
}
if let Some(listener) = v6_listener {
mgr.add_listener(listener).await.unwrap();
}
if let Some(listener) = v4_listener {
mgr.add_listener(listener).await.unwrap();
}
let mgr = Arc::new(mgr);
#[cfg(feature = "embed")]
let (web_router_restful, web_router_static) = if cli.no_web {
(None, None)
} else {
let web_router = web::build_router(cli.api_host.clone());
if cli.web_server_port.is_none()
|| (cli.web_server_port == Some(cli.api_server_port)
&& cli.web_server_addr == cli.api_server_addr)
{
(Some(web_router), None)
} else {
(None, Some(web_router))
}
};
#[cfg(not(feature = "embed"))]
let web_router_restful = None;
let oidc_config = if cli.oidc.oidc_issuer_url.is_some() {
match restful::oidc::OidcConfig::from_params(cli.oidc).await {
Ok(config) => config,
Err(e) => {
eprintln!("Failed to initialize OIDC: {:?}", e);
eprintln!("Please check your OIDC configuration (issuer URL, client ID, etc.)");
std::process::exit(1);
}
}
} else {
restful::oidc::OidcConfig::disabled()
};
let _restful_server_tasks = restful::RestfulServer::new(
std::net::SocketAddr::new(cli.api_server_addr, cli.api_server_port),
mgr.clone(),
db,
web_router_restful,
feature_flags,
oidc_config,
webhook_config,
)
.await
.unwrap()
.start()
.await
.unwrap();
#[cfg(feature = "embed")]
let _web_server_task = if let Some(web_router) = web_router_static {
Some(
web::WebServer::new(
std::net::SocketAddr::new(cli.web_server_addr, cli.web_server_port.unwrap_or(0)),
web_router,
)
.await
.unwrap()
.start()
.await
.unwrap(),
)
} else {
None
};
tokio::signal::ctrl_c().await.unwrap();
}