mod acme; mod altsvc; mod compression; mod limits; mod peer; mod protocol; mod quic; mod robustness; mod tcp; mod tls; mod zerortt; use std::future::Future; use std::net::SocketAddr; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::sync::Arc; use axum::Router; use axum::middleware::from_fn; use rustls::server::ResolvesServerCert; use tokio::net::TcpListener; use tokio_util::sync::CancellationToken; pub use acme::{AcmeCacheDir, AcmeContact, AcmeContactError, AcmeError, AcmeParams}; pub use limits::{ ConnectionBudget, HeaderTimeout, IdleTimeout, ListenLimits, MaxConcurrentStreams, }; pub use peer::SocketPeer; pub use protocol::NegotiatedProtocol; pub use quic::EndpointError; pub use robustness::{ BodyInactivityTimeout, BurstSize, EdgeGuards, MaxInflightRequests, RequestTimeout, RequestsPerSecond, WriteRequestTimeout, }; pub use tls::{ReloadableCertResolver, SpkiPin, TlsError, load_certified_key}; pub use zerortt::{EarlyData, RequiresFullHandshake, ZeroRttRoutes, ZeroRttSafe}; pub mod fuzz { pub fn spki_of_certificate(data: &[u8]) { crate::tls::fuzz_of_certificate(data); } pub fn spki_pin(data: &[u8]) { let _ = crate::SpkiPin::from_base64(&String::from_utf8_lossy(data)); } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct CertChainPath(PathBuf); impl CertChainPath { pub fn new(path: impl Into) -> Self { Self(path.into()) } pub fn as_path(&self) -> &Path { &self.0 } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct PrivateKeyPath(PathBuf); impl PrivateKeyPath { pub fn new(path: impl Into) -> Self { Self(path.into()) } pub fn as_path(&self) -> &Path { &self.0 } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClientCaPath(PathBuf); impl ClientCaPath { pub fn new(path: impl Into) -> Self { Self(path.into()) } pub fn as_path(&self) -> &Path { &self.0 } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PublicBind(SocketAddr); impl PublicBind { pub const fn new(addr: SocketAddr) -> Self { Self(addr) } pub const fn get(self) -> SocketAddr { self.0 } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct InternalBind(SocketAddr); impl InternalBind { pub const fn new(addr: SocketAddr) -> Self { Self(addr) } pub const fn get(self) -> SocketAddr { self.0 } } pub struct StaticCertPaths { pub cert_path: CertChainPath, pub key_path: PrivateKeyPath, } pub enum CertSource { Static(StaticCertPaths), Acme(AcmeParams), } pub struct InternalTls { pub addr: InternalBind, pub client_ca_path: ClientCaPath, pub spki_pin: SpkiPin, } pub struct TlsSetup { pub source: CertSource, pub http3: bool, pub internal: Option, } pub struct EdgeConfig { pub http_addr: PublicBind, pub limits: ListenLimits, pub guards: EdgeGuards, pub tls: Option, } #[derive(Debug, thiserror::Error)] pub enum EdgeError { #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] Tls(#[from] TlsError), #[error(transparent)] Acme(#[from] AcmeError), #[error(transparent)] Endpoint(#[from] EndpointError), } type Served = Pin> + Send>>; fn base_router(app: RequiresFullHandshake, early_data_safe: ZeroRttRoutes) -> Router { early_data_safe .into_router() .merge(app.into_router()) .layer(compression::layer()) .layer(from_fn(zerortt::tag_from_header)) } fn finish(router: Router) -> Router { altsvc::with_host_from_authority(router.layer(from_fn(protocol::tag))) } pub async fn serve( config: EdgeConfig, app: RequiresFullHandshake, early_data_safe: ZeroRttRoutes, shutdown: CancellationToken, ) -> Result<(), EdgeError> { let EdgeConfig { http_addr, limits, guards, tls, } = config; let layers = guards.prepare(&shutdown); let early_data = early_data_safe.early_data_policy(); let wants_internal = tls .as_ref() .and_then(|setup| setup.internal.as_ref()) .is_some(); let base = base_router(app, early_data_safe); let internal_router = wants_internal.then(|| finish(base.clone())); let router = finish(robustness::apply(base, layers)); let listener = TcpListener::bind(http_addr.get()).await?; let Some(setup) = tls else { return Ok(tcp::serve_plaintext(listener, router, limits, shutdown).await?); }; let (resolver, acme): (Arc, bool) = match setup.source { CertSource::Static(paths) => { let certified = tls::load_certified_key(&paths)?; let reloadable = Arc::new(ReloadableCertResolver::new(certified)); tls::spawn_cert_reload(Arc::clone(&reloadable), paths, shutdown.clone()); (reloadable, false) } CertSource::Acme(params) => (acme::start(params, shutdown.clone())?, true), }; let extra_alpn: &[&[u8]] = if acme { &[tls::ACME_TLS_ALPN] } else { &[] }; let tcp_config = Arc::new(tls::build_tls_server_config( Arc::clone(&resolver), extra_alpn, )?); let port = http_addr.get().port(); let mut servers: Vec = Vec::new(); servers.push({ let app = match setup.http3 { true => altsvc::with_alt_svc(router.clone(), altsvc::Port::new(port)), false => router.clone(), }; let shutdown = shutdown.clone(); Box::pin(async move { let result = tcp::serve_tls(listener, app, tcp_config, limits, shutdown.clone()).await; shutdown.cancel(); Ok(result?) }) }); if setup.http3 { let endpoint = quic::build_endpoint(http_addr.get(), Arc::clone(&resolver), limits, early_data)?; let app = router.clone(); let shutdown = shutdown.clone(); servers.push(Box::pin(async move { quic::serve_http3(endpoint, app, limits, shutdown.clone()).await; shutdown.cancel(); Ok(()) })); } if let Some(internal) = setup.internal { let internal_listener = TcpListener::bind(internal.addr.get()).await?; let mtls_config = Arc::new(tls::build_mtls_server_config( Arc::clone(&resolver), &internal.client_ca_path, internal.spki_pin, )?); let app = internal_router .expect("an internal router is built whenever an internal bind is configured"); let shutdown = shutdown.clone(); servers.push(Box::pin(async move { let result = tcp::serve_tls( internal_listener, app, mtls_config, limits, shutdown.clone(), ) .await; shutdown.cancel(); Ok(result?) })); } futures::future::join_all(servers) .await .into_iter() .collect::, EdgeError>>() .map(drop) } #[cfg(test)] mod tests { use super::*; use axum::body::Body; use axum::extract::ConnectInfo; use axum::routing::post; use http::{Request, StatusCode}; use tower::ServiceExt; use std::num::{NonZeroU32, NonZeroU64}; use zerortt::ZeroRttSafe; fn test_layers() -> robustness::GuardLayers { EdgeGuards::new( RequestsPerSecond::new(NonZeroU32::new(10_000).unwrap()), BurstSize::new(NonZeroU32::new(10_000).unwrap()), MaxInflightRequests::new(NonZeroU32::new(1_024).unwrap()), RequestTimeout::from_millis(NonZeroU64::new(30_000).unwrap()), BodyInactivityTimeout::from_millis(NonZeroU64::new(30_000).unwrap()), WriteRequestTimeout::from_millis(NonZeroU64::new(1_800_000).unwrap()), knot_types::ProxyTrust::default(), ) .prepare(&CancellationToken::new()) } fn wired() -> Router { let safe = ZeroRttRoutes::new().get("/info/refs", ZeroRttSafe::new(|| async { "advertisement" })); let full = RequiresFullHandshake::new( Router::new().route("/git-upload-pack", post(|| async { "pack" })), ); finish(robustness::apply(base_router(full, safe), test_layers())) } fn tight_layers() -> robustness::GuardLayers { EdgeGuards::new( RequestsPerSecond::new(NonZeroU32::new(1).unwrap()), BurstSize::new(NonZeroU32::new(2).unwrap()), MaxInflightRequests::new(NonZeroU32::new(1_024).unwrap()), RequestTimeout::from_millis(NonZeroU64::new(30_000).unwrap()), BodyInactivityTimeout::from_millis(NonZeroU64::new(30_000).unwrap()), WriteRequestTimeout::from_millis(NonZeroU64::new(1_800_000).unwrap()), knot_types::ProxyTrust::default(), ) .prepare(&CancellationToken::new()) } async fn status_of(mut request: Request) -> StatusCode { request .extensions_mut() .insert(ConnectInfo(SocketAddr::from(([127, 0, 0, 1], 41001)))); wired().oneshot(request).await.unwrap().status() } #[tokio::test] async fn a_write_in_early_data_is_refused_with_425() { let request = Request::post("/git-upload-pack") .header("early-data", "1") .body(Body::empty()) .unwrap(); assert_eq!(status_of(request).await, StatusCode::TOO_EARLY); } #[tokio::test] async fn a_write_after_the_handshake_is_served() { let request = Request::post("/git-upload-pack") .body(Body::empty()) .unwrap(); assert_eq!(status_of(request).await, StatusCode::OK); } #[tokio::test] async fn the_advertisement_is_served_even_in_early_data() { let request = Request::get("/info/refs") .header("early-data", "1") .body(Body::empty()) .unwrap(); assert_eq!(status_of(request).await, StatusCode::OK); } #[tokio::test] async fn the_internal_admin_router_shares_no_rate_limit_budget_with_the_data_plane() { let safe = ZeroRttRoutes::new().get("/info/refs", ZeroRttSafe::new(|| async { "ok" })); let full = RequiresFullHandshake::new( Router::new().route("/git-upload-pack", post(|| async { "pack" })), ); let base = base_router(full, safe); let public = finish(robustness::apply(base.clone(), tight_layers())); let internal = finish(base); let request = || { let mut request = Request::get("/info/refs").body(Body::empty()).unwrap(); request .extensions_mut() .insert(ConnectInfo(SocketAddr::from(([127, 0, 0, 1], 41001)))); request }; let p1 = public.clone().oneshot(request()).await.unwrap().status(); let p2 = public.clone().oneshot(request()).await.unwrap().status(); let p3 = public.clone().oneshot(request()).await.unwrap().status(); assert_eq!([p1, p2], [StatusCode::OK, StatusCode::OK]); assert_eq!( p3, StatusCode::TOO_MANY_REQUESTS, "the public edge still enforces the per-IP burst" ); let i1 = internal.clone().oneshot(request()).await.unwrap().status(); let i2 = internal.clone().oneshot(request()).await.unwrap().status(); let i3 = internal.clone().oneshot(request()).await.unwrap().status(); let i4 = internal.clone().oneshot(request()).await.unwrap().status(); assert_eq!( [i1, i2, i3, i4], [StatusCode::OK; 4], "the internal admin bind is unguarded, so a public flood never sheds admin requests" ); } }