nautalid 0.1.0

Scratch container substrate — TLS 1.3 HTTP/2+3 kernel, LID/AetherDB, optional filter bus (GPL-3.0-or-later).
//! Run Nautalid inside a **scratch container** (default env + router) or compose in-process.

use std::net::SocketAddr;
use std::sync::Arc;

use axum::Router;
use thiserror::Error;
use tracing::info;

#[cfg(feature = "image-trust")]
use crate::artifacts::ArtifactVerifyError;
use crate::http::RouterBuilder;
#[cfg(feature = "kwt")]
use crate::kwt_access::KwtEnvError;
use crate::platform;
use crate::state::AppState;
use crate::surfaces::{Http1Forbidden, ListenEndpoints, SurfaceConfig, SurfaceConfigError, load_from_env};
use crate::telemetry;
use crate::tls::InboundTls;

#[cfg(feature = "filter")]
use crate::filter::{FilterParseError, FilterStore};

#[derive(Debug, Error)]
pub enum StartupError {
    #[cfg(feature = "image-trust")]
    #[error(transparent)]
    Artifact(#[from] ArtifactVerifyError),
    #[cfg(feature = "kwt")]
    #[error(transparent)]
    Kwt(#[from] KwtEnvError),
    #[cfg(feature = "kwt")]
    #[error(transparent)]
    AetherRuntime(#[from] crate::kwt_replay::RuntimeError),
    #[cfg(feature = "filter")]
    #[error(transparent)]
    Filter(#[from] FilterParseError),
    #[error("rustls crypto provider already installed")]
    CryptoProvider,
    #[cfg(feature = "filter-control")]
    #[error(transparent)]
    Bus(#[from] crate::bus::BusError),
    #[error(transparent)]
    Io(#[from] std::io::Error),
    #[error(transparent)]
    Tls(#[from] crate::tls::TlsConfigError),
    #[error(transparent)]
    Http1(#[from] Http1Forbidden),
    #[error(transparent)]
    Surfaces(#[from] SurfaceConfigError),
    #[error(transparent)]
    Security(#[from] crate::security::SecurityError),
    #[error("at least one of HTTP/2 or HTTP/3 must be enabled")]
    NoHttpListener,
}

/// Load [`AppState`] from standard Nautalid environment variables.
pub fn state_from_env() -> Result<AppState, StartupError> {
    #[cfg(feature = "image-trust")]
    let attestation = crate::artifacts::verify_on_startup_from_env()?;
    #[cfg(feature = "kwt")]
    let aether_runtime = std::sync::Arc::new(crate::kwt_replay::AetherRuntime::bootstrap_from_env()?);
    #[cfg(feature = "kwt")]
    let kwt = crate::kwt_access::kwt_access_with_store(aether_runtime.jti_store())?;
    #[cfg(feature = "filter")]
    let filter = match crate::filter::policy_from_env()? {
        Some(p) => FilterStore::new(p),
        None => FilterStore::default(),
    };
    let state = AppState::new(
        #[cfg(feature = "image-trust")]
        attestation,
        #[cfg(feature = "kwt")]
        kwt,
        #[cfg(feature = "kwt")]
        Some(aether_runtime),
        #[cfg(feature = "filter")]
        filter,
    );
    install_env_subsystems(&state);
    Ok(state)
}

/// Wire optional env-driven subsystems after manual [`AppState`] assembly.
///
/// Prefer [`crate::embed::install_subsystems`] for crate embedding with programmatic config.
pub fn install_env_subsystems(state: &AppState) {
    crate::embed::install_subsystems_from_env(state);
}

/// Loopback address for in-container **`--probe`** (avoids `0.0.0.0` and off-host TLS MITM).
#[must_use]
pub fn probe_addr_from_env() -> SocketAddr {
    let port = listen_addr_from_env().port();
    SocketAddr::from(([127, 0, 0, 1], port))
}

/// Primary HTTP/2 listen address from **`PORT`** / **`NAUTALID_H2_PORT`** / **`NAUTALID_BIND_ADDR`**.
#[must_use]
pub fn listen_addr_from_env() -> SocketAddr {
    SurfaceConfig::from_env().listen.h2
}

/// Substrate warm-up: DNS, outbound TLS, timezone sample (validates scratch-container readiness).
pub async fn warm_up_substrate() {
    if let Err(e) = crate::dns::install_from_env() {
        tracing::warn!(%e, "DNS install failed");
    }
    let ((), (), (), ()) = tokio::join!(
        async { crate::time_info::log_sample_timezone() },
        crate::dns::log_sample_resolution(),
        async {
            #[cfg(feature = "kwt")]
            {
                if let Err(e) = crate::aether_tunnel::warm_up() {
                    tracing::warn!(%e, "AetherDB wire tunnel warm-up failed");
                }
                crate::kwt_replay::warm_up_wire_dns().await;
            }
            #[cfg(not(feature = "kwt"))]
            crate::dns::log_aether_connect_resolution().await;
        },
        crate::outbound::log_https_smoke(),
    );
}

/// Serve **`router`** until SIGTERM/SIGINT — HTTP/2 + HTTP/3, TLS 1.3 only (no HTTP/1.x).
pub async fn serve(
    endpoints: ListenEndpoints,
    tls: Arc<InboundTls>,
    router: Router,
    #[cfg(feature = "filter")] filter: FilterStore,
) -> Result<(), StartupError> {
    if !endpoints.h2_enabled && !endpoints.h3_enabled {
        return Err(StartupError::NoHttpListener);
    }

    let require_client_auth = crate::tls::client_auth_required_from_env();
    let h2_config = tls.h2_config_store();
    let acme_challenge = tls.acme().map(crate::tls::AcmeHandle::challenge_config);
    let quinn_config = tls.quinn_config();

    let h2 = async {
        if endpoints.h2_enabled {
            crate::http_transport::serve_h2_tls(
                endpoints.h2,
                h2_config,
                acme_challenge,
                router.clone(),
                require_client_auth,
            )
            .await
        } else {
            std::future::pending().await
        }
    };
    let h3 = async {
        if endpoints.h3_enabled {
            crate::http_transport::serve_h3_quic(
                endpoints.h3,
                quinn_config.clone(),
                require_client_auth,
                router.clone(),
                #[cfg(feature = "filter")]
                filter.clone(),
            )
            .await
        } else {
            std::future::pending().await
        }
    };

    #[cfg(feature = "kwt")]
    let h3_wt = async {
        if endpoints.h3_enabled {
            if let Some(addr) = crate::wt_aether::dedicated_listen_from_env() {
                if crate::webtransport::is_enabled() {
                    tracing::info!(%addr, "listening (h3/quic dedicated WebTransport)");
                    crate::http_transport::serve_h3_quic(
                        addr,
                        quinn_config.clone(),
                        require_client_auth,
                        router.clone(),
                        #[cfg(feature = "filter")]
                        filter.clone(),
                    )
                    .await
                } else {
                    std::future::pending().await
                }
            } else {
                std::future::pending().await
            }
        } else {
            std::future::pending().await
        }
    };

    #[cfg(not(feature = "kwt"))]
    let h3_wt = async { std::future::pending().await };

    tokio::try_join!(h2, h3, h3_wt)?;
    info!("shutdown complete");
    Ok(())
}

/// Standalone container entry: platform + tracing + env state + default router + serve.
pub async fn run_default() -> Result<(), StartupError> {
    run(RouterBuilder::new(state_from_env()?)).await
}

/// Container entry with a preconfigured [`RouterBuilder`] (extensions merged before [`RouterBuilder::build`]).
pub async fn run(builder: RouterBuilder) -> Result<(), StartupError> {
    crate::surfaces::reject_http1_env()?;

    platform::install_defaults().map_err(|e| {
        tracing::error!(?e, "rustls crypto provider install failed");
        StartupError::CryptoProvider
    })?;
    telemetry::init_tracing();
    crate::security::init()?;
    warm_up_substrate().await;

    let surfaces = load_from_env()?;
    crate::surfaces::log_startup(&surfaces);

    let tls = Arc::new(InboundTls::from_env()?);
    tls.clone().install_global();

    if surfaces.tls_reload {
        spawn_tls_reload_on_sighup(Arc::clone(&tls));
    }

    #[cfg(feature = "filter-control")]
    crate::bus::start_control_plane(builder.state()).await?;

    crate::security::validate_wt_aether_startup()?;

    if !crate::tls::configured_from_env() {
        return Err(StartupError::Tls(crate::tls::TlsConfigError::Required));
    }
    if crate::tls::mtls_configured_from_env() {
        let mode = if crate::tls::client_auth_required_from_env() {
            "required"
        } else {
            "optional"
        };
        info!(client_auth = mode, "inbound mTLS enabled");
    }
    if crate::webtransport::is_enabled() && surfaces.listen.h3_enabled {
        info!(
            wt_paths = ?{
                #[cfg(feature = "kwt")]
                {
                    crate::wt_aether::all_session_paths()
                }
                #[cfg(not(feature = "kwt"))]
                {
                    vec![crate::webtransport::path_from_env()]
                }
            },
            "WebTransport enabled on HTTP/3"
        );
        #[cfg(feature = "kwt")]
        if crate::wt_aether::config_snapshot().enabled {
            info!(
                aether = %crate::wt_aether::config_snapshot().status_kdl(),
                "WebTransport Aether bridge active"
            );
        }
    }
    info!("inbound TLS 1.3 + HTTP/2 (tcp) and HTTP/3 (quic) on PORT");

    #[cfg(feature = "grpc-surface")]
    let acme_challenge = tls.acme().map(crate::tls::AcmeHandle::challenge_config);
    #[cfg(feature = "grpc-surface")]
    {
        let require_client_auth = crate::tls::client_auth_required_from_env();
        crate::surfaces::spawn_grpc(
            &surfaces,
            tls.h2_config_store(),
            acme_challenge,
            require_client_auth,
        );
    }
    #[cfg(not(feature = "grpc-surface"))]
    crate::surfaces::spawn_grpc(&surfaces);

    #[cfg(feature = "filter")]
    let filter = builder.state().filter.clone();
    let router = builder.build(&surfaces);
    #[cfg(feature = "filter")]
    serve(surfaces.listen, tls, router, filter).await?;
    #[cfg(not(feature = "filter"))]
    serve(surfaces.listen, tls, router).await?;
    Ok(())
}

#[cfg(unix)]
fn spawn_tls_reload_on_sighup(tls: Arc<InboundTls>) {
    tokio::spawn(async move {
        use tokio::signal::unix::{SignalKind, signal};
        let mut sighup = match signal(SignalKind::hangup()) {
            Ok(s) => s,
            Err(err) => {
                tracing::warn!(%err, "SIGHUP handler unavailable; TLS reload disabled");
                return;
            }
        };
        loop {
            if sighup.recv().await.is_none() {
                break;
            }
            if let Err(err) = tls.reload_from_env().await {
                tracing::warn!(%err, "inbound TLS reload failed");
            }
        }
    });
}

#[cfg(not(unix))]
fn spawn_tls_reload_on_sighup(_tls: Arc<InboundTls>) {}