mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-07 02:09:06 +00:00
e6ac31fb20
* feat: add webhook-managed access and multi-instance CLI support * fix(foreign): verify credential of foreign credential peer
362 lines
10 KiB
Rust
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();
|
|
}
|