mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-10-14 16:36:16 -08:00
Construct config-server TCP and UDP listeners on IPv4 and IPv6 wildcard addresses. A default route is not needed for listening, so remove the local-IP probes, URL-to-address wrapper, and optional listener pairs. Keep WebSocket on its existing IPv4 address. Propagate all native socket bind errors instead of ignoring IPv6 failures. Try address families independently and stop startup only when no listener could bind, so one unavailable family does not hide a working listener or create an unbound socket. Log every successful listener with its actual bound address, including the allocated port when the configured port is zero. Add regressions for occupied ports and unavailable IPv6 source addresses, failed listener registration, and valid ephemeral ports. Fixes #2611 Co-authored-by: KKRainbow <5665404+KKRainbow@users.noreply.github.com>
495 lines
15 KiB
Rust
495 lines
15 KiB
Rust
#![allow(dead_code)]
|
|
|
|
#[macro_use]
|
|
extern crate rust_i18n;
|
|
|
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
|
use std::sync::Arc;
|
|
|
|
use central_network::gateway;
|
|
|
|
use clap::Parser;
|
|
use easytier::tunnel::websocket::WsTunnelListener;
|
|
use easytier::{
|
|
common::{
|
|
config::{ConsoleLoggerConfig, FileLoggerConfig, LoggingConfigLoader},
|
|
constants::EASYTIER_VERSION,
|
|
error::Error,
|
|
log,
|
|
},
|
|
proto::rpc::standalone::{runtime_rpc_listener, runtime_udp_tunnel_listener},
|
|
utils::panic::setup_panic_handler,
|
|
};
|
|
use easytier_core::{socket::SocketListener, tunnel::Tunnel};
|
|
|
|
use easytier::tunnel::IpScheme;
|
|
use mimalloc::MiMalloc;
|
|
|
|
mod central_network;
|
|
mod client_manager;
|
|
#[cfg(test)]
|
|
mod config_server_listener_tests;
|
|
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,
|
|
env = "ET_WEB_DB",
|
|
default_value = "et.db",
|
|
help = t!("cli.db").to_string()
|
|
)]
|
|
db: String,
|
|
|
|
#[arg(
|
|
long,
|
|
env = "ET_WEB_CONSOLE_LOG_LEVEL",
|
|
help = t!("cli.console_log_level").to_string(),
|
|
)]
|
|
console_log_level: Option<String>,
|
|
|
|
#[arg(
|
|
long,
|
|
env = "ET_WEB_FILE_LOG_LEVEL",
|
|
help = t!("cli.file_log_level").to_string(),
|
|
)]
|
|
file_log_level: Option<String>,
|
|
|
|
#[arg(
|
|
long,
|
|
env = "ET_WEB_FILE_LOG_DIR",
|
|
help = t!("cli.file_log_dir").to_string(),
|
|
)]
|
|
file_log_dir: Option<String>,
|
|
|
|
#[arg(
|
|
long,
|
|
short='c',
|
|
env = "ET_CONFIG_SERVER_PORT",
|
|
default_value = "22020",
|
|
help = t!("cli.config_server_port").to_string(),
|
|
)]
|
|
config_server_port: u16,
|
|
|
|
#[arg(
|
|
long,
|
|
short='p',
|
|
env = "ET_CONFIG_SERVER_PROTOCOL",
|
|
default_value = "udp",
|
|
help = t!("cli.config_server_protocol").to_string(),
|
|
)]
|
|
config_server_protocol: String,
|
|
|
|
#[arg(
|
|
long,
|
|
short='a',
|
|
env = "ET_API_SERVER_PORT",
|
|
default_value = "11211",
|
|
help = t!("cli.api_server_port").to_string(),
|
|
)]
|
|
api_server_port: u16,
|
|
|
|
#[arg(
|
|
long,
|
|
env = "ET_API_SERVER_ADDR",
|
|
default_value = "0.0.0.0",
|
|
help = t!("cli.api_server_addr").to_string(),
|
|
)]
|
|
api_server_addr: IpAddr,
|
|
|
|
#[arg(
|
|
long,
|
|
env = "ET_GEOIP_DB",
|
|
help = t!("cli.geoip_db").to_string(),
|
|
)]
|
|
geoip_db: Option<String>,
|
|
|
|
#[arg(
|
|
long,
|
|
env = "ET_HEARTBEAT_MIN_RESPONSE_MS",
|
|
default_value = "3500",
|
|
help = t!("cli.heartbeat_min_response_ms").to_string(),
|
|
)]
|
|
heartbeat_min_response_ms: u64,
|
|
|
|
#[arg(
|
|
long,
|
|
env = "ET_HEARTBEAT_TIMEOUT_MS",
|
|
default_value = "15000",
|
|
help = t!("cli.heartbeat_timeout_ms").to_string(),
|
|
)]
|
|
heartbeat_timeout_ms: u64,
|
|
|
|
#[arg(long, env = "ET_GATEWAY_PEER_URL")]
|
|
gateway_peer_url: Option<url::Url>,
|
|
|
|
#[arg(long, env = "ET_GATEWAY_RELAY_DATA", default_value = "false")]
|
|
gateway_relay_data: bool,
|
|
|
|
#[cfg(feature = "embed")]
|
|
#[arg(
|
|
long,
|
|
short='l',
|
|
env = "ET_WEB_SERVER_PORT",
|
|
help = t!("cli.web_server_port").to_string(),
|
|
)]
|
|
web_server_port: Option<u16>,
|
|
|
|
#[cfg(feature = "embed")]
|
|
#[arg(
|
|
long,
|
|
env = "ET_WEB_SERVER_ADDR",
|
|
default_value = "0.0.0.0",
|
|
help = t!("cli.web_server_addr").to_string(),
|
|
)]
|
|
web_server_addr: IpAddr,
|
|
|
|
#[cfg(feature = "embed")]
|
|
#[arg(
|
|
long,
|
|
env = "ET_NO_WEB",
|
|
help = t!("cli.no_web").to_string(),
|
|
default_value = "false"
|
|
)]
|
|
no_web: bool,
|
|
|
|
#[cfg(feature = "embed")]
|
|
#[arg(
|
|
long,
|
|
env = "ET_API_HOST",
|
|
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, env = "ET_WEBHOOK_URL")]
|
|
pub webhook_url: Option<String>,
|
|
|
|
/// Shared secret used to authenticate outbound webhook calls.
|
|
#[arg(long, env = "ET_WEBHOOK_SECRET", hide_env_values = true)]
|
|
pub webhook_secret: Option<String>,
|
|
|
|
/// Token for X-Internal-Auth header. When set, API requests with this header
|
|
/// bypass session authentication.
|
|
#[arg(long, env = "ET_INTERNAL_AUTH_TOKEN", hide_env_values = true)]
|
|
pub internal_auth_token: Option<String>,
|
|
|
|
/// Stable identifier for this easytier-web instance when routing webhook callbacks.
|
|
#[arg(long, env = "ET_WEB_INSTANCE_ID")]
|
|
pub web_instance_id: Option<String>,
|
|
|
|
/// Reachable base URL for this easytier-web instance's internal REST API.
|
|
#[arg(long, env = "ET_WEB_INSTANCE_API_BASE_URL")]
|
|
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,
|
|
env = "ET_DISABLE_REGISTRATION",
|
|
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,
|
|
env = "ET_ALLOW_AUTO_CREATE_USER",
|
|
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,
|
|
}
|
|
}
|
|
}
|
|
|
|
type ConfigServerListener = Box<dyn SocketListener<Accepted = Box<dyn Tunnel>>>;
|
|
|
|
fn get_config_server_listeners(
|
|
protocol: &str,
|
|
port: u16,
|
|
) -> Result<Vec<ConfigServerListener>, Error> {
|
|
let scheme = protocol
|
|
.parse()
|
|
.map_err(|_| Error::InvalidUrl(protocol.to_string()))?;
|
|
// Wildcard binds do not require a default route or a public IP address.
|
|
let ipv6 = SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), port);
|
|
let ipv4 = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port);
|
|
match scheme {
|
|
IpScheme::Tcp => Ok(vec![
|
|
Box::new(runtime_rpc_listener(ipv6)),
|
|
Box::new(runtime_rpc_listener(ipv4)),
|
|
]),
|
|
IpScheme::Udp => Ok(vec![
|
|
Box::new(runtime_udp_tunnel_listener(
|
|
format!("udp://{ipv6}").parse().unwrap(),
|
|
ipv6,
|
|
)),
|
|
Box::new(runtime_udp_tunnel_listener(
|
|
format!("udp://{ipv4}").parse().unwrap(),
|
|
ipv4,
|
|
)),
|
|
]),
|
|
IpScheme::Ws => Ok(vec![Box::new(WsTunnelListener::new(
|
|
format!("ws://{ipv4}").parse().unwrap(),
|
|
))]),
|
|
_ => Err(Error::InvalidUrl(protocol.to_string())),
|
|
}
|
|
}
|
|
|
|
#[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();
|
|
easytier::utils::init_crypto_provider();
|
|
|
|
let cli = Cli::parse();
|
|
log::init_with_default_console_targets(&cli, false, &["CORE", "easytier_web"]).unwrap();
|
|
tracing::info!(
|
|
version = EASYTIER_VERSION,
|
|
web_instance_id = ?cli.webhook.web_instance_id,
|
|
api_address = %cli.api_server_addr,
|
|
api_port = cli.api_server_port,
|
|
config_protocol = %cli.config_server_protocol,
|
|
config_port = cli.config_server_port,
|
|
heartbeat_min_response_ms = cli.heartbeat_min_response_ms,
|
|
heartbeat_timeout_ms = cli.heartbeat_timeout_ms,
|
|
webhook_enabled = cli.webhook.webhook_url.as_deref().is_some_and(|url| !url.trim().is_empty()),
|
|
rust_log_override = std::env::var_os("RUST_LOG").is_some(),
|
|
console_log_override = cli.console_log_level.is_some(),
|
|
"easytier-web starting"
|
|
);
|
|
|
|
// 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 mut webhook_config = 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 central_mode = !webhook_config.has_external_endpoint();
|
|
if central_mode {
|
|
webhook_config =
|
|
webhook_config.with_handler(Arc::new(central_network::device_auth::DeviceAuth::new(
|
|
db.clone(),
|
|
feature_flags.allow_auto_create_user,
|
|
)));
|
|
} else if cli.gateway_peer_url.is_some() {
|
|
eprintln!("Gateway requires central network mode; remove --webhook-url");
|
|
std::process::exit(2);
|
|
}
|
|
let webhook_config = Arc::new(webhook_config);
|
|
let heartbeat_policy = client_manager::HeartbeatPolicy::from_millis(
|
|
cli.heartbeat_min_response_ms,
|
|
cli.heartbeat_timeout_ms,
|
|
)
|
|
.unwrap_or_else(|error| {
|
|
eprintln!("Invalid heartbeat configuration: {error}");
|
|
std::process::exit(2);
|
|
});
|
|
|
|
let network_instances = cli.gateway_peer_url.as_ref().map(|peer_url| {
|
|
let config = gateway::GatewayConfig {
|
|
peer_url: peer_url.to_string(),
|
|
relay_data: cli.gateway_relay_data,
|
|
};
|
|
config
|
|
.validate(&cli.config_server_protocol)
|
|
.unwrap_or_else(|error| {
|
|
eprintln!("Invalid Gateway configuration: {error}");
|
|
std::process::exit(2);
|
|
});
|
|
Arc::new(gateway::NetworkInstanceManager::new(config))
|
|
});
|
|
|
|
let mut mgr = client_manager::ClientManager::new(
|
|
db.clone(),
|
|
cli.geoip_db,
|
|
heartbeat_policy,
|
|
feature_flags.clone(),
|
|
webhook_config.clone(),
|
|
);
|
|
for listener in
|
|
get_config_server_listeners(&cli.config_server_protocol, cli.config_server_port).unwrap()
|
|
{
|
|
let local_url = listener.local_url();
|
|
let result = if let Some(instances) = network_instances.as_ref() {
|
|
mgr.add_listener(gateway::listener::GatewayListener::new(
|
|
listener,
|
|
instances.clone(),
|
|
))
|
|
.await
|
|
} else {
|
|
mgr.add_listener(listener).await
|
|
};
|
|
match result {
|
|
Ok(local_url) => {
|
|
tracing::info!(%local_url, "Config server listener bound");
|
|
}
|
|
Err(error) => {
|
|
tracing::warn!(%local_url, error = %format_args!("{error:#}"), "Failed to bind config server listener");
|
|
}
|
|
}
|
|
}
|
|
if !mgr.is_running() {
|
|
panic!(
|
|
"Failed to bind any config server listener for {} on port {}",
|
|
cli.config_server_protocol, cli.config_server_port
|
|
);
|
|
}
|
|
|
|
let mgr = Arc::new(mgr);
|
|
let central_service = Arc::new(
|
|
central_network::service::CentralNetworkService::with_gateway(
|
|
db.clone(),
|
|
mgr.clone(),
|
|
network_instances,
|
|
),
|
|
);
|
|
if central_mode {
|
|
central_service.start_reconciler();
|
|
}
|
|
|
|
#[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(),
|
|
central_service,
|
|
db,
|
|
web_router_restful,
|
|
feature_flags,
|
|
oidc_config,
|
|
webhook_config,
|
|
cli.config_server_protocol.clone(),
|
|
cli.config_server_port,
|
|
)
|
|
.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();
|
|
}
|