async fn run( data_dir: PathBuf, startup_admin_addr: SocketAddr, options: ServeOptions, startup_locks: rust_proxy::settings::StartupLocks, update_check: rust_proxy::update_check::Config, ) -> Result<()> { ensure!( startup_admin_addr.port() > 0 && options.proxy_listen.port() > 0, "listener ports must be nonzero" ); ensure!( (1..=4096).contains(&options.max_connections) && (1..=4096).contains(&options.max_inflight), "limits must be 1..4096" ); ensure!( options.upstream_timeout_secs > 0 && options.body_idle_timeout_secs > 0 && options.shutdown_timeout_secs > 0 && options.websocket_idle_timeout_secs > 0, "timeouts must be positive" ); ensure!( startup_admin_addr.port() != options.proxy_listen.port(), "admin and proxy ports must differ" ); if let Some(tls) = options.tls_listen { ensure!( tls.port() > 0 && tls.port() != options.proxy_listen.port() && tls.port() != startup_admin_addr.port(), "TLS listener port must be nonzero and differ from HTTP/admin" ); } if options.demo { ensure!( options.demo_listen.ip().is_loopback() && options.demo_listen.port() > 0, "demo must bind to a nonzero loopback port" ); ensure!( options.demo_listen.port() != startup_admin_addr.port() && options.demo_listen.port() != options.proxy_listen.port() && options .tls_listen .is_none_or(|tls| tls.port() != options.demo_listen.port()), "demo port must differ from proxy/admin" ); } auth::prepare_directory(&data_dir)?; let _process_lock = auth::lock_directory(&data_dir)?; auth::prepare_database(&data_dir.join("config.sqlite3"))?; let startup_listeners = (options.proxy_listen, options.tls_listen); let initial_listeners = rust_proxy::settings::Listeners::from_startup(options.proxy_listen, options.tls_listen); let initial = if options.demo { let mut config = config::demo_config(options.demo_listen); config.listeners = initial_listeners; config.vhosts[0].frontend.listener_ids = config .listeners .endpoints .iter() .map(|listener| listener.id.clone()) .collect(); config } else { let mut config = Config { listeners: initial_listeners, ..Config::default() }; // Only a NEW database receives this neutral landing page. config.pages.direct_ip = Some(Default::default()); config.pages.unknown_host = Some(Default::default()); config }; let store = Store::open(&data_dir.join("config.sqlite3"), &initial).await?; let mut saved = store.load().await?; startup_locks.ensure_config(&mut saved.config); startup_locks .validate(&saved.config, startup_admin_addr) .context("stored configuration conflicts with a listener fixed by systemd/startup")?; let admin_addr = saved .config .administration .resolved_listen(startup_admin_addr); let admin_tls_enabled = saved.config.administration.tls; let remote_admin = validate_admin_binding(admin_addr, admin_tls_enabled, options.allow_remote_admin)?; ensure!( admin_addr.port() != options.proxy_listen.port() && options .tls_listen .is_none_or(|tls| tls.port() != admin_addr.port()) && (!options.demo || options.demo_listen.port() != admin_addr.port()), "active administration port conflicts with another startup listener" ); let resolved_policy = ValidationPolicy { proxy_addr: options.proxy_listen, admin_addr, tls_addr: options.tls_listen, cert_dir: None, data_dir: Some(data_dir.clone()), }; saved.config.listeners.validate(&resolved_policy)?; // The certificate library is available even on an HTTP-only installation. // Initializing private files does not bind or expose a TLS listener. let directory = data_dir.join("certs"); let initialize = directory.clone(); tokio::task::spawn_blocking(move || rust_proxy::tls::initialize_default(&initialize)).await??; rust_proxy::secret_store::initialize(&data_dir)?; let migrated = rust_proxy::secret_store::migrate_acme_credentials(&store, &data_dir, &directory).await?; if migrated > 0 { tracing::info!( migrated, "legacy ACME credentials migrated to encrypted Secret Store" ); } let cert_dir = Some(directory.clone()); let policy = ValidationPolicy { proxy_addr: options.proxy_listen, admin_addr, tls_addr: options.tls_listen, cert_dir, data_dir: Some(data_dir.clone()), }; let validation_policy = policy.clone(); let snapshot = tokio::task::spawn_blocking(move || { config::compile(saved.config, saved.revision, &validation_policy) }) .await? .context("stored configuration is invalid; no automatic reset was performed")?; rust_proxy::logging::configure_error_log(snapshot.envelope.config.error_log.clone()); let admin_tls_config = { let tls = snapshot.envelope.config.tls.clone(); let administration = snapshot.envelope.config.administration.clone(); let cert_dir = policy.cert_dir.clone(); tokio::task::spawn_blocking(move || { rust_proxy::tls::compile_admin(&tls, &administration, cert_dir.as_deref()) }) .await?? }; let setup_required = store.admin_record().await?.is_none(); let authentication = auth::Auth::new(store.clone(), data_dir.clone()).await?; if !setup_required { authentication.remove_bootstrap_files().await; } let renewal_observations = store.load_certificate_renewals().await?; let geoip = match rust_proxy::geoip::Database::load(&data_dir.join("geoip")) { Ok(database) => std::sync::Arc::new(database), Err(error) => { tracing::warn!(%error, "GeoIP country database unavailable; country allow lists fail closed"); std::sync::Arc::new(rust_proxy::geoip::Database::empty()) } }; let state = App::new_with_startup_locks_geoip( store.clone(), snapshot, policy, authentication, options.max_inflight, Duration::from_secs(options.upstream_timeout_secs), Duration::from_secs(options.body_idle_timeout_secs), Duration::from_secs(options.websocket_idle_timeout_secs), startup_listeners, startup_locks, geoip, ); state.acme.restore_renewals(renewal_observations); state.start_update_check(update_check); // Bind EVERY listener before spawning servers: a missing private IP or // occupied port fails startup atomically, never falling back to 0.0.0.0. let mut public_listeners = Vec::new(); for binding in &state.active_listeners { if options.demo { ensure!( !rust_proxy::vhost::socket_overlap(binding.address, options.demo_listen), "vhost listener conflicts with demo backend" ); } let listener = TcpListener::bind(binding.address).await.with_context(|| { format!( "cannot bind frontend {} (TLS={}); check local IP, port permissions and conflicts", binding.address, binding.tls ) })?; public_listeners.push((*binding, listener)); } let mut quic_listeners = Vec::new(); for binding in &state.active_listeners { if binding.http3.is_some() { quic_listeners.push(rust_proxy::http3::bind(*binding, &state)?); } } let mut tcp_stream_listeners = Vec::new(); let mut udp_stream_listeners = Vec::new(); for stream in state.active_streams.iter().filter(|stream| stream.enabled) { let stream_metrics = state .stream_metrics .get(&stream.id) .cloned() .context("missing stream metrics registry entry")?; if options.demo && stream.protocol == rust_proxy::stream::Protocol::Tcp { ensure!( !rust_proxy::vhost::socket_overlap(stream.listen, options.demo_listen), "TCP stream listener conflicts with demo backend" ); } match stream.protocol { rust_proxy::stream::Protocol::Tcp => { let listener = TcpListener::bind(stream.listen).await.with_context(|| { format!("cannot bind TCP stream {} on {}", stream.id, stream.listen) })?; tcp_stream_listeners.push((stream.clone(), listener, stream_metrics)); } rust_proxy::stream::Protocol::Udp => { let socket = UdpSocket::bind(stream.listen).await.with_context(|| { format!("cannot bind UDP stream {} on {}", stream.id, stream.listen) })?; udp_stream_listeners.push((stream.clone(), socket, stream_metrics)); } } } let metrics_token = if state.active_prometheus.enabled { state.active_prometheus.token()? } else { None }; let metrics_listener = if state.active_prometheus.enabled { ensure!( !options.demo || !rust_proxy::vhost::socket_overlap( state.active_prometheus.listen, options.demo_listen ), "metrics exporter overlaps demo origin" ); Some( TcpListener::bind(state.active_prometheus.listen) .await .context("cannot bind Prometheus exporter")?, ) } else { None }; let admin_listener = TcpListener::bind(admin_addr) .await .context("cannot bind admin listener")?; let demo_listener = if options.demo { Some( TcpListener::bind(options.demo_listen) .await .context("cannot bind demo backend")?, ) } else { None }; let shutdown = state.shutdown.clone(); let grace = Duration::from_secs(options.shutdown_timeout_secs); let mut tasks = JoinSet::new(); let connection_slots = std::sync::Arc::new(tokio::sync::Semaphore::new(options.max_connections)); for (binding, listener) in public_listeners { tasks.spawn(rust_proxy::server::serve_frontend_with_http2( listener, rust_proxy::proxy::router(state.clone()), shutdown.clone(), connection_slots.clone(), grace, state.metrics.clone(), binding.tls.then(|| (state.clone(), binding.http2)), binding.h2c, Some(state.clone()), binding.http2_settings.unwrap_or_default(), )); } for listener in quic_listeners { tasks.spawn(rust_proxy::http3::serve( listener, rust_proxy::proxy::router(state.clone()), state.clone(), grace, )); } for (stream, listener, metrics) in tcp_stream_listeners { tasks.spawn(rust_proxy::stream::serve_tcp( stream, listener, shutdown.clone(), grace, metrics, )); } for (stream, socket, metrics) in udp_stream_listeners { tasks.spawn(rust_proxy::stream::serve_udp( stream, socket, shutdown.clone(), metrics, )); } if let Some(tls) = admin_tls_config { tasks.spawn(rust_proxy::server::serve_tls_with_plain_redirect( admin_listener, rust_proxy::admin::router(state.clone()), rust_proxy::admin::https_redirect_router(), shutdown.clone(), 32, grace, tls, )); } else { tasks.spawn(rust_proxy::server::serve( admin_listener, rust_proxy::admin::router(state.clone()), shutdown.clone(), 32, grace, None, )); } if let Some(listener) = metrics_listener { tasks.spawn(rust_proxy::server::serve( listener, rust_proxy::prometheus::router(state.clone(), metrics_token), shutdown.clone(), 16, grace, None, )); } if let Some(listener) = demo_listener { tasks.spawn(rust_proxy::server::serve( listener, rust_proxy::demo::router(), shutdown.clone(), 256, grace, None, )); } println!("\n RustProxy {}\n", env!("CARGO_PKG_VERSION")); for binding in &state.active_listeners { println!( " Frontend: {}://{} ({})", if binding.tls { "https" } else { "http" }, binding.address, if binding.http3.is_some() && binding.http2 { "HTTP/3 (UDP) + HTTP/2 + HTTP/1.1" } else if binding.http3.is_some() { "HTTP/3 (UDP) + HTTP/1.1" } else if binding.tls && binding.http2 { "HTTP/2 + HTTP/1.1" } else if binding.h2c { "h2c prior-knowledge + HTTP/1.1" } else { "HTTP/1.1" } ); } for stream in state.active_streams.iter().filter(|stream| stream.enabled) { println!( " Stream: {}://{} -> {} ({})", match stream.protocol { rust_proxy::stream::Protocol::Tcp => "tcp", rust_proxy::stream::Protocol::Udp => "udp", }, stream.listen, stream.upstream, stream.id ); } println!( " Panel: {}://{admin_addr}", if admin_tls_enabled { "https" } else { "http" } ); if remote_admin && !admin_tls_enabled { tracing::warn!( "REMOTE ADMIN ENABLED OVER PLAIN HTTP: credentials and session cookies can be exposed; restrict access with a firewall/private network and prefer a protected tunnel" ); } if state.active_prometheus.enabled { println!( " Metrics: http://{}{}", state.active_prometheus.listen, state.active_prometheus.path ); } if setup_required { println!( " Setup: rust-proxy --data-dir {} admin setup-token", data_dir.display() ); println!( " Or: rust-proxy --data-dir {} admin init --username YOUR_NAME", data_dir.display() ); } else { println!(" Sign in with your administrator account."); } println!(" Data: {}\n Stop: Ctrl+C\n", data_dir.display()); if state .active_listeners .iter() .any(|l| !l.address.ip().is_loopback()) { tracing::warn!("public binding enabled: development release; configure host/route ACLs and trusted certificates before exposure"); } let result = tokio::select! { result = shutdown_signal() => result, result = tasks.join_next() => match result { Some(Ok(Ok(()))) => Err(anyhow::anyhow!("listener stopped unexpectedly")), Some(Ok(Err(error))) => Err(error), Some(Err(error)) => Err(error.into()), None => Err(anyhow::anyhow!("all listeners stopped")), }, }; shutdown.cancel(); while tasks.join_next().await.is_some() {} state.tunnels.close(); let _ = tokio::time::timeout(grace, state.tunnels.wait()).await; state.drain_updates().await; let managed_update = data_dir.join(".managed-update").is_file(); let cache_flush_budget = if managed_update { std::cmp::max(grace, Duration::from_secs(90)) } else { grace }; let flush_result = if managed_update { tokio::time::timeout( cache_flush_budget, state.cache.flush_hot_to_cold_for_update(), ) .await } else { tokio::time::timeout(cache_flush_budget, state.cache.flush_hot_to_cold()).await }; match flush_result { Ok(moved) if moved > 0 && managed_update => tracing::info!( moved, "managed update moved hot cache objects to cold storage" ), Ok(moved) if moved > 0 => tracing::info!( moved, "flushed hot cache objects to cold storage before shutdown" ), Err(_) if managed_update => { tracing::warn!("managed-update hot-to-cold cache handoff exceeded its shutdown budget") } Err(_) => tracing::warn!("cold-cache shutdown flush exceeded graceful shutdown timeout"), _ => {} } store.close().await; result }