This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-server / src / main.rs
29 kB 752 lines
1mod allocator; 2 3#[global_allocator] 4static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; 5 6#[allow(non_upper_case_globals)] 7#[unsafe(export_name = "_rjem_malloc_conf")] 8pub static malloc_conf: &[u8] = 9 b"background_thread:true,retain:false,dirty_decay_ms:0,muzzy_decay_ms:0\0"; 10 11use std::collections::BTreeSet; 12use std::num::{NonZeroU32, NonZeroU64}; 13use std::path::PathBuf; 14use std::sync::Arc; 15use std::time::Duration; 16 17use tokio_util::sync::CancellationToken; 18 19use anyhow::Context; 20use axum::Json; 21use axum::response::Html; 22use axum::routing::get; 23use base64::Engine; 24use knot_atproto::Atproto; 25use knot_config::HomepageSource; 26use knot_index::Index; 27use knot_runtime::{Clock, HttpTransport, OsEntropy, ReqwestHttp, SystemClock}; 28use knot_secrets::{MasterKey, SealedStore}; 29use knot_types::{ActorId, AuthorName, BranchName, CiLogsAddr, Email, KnotHostname, ObjectCount}; 30use knot_xrpc::XrpcState; 31use tower_http::services::ServeFile; 32 33const MAINTENANCE_SHUTDOWN_DRAIN: Duration = Duration::from_secs(30); 34const EDGE_SHUTDOWN_DRAIN: Duration = Duration::from_secs(40); 35 36const DEFAULT_HOMEPAGE: &str = include_str!("homepage.html"); 37 38struct IndexRepos(Arc<Index>); 39 40impl knot_maintenance::RepoSource for IndexRepos { 41 fn repos(&self) -> Vec<knot_types::RepoDid> { 42 self.0.hosted_repos() 43 } 44 45 fn ready_repos(&self) -> Option<Vec<knot_types::RepoDid>> { 46 match self.0.coverage().registry { 47 knot_index::Coverage::Ready => Some(self.0.hosted_repos()), 48 knot_index::Coverage::Warming => None, 49 } 50 } 51} 52 53struct AtprotoHandleResolver<H, C> { 54 atproto: Arc<Atproto<H, C>>, 55} 56 57impl<H: HttpTransport, C: Clock> knot_pack::HandleResolver for AtprotoHandleResolver<H, C> { 58 fn resolve( 59 &self, 60 handle: knot_types::Handle, 61 ) -> std::pin::Pin< 62 Box<dyn std::future::Future<Output = Option<knot_types::AccountDid>> + Send + '_>, 63 > { 64 Box::pin(async move { self.atproto.resolve_handle_to_did(&handle).await.ok() }) 65 } 66} 67 68fn init_tracing() { 69 let filter = tracing_subscriber::EnvFilter::try_from_default_env() 70 .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")); 71 tracing_subscriber::fmt() 72 .with_env_filter(filter) 73 .with_writer(std::io::stderr) 74 .init(); 75} 76 77fn subcommand(name: &str) -> Option<anyhow::Result<()>> { 78 match name { 79 "config-template" => { 80 print!("{}", knot_config::template()); 81 Some(Ok(())) 82 } 83 "validate" => Some( 84 knot_config::load(std::env::args().nth(2).map(PathBuf::from).as_deref()) 85 .context("load configuration") 86 .and_then(|config| { 87 config 88 .verify_environment() 89 .context("verify runtime environment") 90 }) 91 .map(|()| println!("configuration is valid")), 92 ), 93 _ => None, 94 } 95} 96 97#[tokio::main] 98async fn main() -> anyhow::Result<()> { 99 #[cfg(target_os = "linux")] 100 rustix::process::set_dumpable_behavior(rustix::process::DumpableBehavior::NotDumpable) 101 .context("disable core dumps and ptrace attachment")?; 102 103 if let Some(result) = std::env::args().nth(1).as_deref().and_then(subcommand) { 104 return result; 105 } 106 107 init_tracing(); 108 109 tracing::info!("!"); 110 tracing::info!("!"); 111 tracing::info!("!"); 112 tracing::info!("> If knot1 was so good then why isn't there a... ( ˶°ㅁ°)"); 113 tracing::info!("..."); 114 tracing::info!("Welcome to knot2!"); 115 tracing::info!("This code was made with love."); 116 tracing::info!("Hachapuri is sho tasty, definitely worth a try. Better than pizza tbh."); 117 tracing::info!("!"); 118 tracing::info!("!"); 119 tracing::info!("!"); 120 121 let config_path = std::env::args().nth(1).map(PathBuf::from); 122 let config = knot_config::load(config_path.as_deref()).context("load configuration")?; 123 config 124 .verify_environment() 125 .context("verify runtime environment")?; 126 127 let resources = knot_resource::init(knot_resource::Ceilings { 128 max_threads: match config.resources.max_threads { 129 0 => None, 130 n => Some(knot_resource::ThreadCount::new(n as usize)), 131 }, 132 max_memory: match config.resources.max_memory_bytes { 133 0 => None, 134 n => Some(knot_resource::MemoryBudget::new(n)), 135 }, 136 }); 137 tracing::info!( 138 threads = resources.threads.get(), 139 memory_bytes = resources.memory.map(knot_resource::MemoryBudget::get), 140 memory_source = ?resources.memory_source, 141 memory_high_bytes = resources 142 .memory_high_bytes 143 .map(knot_resource::MemoryHighBytes::get), 144 "resource governor initialized" 145 ); 146 allocator::install(); 147 148 // I made the hostname stay aa string in the config, 149 // so that confique can layer env-over-file. 150 // Here's where it becomes a real type, 151 // and the DID + the service url stack on this. 152 let hostname = KnotHostname::new(config.server.hostname.clone()) 153 .context("server.hostname isn't a valid knot hostname")?; 154 let knot_did = hostname.knot_did(); 155 let object_format = config.object_format().context("parse git.object_format")?; 156 let default_branch = 157 BranchName::new(config.repo.default_branch.as_str()).context("parse default branch")?; 158 let layout = knot_git::Layout::new(&config.repo.scan_path) 159 .with_default_branch(default_branch) 160 .with_object_format(object_format) 161 .reserving_meta(&knot_did) 162 .context("reserve meta-repo path")?; 163 164 let swept = knot_pack::sweep_incoming(&config.repo.scan_path); 165 if swept > 0 { 166 tracing::info!(swept, "swept abandoned receive staging directories"); 167 } 168 169 layout 170 .bootstrap_meta(&knot_did) 171 .context("bootstrap meta-repo")?; 172 let meta_path = layout 173 .meta_path(&knot_did) 174 .context("resolve meta-repo path")?; 175 176 let index = Arc::new(Index::new(meta_path.clone(), layout.clone())); 177 index.rebuild().context("rebuild index from meta-repo")?; 178 tracing::info!(coverage = ?index.coverage(), "index ready"); 179 180 let warm = Arc::clone(&index); 181 tokio::task::spawn_blocking(move || warm.warm_collaborators()); 182 183 let http = ReqwestHttp::new(config.http_limits()).context("build outbound HTTP client")?; 184 let git_http: Arc<dyn knot_runtime::HttpTransport> = Arc::new( 185 ReqwestHttp::new(config.fork_http_limits()).context("build outbound git fetch client")?, 186 ); 187 let atproto = Arc::new(Atproto::new( 188 http, 189 SystemClock, 190 knot_did.clone(), 191 knot_atproto::PlcDirectory::new(config.atproto.plc_directory.clone()) 192 .context("atproto.plc_directory isn't a valid PLC base URL")?, 193 )); 194 let admins: BTreeSet<_> = config.server.admins.iter().cloned().collect(); 195 let admission = config.acl.admission; 196 let service_owner = config 197 .server 198 .admins 199 .first() 200 .cloned() 201 .context("at least one admin is configured")?; 202 203 let master_key = MasterKey::new( 204 base64::engine::general_purpose::STANDARD 205 .decode( 206 std::env::var(&config.secrets.master_key_env) 207 .context("read master key from environment")? 208 .trim(), 209 ) 210 .context("decode master key as base64")?, 211 ) 212 .context("master key from environment")?; 213 let secrets = Arc::new( 214 SealedStore::open( 215 &config.secrets.sealed_key_file, 216 &master_key, 217 Box::new(OsEntropy), 218 ) 219 .context("open sealed key store")?, 220 ); 221 let knot_signing_key = secrets 222 .ensure(&knot_did) 223 .context("seal knot's own signing key")?; 224 let knot_actor = ActorId::from_secp256k1(knot_signing_key.as_bytes()); 225 let appview_endpoint = config.server.appview_endpoint.clone(); 226 let knot_service_url = knot_types::KnotServiceUrl::new(format!("https://{hostname}")) 227 .context("server.hostname doesn't form a valid knot service URL")?; 228 let did_document = 229 knot_atproto::knot_did_document(&knot_did, &knot_signing_key, &knot_service_url); 230 231 let http_addr = config.server.listen_addr; 232 let listen_limits = knot_edge::ListenLimits::new( 233 knot_edge::HeaderTimeout::from_millis( 234 NonZeroU64::new(config.server.listen_header_timeout_ms) 235 .context("server.listen_header_timeout_ms must be greater than zero")?, 236 ), 237 knot_edge::IdleTimeout::from_millis( 238 NonZeroU64::new(config.server.listen_idle_timeout_ms) 239 .context("server.listen_idle_timeout_ms must be greater than zero")?, 240 ), 241 NonZeroU32::new(config.server.listen_max_connections) 242 .context("server.listen_max_connections must be greater than zero")?, 243 ); 244 // A header name that doesn't parse will never match, 245 // `effective_peer` falls back to socket, 246 // and every request in the world shares 247 // the proxy's address + its one ratelimit bucket. 248 // So... better to refuse to start. 249 let trusted_proxy_header = config 250 .xrpc 251 .trusted_proxy_header 252 .as_deref() 253 .map(|header| axum::http::HeaderName::from_bytes(header.as_bytes())) 254 .transpose() 255 .context("xrpc.trusted_proxy_header isn't a valid HTTP header name")?; 256 let edge_guards = knot_edge::EdgeGuards::new( 257 knot_edge::RequestsPerSecond::new( 258 NonZeroU32::new(config.server.listen_rate_limit_per_second) 259 .context("server.listen_rate_limit_per_second must be greater than zero")?, 260 ), 261 knot_edge::BurstSize::new( 262 NonZeroU32::new(config.server.listen_rate_limit_burst) 263 .context("server.listen_rate_limit_burst must be greater than zero")?, 264 ), 265 knot_edge::MaxInflightRequests::new( 266 NonZeroU32::new(config.server.listen_max_inflight_requests) 267 .context("server.listen_max_inflight_requests must be greater than zero")?, 268 ), 269 knot_edge::RequestTimeout::from_millis( 270 NonZeroU64::new(config.server.listen_request_timeout_ms) 271 .context("server.listen_request_timeout_ms must be greater than zero")?, 272 ), 273 knot_edge::BodyInactivityTimeout::from_millis( 274 NonZeroU64::new(config.server.listen_body_timeout_ms) 275 .context("server.listen_body_timeout_ms must be greater than zero")?, 276 ), 277 knot_edge::WriteRequestTimeout::from_millis( 278 NonZeroU64::new(config.server.listen_write_request_timeout_ms) 279 .context("server.listen_write_request_timeout_ms must be greater than zero")?, 280 ), 281 trusted_proxy_header.clone(), 282 ); 283 let tls_setup = build_tls_setup(&config, &hostname).context("assemble TLS configuration")?; 284 if config.tls.http3 && tls_setup.is_none() { 285 tracing::warn!( 286 "tls.http3 is set without any TLS certificate, so HTTP/3 won't start. Configure a static cert or ACME to serve h3." 287 ); 288 } 289 if tls_setup.is_none() && config.xrpc.trusted_proxy_header.is_none() { 290 tracing::warn!( 291 "running plaintext behind a reverse proxy without xrpc.trusted_proxy_header. Per-IP rate limiting will key on the proxy socket address, throttling all clients as one. Set xrpc.trusted_proxy_header to the header your proxy appends." 292 ); 293 } 294 if config.tls.acme_enabled && http_addr.port() != 443 { 295 tracing::warn!( 296 listen_port = http_addr.port(), 297 "ACME validation over TLS-ALPN-01 needs the certificate authority to reach this host on TCP 443. Map 443 to the listen port if it differs." 298 ); 299 } 300 if config.tls.acme_enabled && config.tls.acme_staging { 301 tracing::warn!( 302 "ACME is using the Let's Encrypt staging directory. Its certificates aren't browser-trusted. Unset tls.acme_staging for real certificates." 303 ); 304 } 305 let ssh_addr = config.server.ssh_listen_addr; 306 let ssh_max_pack_bytes = config.server.ssh_max_pack_bytes as usize; 307 let pack_limits = knot_pack::PackLimits { 308 max_objects: ObjectCount::from(config.pack.max_objects), 309 max_total_bytes: knot_pack::MaxTotalBytes::new(config.pack.max_total_bytes), 310 ..knot_pack::PackLimits::default() 311 }; 312 knot_pack::init_selection_limits(knot_pack::SelectionLimits { 313 max_objects: ObjectCount::from(config.pack.selection_max_objects), 314 time_budget: Duration::from_secs(config.pack.selection_time_budget_secs), 315 }); 316 let host_key = knot_ssh::load_or_create_host_key(&config.server.ssh_host_key_file) 317 .context("load or create SSH host key")?; 318 319 let xrpc_limits = knot_xrpc::LimitConfig { 320 rate: Some(knot_xrpc::RateLimit { 321 burst: knot_xrpc::Burst::new(config.xrpc.preauth_burst), 322 refill: knot_xrpc::RefillMicros::new( 323 config.xrpc.preauth_refill_ms.saturating_mul(1_000), 324 ), 325 }), 326 per_peer_inflight: Some(knot_xrpc::PerPeerInflight::new( 327 config.xrpc.per_peer_inflight as usize, 328 )), 329 global_inflight: Some(knot_xrpc::GlobalInflight::new( 330 config.xrpc.global_inflight as usize, 331 )), 332 }; 333 let byte_limits = knot_xrpc::ByteLimits { 334 body: knot_xrpc::BodyLimit::new(config.xrpc.max_body_bytes as usize), 335 patch: knot_xrpc::PatchLimit::new(config.xrpc.max_patch_bytes as usize), 336 patch_decompressed: knot_xrpc::PatchDecompressedLimit::new( 337 config.xrpc.max_patch_decompressed_bytes, 338 ), 339 response: knot_xrpc::ResponseLimit::new(config.xrpc.max_response_bytes as usize), 340 archive: knot_xrpc::ArchiveLimit::new(config.xrpc.max_archive_bytes), 341 fork_pack: knot_xrpc::ForkPackLimit::new(config.xrpc.fork_max_pack_bytes), 342 pack: knot_xrpc::MaxWireBytes::new(ssh_max_pack_bytes), 343 }; 344 let budgets = knot_xrpc::Budgets { 345 tree_last_commit: knot_xrpc::TreeReadBudget::new(knot_xrpc::ReadBudget::Within( 346 Duration::from_millis(config.xrpc.tree_last_commit_budget_ms), 347 )), 348 blob_last_commit: knot_xrpc::BlobReadBudget::new(knot_xrpc::ReadBudget::Within( 349 Duration::from_millis(config.xrpc.blob_last_commit_budget_ms), 350 )), 351 languages: knot_xrpc::LanguagesReadBudget::new(knot_xrpc::ReadBudget::Within( 352 Duration::from_millis(config.xrpc.languages_budget_ms), 353 )), 354 languages_push: knot_xrpc::LanguagesPushBudget::new(Duration::from_millis( 355 config.xrpc.languages_push_budget_ms, 356 )), 357 }; 358 let committer = knot_xrpc::Committer { 359 name: AuthorName::new(config.git.user_name.clone()), 360 email: Email::new(config.git.user_email.clone()), 361 }; 362 let reservations = Arc::new(knot_xrpc::Reservations::new( 363 knot_xrpc::ReservationTtl::new(config.xrpc.reservation_ttl_secs as i64), 364 knot_xrpc::PerActorQuota::new(config.xrpc.per_actor_reservations as usize), 365 knot_xrpc::GlobalQuota::new(config.xrpc.max_pending_reservations as usize), 366 )); 367 let replay_bounds = knot_events::ReplayBounds::new( 368 knot_events::ReplayEvents::new(config.xrpc.events_replay_buffer as usize) 369 .context("xrpc.events_replay_buffer must be greater than zero")?, 370 knot_events::ReplayBytes::new(config.xrpc.events_replay_bytes as usize) 371 .context("xrpc.events_replay_bytes must be greater than zero")?, 372 ); 373 let events = Arc::new(knot_events::EventLog::new(SystemClock, replay_bounds)); 374 let subscriber_gate = Arc::new(knot_events::SubscriberGate::new( 375 knot_events::GlobalSubscriberLimit::new(config.xrpc.events_max_subscribers as usize), 376 knot_events::PerPeerSubscriberLimit::new(config.xrpc.events_max_per_peer as usize), 377 )); 378 379 let maintenance_enabled = config.maintenance.enabled; 380 let maintenance_options = knot_maintenance::Options::from_config(&config.maintenance); 381 let maintenance_interval = Duration::from_secs(config.maintenance.interval_secs); 382 let maintenance_large_push = 383 knot_maintenance::PushBytes::new(config.maintenance.large_push_bytes); 384 385 let lfs_handle = config 386 .lfs 387 .store_path 388 .as_ref() 389 .map(|path| { 390 knot_lfs::LfsHandle::open( 391 knot_lfs::LfsStorePath::new(path), 392 knot_lfs::LfsSize::new(config.lfs.max_object_bytes), 393 knot_lfs::FreeSpaceFloor::new(config.lfs.free_space_floor_bytes), 394 ) 395 .inspect(|_| { 396 tracing::info!(store = %path.display(), "git-lfs capability enabled"); 397 }) 398 }) 399 .transpose() 400 .context("open LFS object store")?; 401 let lfs_max_ssh_transfers = config.lfs.max_ssh_transfers as usize; 402 let lfs_max_http_downloads = config.lfs.max_http_downloads as usize; 403 let lfs_gc_grace = knot_maintenance::lfs_grace( 404 knot_maintenance::GcGrace::from_secs(config.lfs.gc_grace_secs), 405 knot_maintenance::ReflogRetention::from_secs(config.maintenance.reflog_expire_secs), 406 ); 407 let lfs_gc_interval = 408 knot_maintenance::SweepInterval::new(Duration::from_secs(config.lfs.gc_interval_secs)); 409 if lfs_handle.is_some() && !maintenance_enabled { 410 tracing::warn!( 411 "LFS store configured but maintenance is disabled. Unreferenced LFS objects will accumulate with no garbage collection or orphan sweep." 412 ); 413 } 414 415 let pack_cache_config = knot_pack::CacheConfig { 416 enabled: config.pack_cache.enabled, 417 ttl: Duration::from_secs(config.pack_cache.ttl_secs), 418 max_entry_bytes: knot_pack::MaxEntryBytes::new(config.pack_cache.max_entry_bytes as usize), 419 max_total_bytes: knot_pack::MaxCacheBytes::new(knot_resource::pack_cache_bytes( 420 config.pack_cache.max_total_bytes, 421 ) as usize), 422 }; 423 424 let ci_logs = config 425 .ci 426 .logs_addr 427 .as_deref() 428 .map(CiLogsAddr::new) 429 .transpose() 430 .context("ci.logs_addr must be host:port")?; 431 432 let homepage = config.homepage.source(); 433 let catalog = Arc::new( 434 knot_messages::Catalog::parse(&config.messages).context("parse message templates")?, 435 ); 436 let legacy_admin = legacy_admin_secret(&config); 437 438 knot_config::init(config); 439 440 let (maintenance_handle, maintenance_shutdown, maintenance_task) = if maintenance_enabled { 441 let (scheduler, handle) = knot_maintenance::Scheduler::new( 442 layout.clone(), 443 Arc::new(IndexRepos(Arc::clone(&index))), 444 SystemClock, 445 maintenance_options, 446 maintenance_interval, 447 maintenance_large_push, 448 ); 449 let scheduler = match &lfs_handle { 450 Some(lfs) => { 451 scheduler.with_lfs_gc(Arc::clone(&lfs.store), lfs_gc_grace, lfs_gc_interval) 452 } 453 None => scheduler, 454 }; 455 let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false); 456 let task = tokio::spawn(scheduler.run(shutdown_rx)); 457 tracing::info!( 458 interval_secs = maintenance_interval.as_secs(), 459 "maintenance scheduler running" 460 ); 461 (handle, Some(shutdown_tx), Some(task)) 462 } else { 463 (knot_maintenance::MaintenanceHandle::disabled(), None, None) 464 }; 465 466 let slots = knot_resource::Slots::for_machine(); 467 468 let ssh_base = knot_ssh::SshState::new( 469 layout.clone(), 470 Arc::clone(&index), 471 Arc::clone(&atproto), 472 knot_actor, 473 Arc::clone(&events), 474 hostname.clone(), 475 appview_endpoint.clone(), 476 admins.clone(), 477 admission, 478 byte_limits.pack, 479 budgets.languages_push, 480 ci_logs.clone(), 481 ) 482 .with_maintenance(maintenance_handle.clone()) 483 .with_limits(pack_limits) 484 .with_slots(slots.clone()) 485 .with_catalog(Arc::clone(&catalog)); 486 let ssh_state = Arc::new(match &lfs_handle { 487 Some(handle) => ssh_base.with_lfs(handle.clone(), lfs_max_ssh_transfers), 488 None => ssh_base, 489 }); 490 491 let xrpc_state = Arc::new(XrpcState { 492 layout: layout.clone(), 493 index: Arc::clone(&index), 494 atproto: Arc::clone(&atproto), 495 secrets, 496 entropy: Arc::new(OsEntropy), 497 ci_logs, 498 admins, 499 admission, 500 knot_did, 501 knot_hostname: hostname, 502 meta_path, 503 knot_service_url, 504 limiter: Arc::new(knot_xrpc::PreAuthLimiter::with_config(xrpc_limits)), 505 cob_locks: Arc::new(knot_xrpc::CobLocks::default()), 506 reservations, 507 trusted_proxy_header, 508 committer, 509 byte_limits, 510 budgets, 511 git_http, 512 pack_limits, 513 service_owner, 514 subscriber_gate, 515 maintenance: maintenance_handle, 516 appview: appview_endpoint, 517 slots: slots.clone(), 518 events, 519 lfs: lfs_handle.map(|handle| knot_xrpc::LfsWeb::new(handle, lfs_max_http_downloads)), 520 catalog: Arc::clone(&catalog), 521 }); 522 523 let resolver: Arc<dyn knot_pack::RepoResolver> = { 524 let index = Arc::clone(&index); 525 Arc::new(move |target: &knot_pack::RepoTarget| match target { 526 knot_pack::RepoTarget::Did(did) => { 527 knot_pack::RepoLookup::from_resolved(index.owner_of(did), |_| did.clone()) 528 } 529 knot_pack::RepoTarget::OwnerRkey(owner, rkey) => { 530 knot_pack::RepoLookup::from_resolved(index.resolve_repo(owner, rkey), |found| found) 531 } 532 }) 533 }; 534 let receive_advertiser = knot_xrpc::receive_advertiser(Arc::clone(&xrpc_state)); 535 let handle_resolver: Arc<dyn knot_pack::HandleResolver> = Arc::new(AtprotoHandleResolver { 536 atproto: Arc::clone(&atproto), 537 }); 538 let (write_routes, early_data_safe) = knot_pack::edge_routes( 539 layout, 540 resolver, 541 Some(receive_advertiser), 542 Some(handle_resolver), 543 slots.pack.clone(), 544 pack_cache_config, 545 Arc::clone(&catalog), 546 xrpc_state.knot_hostname.clone(), 547 Arc::new(SystemClock), 548 ); 549 let legacy_admin_routes = legacy_admin.map(|secret| { 550 tracing::warn!( 551 route = knot_xrpc::legacy_admin::ADD_MEMBER_ROUTE, 552 "serving the legacy basic-auth admin route" 553 ); 554 knot_xrpc::legacy_admin::router(Arc::clone(&xrpc_state), secret) 555 }); 556 let base_router = write_routes.merge(knot_xrpc::router(xrpc_state)).route( 557 "/.well-known/did.json", 558 get(move || { 559 let document = did_document.clone(); 560 async move { Json(document) } 561 }), 562 ); 563 let base_router = match legacy_admin_routes { 564 Some(routes) => base_router.merge(routes), 565 None => base_router, 566 }; 567 let base_router = match homepage { 568 HomepageSource::Disabled => base_router, 569 HomepageSource::Default => base_router.route("/", get(|| async { Html(DEFAULT_HOMEPAGE) })), 570 HomepageSource::File(path) => base_router.route_service("/", ServeFile::new(path)), 571 }; 572 let app = knot_edge::RequiresFullHandshake::new(base_router); 573 let scheme = if tls_setup.is_some() { "https" } else { "http" }; 574 let edge_config = knot_edge::EdgeConfig { 575 http_addr: knot_edge::PublicBind::new(http_addr), 576 limits: listen_limits, 577 guards: edge_guards, 578 tls: tls_setup, 579 }; 580 tracing::info!("listening on {scheme}://{http_addr} and ssh://{ssh_addr}"); 581 582 let shutdown = CancellationToken::new(); 583 tokio::spawn(allocator::govern_decay(shutdown.clone())); 584 let mut edge_task = tokio::spawn(knot_edge::serve( 585 edge_config, 586 app, 587 early_data_safe, 588 shutdown.clone(), 589 )); 590 let mut ssh_task = { 591 let shutdown = shutdown.clone(); 592 tokio::spawn(async move { knot_ssh::serve(ssh_addr, host_key, ssh_state, shutdown).await }) 593 }; 594 595 let exit = tokio::select! { 596 result = &mut edge_task => FirstExit::Edge(result), 597 result = &mut ssh_task => FirstExit::Ssh(result), 598 () = shutdown_signal() => { 599 tracing::info!("shutdown signal received"); 600 FirstExit::Signal 601 } 602 }; 603 shutdown.cancel(); 604 let drain = async { 605 match &exit { 606 FirstExit::Edge(_) => { 607 let _ = (&mut ssh_task).await; 608 } 609 FirstExit::Ssh(_) => { 610 let _ = (&mut edge_task).await; 611 } 612 FirstExit::Signal => { 613 let _ = (&mut edge_task).await; 614 let _ = (&mut ssh_task).await; 615 } 616 } 617 }; 618 if tokio::time::timeout(EDGE_SHUTDOWN_DRAIN, drain) 619 .await 620 .is_err() 621 { 622 tracing::warn!( 623 timeout_secs = EDGE_SHUTDOWN_DRAIN.as_secs(), 624 "aborting edge drain after timeout" 625 ); 626 } 627 if let Some(shutdown) = maintenance_shutdown { 628 let _ = shutdown.send(true); 629 } 630 if let Some(task) = maintenance_task 631 && tokio::time::timeout(MAINTENANCE_SHUTDOWN_DRAIN, task) 632 .await 633 .is_err() 634 { 635 tracing::warn!( 636 timeout_secs = MAINTENANCE_SHUTDOWN_DRAIN.as_secs(), 637 "aborting maintenance drain after timeout :3" 638 ); 639 } 640 match exit { 641 FirstExit::Edge(result) => result 642 .context("edge server task panicked")? 643 .context("serve edge")?, 644 FirstExit::Ssh(result) => result 645 .context("ssh server task panicked")? 646 .context("serve ssh")?, 647 FirstExit::Signal => {} 648 } 649 Ok(()) 650} 651 652enum FirstExit { 653 Edge(Result<Result<(), knot_edge::EdgeError>, tokio::task::JoinError>), 654 Ssh(Result<Result<(), knot_ssh::SshError>, tokio::task::JoinError>), 655 Signal, 656} 657 658fn legacy_admin_secret( 659 config: &knot_config::Validated, 660) -> Option<knot_xrpc::legacy_admin::LegacyAdminSecret> { 661 let name = config.acl.legacy_admin_secret_env.as_deref()?; 662 let value = zeroize::Zeroizing::new(std::env::var(name).unwrap_or_default()); 663 match knot_xrpc::legacy_admin::LegacyAdminSecret::new(&value) { 664 Ok(secret) => Some(secret), 665 Err(_) => { 666 tracing::warn!( 667 secret_env = name, 668 "the environment variable in acl.legacy_admin_secret_env is unset or empty, so we won't serve the admin route" 669 ); 670 None 671 } 672 } 673} 674 675fn build_tls_setup( 676 config: &knot_config::KnotConfig, 677 hostname: &knot_types::KnotHostname, 678) -> anyhow::Result<Option<knot_edge::TlsSetup>> { 679 let tls = &config.tls; 680 let source = if tls.acme_enabled { 681 knot_edge::CertSource::Acme(knot_edge::AcmeParams { 682 domains: vec![hostname.clone()], 683 contact: knot_edge::AcmeContact::new( 684 tls.acme_contact 685 .clone() 686 .context("tls.acme_contact is required when ACME is enabled")?, 687 )?, 688 cache_dir: knot_edge::AcmeCacheDir::new( 689 tls.acme_cache_dir 690 .clone() 691 .context("tls.acme_cache_dir is required when ACME is enabled")?, 692 ), 693 production: !tls.acme_staging, 694 }) 695 } else { 696 match (&tls.cert_path, &tls.key_path) { 697 (Some(cert_path), Some(key_path)) => { 698 knot_edge::CertSource::Static(knot_edge::StaticCertPaths { 699 cert_path: knot_edge::CertChainPath::new(cert_path.clone()), 700 key_path: knot_edge::PrivateKeyPath::new(key_path.clone()), 701 }) 702 } 703 _ => return Ok(None), 704 } 705 }; 706 707 let internal = match tls.mtls_enabled { 708 true => Some(knot_edge::InternalTls { 709 addr: knot_edge::InternalBind::new(config.server.internal_listen_addr), 710 client_ca_path: knot_edge::ClientCaPath::new( 711 tls.mtls_client_ca_path 712 .clone() 713 .context("tls.mtls_client_ca_path is required when mTLS is enabled")?, 714 ), 715 spki_pin: knot_edge::SpkiPin::from_base64( 716 tls.mtls_admin_spki_pin 717 .as_deref() 718 .context("tls.mtls_admin_spki_pin is required when mTLS is enabled")?, 719 ) 720 .context("parse tls.mtls_admin_spki_pin")?, 721 }), 722 false => None, 723 }; 724 725 Ok(Some(knot_edge::TlsSetup { 726 source, 727 http3: tls.http3, 728 internal, 729 })) 730} 731 732async fn shutdown_signal() { 733 let interrupt = async { 734 let _ = tokio::signal::ctrl_c().await; 735 }; 736 #[cfg(unix)] 737 let terminate = async { 738 match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) { 739 Ok(mut stream) => { 740 stream.recv().await; 741 } 742 Err(_) => std::future::pending::<()>().await, 743 } 744 }; 745 #[cfg(not(unix))] 746 let terminate = std::future::pending::<()>(); 747 748 tokio::select! { 749 () = interrupt => {} 750 () = terminate => {} 751 } 752}