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