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