Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

server/lib/src/exposed/mod.rs

//! Exposed endpoints: the listener side of the wiring graph.
//!
//! An [`ExposedEndpoint`] is a RON-configured serving surface — a gRPC address,
//! optionally a twin HTTP/OCI address (`ron_http_url`) fronting the same repos
//! for ecosystem clients (helm/pip/cargo, OCI registry) plus `/healthz`+`/readyz`.
//! One `ron_tls` covers BOTH ports; absent = cleartext, dev/loopback only.
//! Submodules: `http` (the gateway), `tls` (settings/loading), `fast_routes`
//! (per-endpoint aggregated repo lookup, attached last in the wiring pass).
//!
//! Gotcha: `wired_{in,out}_repositories` hold raw `*const Repository` back-refs
//! and are `#[serde(skip)]` — they are populated by the wiring engine after the
//! graph is indexed, so they are dangling/empty on a freshly deserialized config
//! and must never outlive the `Repository` arena that backs them.

use crate::repository::Repository;
use fast_routes::FastRoutes;
use serde::{Deserialize, Serialize};

pub mod fast_routes;
pub mod http;
pub mod tls;

/// An exposed gRPC endpoint configured in RON
#[derive(Serialize, Deserialize)]
pub struct ExposedEndpoint {
    pub ron_name: String,
    pub ron_url: String, // e.g. "0.0.0.0:50051"

    /// Optional HTTP/OCI listener (e.g. "0.0.0.0:8443") serving the same
    /// repositories to ecosystem clients (helm/pip/cargo, OCI registry) plus
    /// /healthz and /readyz for Kubernetes probes.
    #[serde(default)]
    pub ron_http_url: Option<String>,

    /// Optional TLS for this endpoint. Applies to BOTH the gRPC port and the
    /// HTTP/OCI port. Absent = cleartext (loopback/dev only; logged loudly).
    #[serde(default)]
    pub ron_tls: Option<crate::exposed::tls::TlsSettings>,

    /// Max accepted request-body size for the HTTP/OCI gateway, in bytes.
    /// Guards against memory-exhaustion uploads. Default 1 GiB.
    #[serde(default)]
    pub ron_max_body_bytes: Option<usize>,

    /// **Serve this endpoint in clear text off loopback** (lane HOL-NCSC). Default `false`:
    /// a listener that is not loopback needs `ron_tls`, or the start is refused
    /// (`holger-listen-2`). Set it only where something else encrypts the wire (a WireGuard
    /// overlay, a TLS front on the same host or link) and say which in a comment.
    #[serde(default)]
    pub insecure: bool,

    /// **The admin plane's own gRPC door** (lane HOL-NCSC, NCSC P2). Set, AdminService,
    /// PromotionService, AuditService, TokenService, the IdentityService, the jobs, admission
    /// and migration services and the thin client's doors are served HERE only; `ron_url`
    /// keeps the package services (`crate::listen::Plane`). Absent: one listener serves all.
    #[serde(default)]
    pub ron_admin_url: Option<String>,
    /// **The admin plane's HTTP door**: the `/-/auth`, `/-/bundle`, `/-/intel`, `/-/intake`,
    /// `/-/audit`, `/-/flow`, `/-/twin`, `/-/sandbox` and sync administration doors
    /// (`crate::listen::ADMIN_HTTP_PREFIXES`), here only; `ron_http_url` keeps the packages.
    #[serde(default)]
    pub ron_admin_http_url: Option<String>,
    /// **Who may connect to the admin doors**: CIDRs, required with either admin door
    /// (`holger-listen-5`); any other peer is dropped and audited (`holger-listen-4`).
    #[serde(default)]
    pub ron_admin_allow: Vec<String>,

    /// **Who may say what this endpoint's public face is** (HOL-DOCFIND a): CIDRs of the TLS
    /// fronts that end TLS and dial this endpoint's HTTP door. Only from a peer in this list
    /// are `X-Forwarded-Proto` and `X-Forwarded-Host` believed when Holger writes a link (an
    /// npm packument's tarballs, a crates source's `dl`). Empty (the default): nobody, and a
    /// link carries the door's own scheme (`https` with `ron_tls`) and the request's host.
    #[serde(default)]
    pub ron_trusted_proxies: Vec<String>,

    #[serde(skip_serializing, skip_deserializing, default)]
    pub aggregated_routes: Option<FastRoutes>,

    #[serde(skip_serializing, skip_deserializing, default)]
    pub wired_in_repositories: Vec<*const Repository>,
    #[serde(skip_serializing, skip_deserializing, default)]
    pub wired_out_repositories: Vec<*const Repository>,
}

impl ExposedEndpoint {
    /// The parsed `ron_trusted_proxies`: `None` when empty, a refusal naming the entry when
    /// one is not a CIDR (`holger-listen-6` at the start).
    pub fn trusted_proxies(&self) -> Result<Option<crate::listen::AllowList>, String> {
        if self.ron_trusted_proxies.is_empty() {
            return Ok(None);
        }
        crate::listen::AllowList::parse_named(&self.ron_trusted_proxies, "ron_trusted_proxies")
            .map(Some)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn endpoint(trusted: &str) -> ExposedEndpoint {
        ron::from_str(&format!(
            r#"(ron_name: "door", ron_url: "127.0.0.1:1", ron_trusted_proxies: {trusted})"#
        ))
        .expect("the RON parses")
    }

    /// `ron_trusted_proxies` is CIDRs (HOL-DOCFIND a): empty trusts nobody, an entry that is
    /// not a network is refused by name (`holger-listen-6` at the start).
    #[test]
    fn trusted_proxies_are_cidrs_or_refused_by_name() {
        assert_eq!(endpoint("[]").trusted_proxies(), Ok(None));
        let t = endpoint(r#"["10.0.0.1", "fd00::/8"]"#)
            .trusted_proxies()
            .unwrap()
            .expect("a list");
        assert!(t.allows("10.0.0.1".parse().unwrap()));
        assert!(!t.allows("10.0.0.2".parse().unwrap()));
        let e = endpoint(r#"["front.example"]"#)
            .trusted_proxies()
            .unwrap_err();
        assert!(
            e.contains("ron_trusted_proxies") && e.contains("front.example"),
            "{e}"
        );
    }
}