gunnar-server 3.0.0

The gunnar git server: SSH and smart-HTTP git transports and the gRPC control plane. The appliance half, extracted from gunnar-cli so the server code lives in its own crate.
Documentation
//! **The relay this main mails through, fed again by the ONE box and kept** (monetize
//! MAIL-RELAY, 2026-10-28).
//!
//! A main is fed its relay (`secret.smtp_url` / `secret.smtp_from`) at LAY time only, by the ONE
//! box. gunnar.rs and test2 were laid while the ONE box held none, so their mains mail nothing.
//! The ONE box now feeds a laid main its relay again as a signed `mail_relay` order over the pull
//! channel (`gunnar_repair::order::MailRelay`); this module keeps it.
//!
//! * kept SEALED at [`KEPT_REL`] under the data root, 0600 — the seal and the key custody the
//!   installer's feed has at rest (`fed_at_rest`: AES-256-GCM under the pairing KEK);
//! * read at every mail ([`over`]): the kept relay goes over the feed in force, so a mail sent
//!   after the order goes through it, with no restart. It is the newer one (a lay feeds first,
//!   the operator re-feeds after), so it wins over what the lay fed;
//! * a relay that does not parse is refused by name and nothing is kept; the token is never
//!   logged, never in an answer;
//! * its `secret.smtp_ca` (a relay behind a private CA) is the root the relay's TLS is checked
//!   under (RA5-23); one that does not parse keeps nothing.

use std::path::Path;

use gunnar_repair::order::MailRelay;

/// Where the fed relay is kept under the data root (sealed).
pub const KEPT_REL: &str = "pairing/mail-relay.json";

/// **Keep `relay` as the relay this box mails through**: checked as the postman reads it
/// ([`crate::postman::Smtp::from_url`]), then sealed at [`KEPT_REL`], 0600. The answer names the
/// relay's host and port and its sender, never the token.
pub fn keep(root: &Path, relay: &MailRelay) -> Result<String, String> {
    let smtp = crate::postman::Smtp::from_url(relay.smtp_url.trim(), relay.smtp_from.trim())
        .map_err(|why| {
            format!("the mail_relay this box was fed cannot mail ({why}): nothing was kept")
        })?;
    // The relay's own root (RA5-23): checked here, so a kept relay is one that can mail.
    let smtp = smtp.with_ca(&relay.smtp_ca).map_err(|why| {
        format!("the mail_relay's secret.smtp_ca is no root ({why}): nothing was kept")
    })?;
    let (host, port, _) = smtp.endpoint();
    let at = format!("{host}:{port}");
    let json = serde_json::to_vec(relay)
        .map_err(|e| format!("the mail_relay could not be written: {e}"))?;
    seal_to(root, &json)
        .map_err(|e| format!("the mail_relay could not be kept at {KEPT_REL}: {e:#}"))?;
    tracing::warn!(target: "gunnar::audit", event = "mail_relay_kept", relay = %at, from = %relay.smtp_from.trim(), "the ONE box fed this box its mail relay again: kept sealed, in use from the next mail");
    Ok(format!(
        "the mail relay {at} is kept: this box mails as {} now",
        relay.smtp_from.trim()
    ))
}

/// **The kept relay**, or `None` (none kept, or a kept file that does not open: said).
pub fn kept(root: &Path) -> Option<MailRelay> {
    let text = match open_from(root) {
        Ok(t) => t?,
        Err(e) => {
            tracing::error!(file = KEPT_REL, error = %format!("{e:#}"), "the kept mail relay does not open: the relay fed at lay is used");
            return None;
        }
    };
    serde_json::from_str::<MailRelay>(&text)
        .ok()
        .filter(|r| !r.smtp_url.trim().is_empty() && !r.smtp_from.trim().is_empty())
}

/// **The feed in force with the kept relay over it** (the relay the ONE box fed again wins).
/// Nothing kept: the feed as it is.
pub fn over(fed: Option<crate::feed::Fed>, root: &Path) -> Option<crate::feed::Fed> {
    let Some(k) = kept(root) else { return fed };
    let mut f = fed.unwrap_or_default();
    f.smtp_url = Some(crate::feed::Withheld(k.smtp_url.trim().to_string()));
    f.smtp_from = Some(k.smtp_from.trim().to_string());
    // The kept relay's own root, or none: the lay's CA was for the lay's relay.
    f.smtp_ca = Some(k.smtp_ca.trim().to_string()).filter(|c| !c.is_empty());
    Some(f)
}

fn kek_dir(root: &Path) -> std::path::PathBuf {
    root.join("pairing")
}

fn seal_to(root: &Path, bytes: &[u8]) -> anyhow::Result<()> {
    let dir = kek_dir(root);
    std::fs::create_dir_all(&dir)?;
    let kek = gunnar_core::Kek::for_control_dir(&dir)
        .map_err(|e| anyhow::anyhow!("the pairing KEK under {}: {e}", dir.display()))?;
    let sealed = gunnar_core::seal::seal(&kek, KEPT_REL, bytes)
        .map_err(|e| anyhow::anyhow!("sealing {KEPT_REL}: {e}"))?;
    crate::durable::write_atomic_unframed(root, KEPT_REL, &sealed)?;
    use std::os::unix::fs::PermissionsExt as _;
    std::fs::set_permissions(root.join(KEPT_REL), std::fs::Permissions::from_mode(0o600))?;
    Ok(())
}

fn open_from(root: &Path) -> anyhow::Result<Option<String>> {
    let path = root.join(KEPT_REL);
    let raw = match std::fs::read(&path) {
        Ok(r) => r,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
        Err(e) => anyhow::bail!("{}: {e}", path.display()),
    };
    if !gunnar_core::seal::is_sealed(&raw) {
        anyhow::bail!("{} is not sealed", path.display());
    }
    let kek_file = kek_dir(root).join(gunnar_core::seal::KEK_FILE);
    let kek = match gunnar_core::Kek::from_env().map_err(|e| anyhow::anyhow!("{e}"))? {
        Some(k) => k,
        None if kek_file.exists() => {
            gunnar_core::Kek::from_file(&kek_file).map_err(|e| anyhow::anyhow!("{e}"))?
        }
        None => anyhow::bail!(
            "{} is sealed and this box holds no key for it",
            path.display()
        ),
    };
    let plain = gunnar_core::seal::unseal(&kek, KEPT_REL, &raw).map_err(|e| {
        anyhow::anyhow!("{} does not open under this box's key: {e}", path.display())
    })?;
    String::from_utf8(plain)
        .map(Some)
        .map_err(|e| anyhow::anyhow!("{}: {e}", path.display()))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::postman::{FedRelay, Postman as _};

    const TOKEN: &str = "t0kenVALUEnever-shown";

    /// ★ **MAIL-RELAY: a laid main fed no relay cannot mail; once the ONE box's relay is kept
    /// the SAME postman is ready, over the lay's feed, and the kept file is sealed** — the token
    /// never on disk in the clear, never in an answer. A relay that does not parse keeps nothing.
    #[test]
    fn a_main_fed_its_relay_again_keeps_it_sealed_and_its_postman_mails_through_it() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path().to_path_buf();
        let r = root.clone();
        let postman = FedRelay {
            fed: move || over(None, &r),
        };
        assert!(
            postman.ready().unwrap_err().contains("secret.smtp_url"),
            "fed no relay: mail is off"
        );
        let bad = MailRelay {
            smtp_url: format!("http://u:{TOKEN}@relay.mock.invalid"),
            smtp_from: "g@mock.invalid".into(),
            smtp_ca: String::new(),
        };
        let e = keep(&root, &bad).unwrap_err();
        assert!(e.contains("mail_relay") && !e.contains(TOKEN), "{e}");
        assert!(!root.join(KEPT_REL).exists() && kept(&root).is_none());

        let relay = MailRelay {
            smtp_url: format!("smtp://gunnar%40mock.invalid:{TOKEN}@relay.mock.invalid:587"),
            smtp_from: "Gunnar <gunnar@mock.invalid>".into(),
            smtp_ca: String::new(),
        };
        let said = keep(&root, &relay).expect("kept");
        assert!(
            said.contains("relay.mock.invalid:587")
                && said.contains("gunnar@mock.invalid")
                && !said.contains(TOKEN),
            "{said}"
        );
        let raw = std::fs::read(root.join(KEPT_REL)).unwrap();
        assert!(
            gunnar_core::seal::is_sealed(&raw) && !String::from_utf8_lossy(&raw).contains(TOKEN),
            "sealed at rest"
        );
        let mode = std::os::unix::fs::PermissionsExt::mode(
            &std::fs::metadata(root.join(KEPT_REL))
                .unwrap()
                .permissions(),
        ) & 0o777;
        assert_eq!(mode, 0o600);
        assert_eq!(kept(&root), Some(relay.clone()));
        postman.ready().expect("the kept relay is in use at once");

        // Over the lay's feed: the kept relay wins, the rest of the feed stays.
        let lay = crate::feed::Fed {
            smtp_url: Some(crate::feed::Withheld(
                "smtp://old:old@old.mock.invalid:587".into(),
            )),
            smtp_from: Some("old@mock.invalid".into()),
            fleet: Some("f".into()),
            ..Default::default()
        };
        let f = over(Some(lay), &root).unwrap();
        assert_eq!(
            (
                f.smtp_url.map(|w| w.0),
                f.smtp_from.as_deref(),
                f.fleet.as_deref()
            ),
            (
                Some(relay.smtp_url.clone()),
                Some("Gunnar <gunnar@mock.invalid>"),
                Some("f")
            )
        );
    }

    /// A CA, and a certificate for `localhost` issued under it: (CA PEM, leaf DER, leaf key DER).
    fn private_ca() -> (String, Vec<u8>, Vec<u8>) {
        let ca_key = rcgen::KeyPair::generate().unwrap();
        let mut ca = rcgen::CertificateParams::new(Vec::<String>::new()).unwrap();
        ca.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
        ca.key_usages = vec![
            rcgen::KeyUsagePurpose::KeyCertSign,
            rcgen::KeyUsagePurpose::CrlSign,
        ];
        ca.distinguished_name
            .push(rcgen::DnType::CommonName, "the relay operator's own CA");
        let ca_cert = ca.self_signed(&ca_key).unwrap();
        let issuer = rcgen::Issuer::new(ca, ca_key);
        let leaf_key = rcgen::KeyPair::generate().unwrap();
        let leaf = rcgen::CertificateParams::new(vec!["localhost".to_owned()])
            .unwrap()
            .signed_by(&leaf_key, &issuer)
            .unwrap();
        (ca_cert.pem(), leaf.der().to_vec(), leaf_key.serialize_der())
    }

    /// **A fake relay behind `leaf`** (smtps://, TLS from the first byte) on loopback: it
    /// speaks just enough SMTP to take one mail, and counts the mails it took. A handshake the
    /// client refuses is dropped.
    fn fake_relay(
        leaf: Vec<u8>,
        key: Vec<u8>,
    ) -> (u16, std::sync::Arc<std::sync::atomic::AtomicUsize>) {
        use std::io::{BufRead as _, Write as _};
        let provider = std::sync::Arc::new(rustls::crypto::ring::default_provider());
        let config = rustls::ServerConfig::builder_with_provider(provider)
            .with_safe_default_protocol_versions()
            .unwrap()
            .with_no_client_auth()
            .with_single_cert(
                vec![leaf.into()],
                rustls::pki_types::PrivateKeyDer::Pkcs8(key.into()),
            )
            .unwrap();
        let config = std::sync::Arc::new(config);
        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
        let port = listener.local_addr().unwrap().port();
        let took = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
        let count = std::sync::Arc::clone(&took);
        std::thread::spawn(move || {
            for stream in listener.incoming().flatten() {
                let (config, count) = (
                    std::sync::Arc::clone(&config),
                    std::sync::Arc::clone(&count),
                );
                std::thread::spawn(move || {
                    let Ok(conn) = rustls::ServerConnection::new(config) else {
                        return;
                    };
                    let mut tls = rustls::StreamOwned::new(conn, stream);
                    if tls
                        .write_all(b"220 localhost ESMTP fake\r\n")
                        .and_then(|()| tls.flush())
                        .is_err()
                    {
                        return;
                    }
                    let mut reader = std::io::BufReader::new(tls);
                    let mut data = false;
                    loop {
                        let mut line = String::new();
                        if reader.read_line(&mut line).map_or(true, |n| n == 0) {
                            return;
                        }
                        let upper = line.to_ascii_uppercase();
                        let answer: &[u8] = if data {
                            if line == ".\r\n" {
                                data = false;
                                count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
                                b"250 queued\r\n"
                            } else {
                                continue;
                            }
                        } else if upper.starts_with("EHLO") || upper.starts_with("HELO") {
                            b"250-localhost\r\n250 AUTH PLAIN LOGIN\r\n"
                        } else if upper.starts_with("AUTH") {
                            b"235 ok\r\n"
                        } else if upper.starts_with("DATA") {
                            data = true;
                            b"354 go on\r\n"
                        } else if upper.starts_with("QUIT") {
                            let _ = reader.get_mut().write_all(b"221 bye\r\n");
                            return;
                        } else {
                            b"250 ok\r\n"
                        };
                        let tls = reader.get_mut();
                        if tls.write_all(answer).and_then(|()| tls.flush()).is_err() {
                            return;
                        }
                    }
                });
            }
        });
        (port, took)
    }

    /// ★ **RA5-23: `secret.smtp_ca` is the root the relay's TLS is checked under.** A relay
    /// behind a private CA: kept without the CA, the mail fails its handshake (the public
    /// roots do not know it); kept with it, the SAME postman mails through it; another CA is
    /// not it; a CA that does not parse keeps nothing, by name.
    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn a_relay_behind_a_private_ca_is_trusted_under_the_fed_smtp_ca() {
        let (ca, leaf, key) = private_ca();
        let (other_ca, _, _) = private_ca();
        let (port, took) = fake_relay(leaf, key);
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path().to_path_buf();
        let r = root.clone();
        let postman = FedRelay {
            fed: move || over(None, &r),
        };
        let mail = gunnar_repair::sign_in::SignInMail {
            to: "buyer@mock.invalid".into(),
            subject: "sign in".into(),
            body: "a link".into(),
        };
        let relay = |ca: &str| MailRelay {
            smtp_url: format!("smtps://gunnar%40mock.invalid:{TOKEN}@localhost:{port}"),
            smtp_from: "Gunnar <gunnar@mock.invalid>".into(),
            smtp_ca: ca.to_owned(),
        };

        keep(&root, &relay("")).expect("kept without a CA");
        let refused = postman
            .post(&mail)
            .await
            .expect_err("the public roots do not know the relay's CA");
        assert!(!refused.contains(TOKEN), "{refused}");
        keep(&root, &relay(&other_ca)).expect("kept with another CA");
        postman
            .post(&mail)
            .await
            .expect_err("another CA is not the relay's");
        assert_eq!(
            took.load(std::sync::atomic::Ordering::SeqCst),
            0,
            "no mail went to an untrusted relay"
        );

        let said = keep(&root, &relay(&ca)).expect("kept with the relay's CA");
        assert!(
            said.contains("localhost") && !said.contains(TOKEN),
            "{said}"
        );
        assert_eq!(
            kept(&root).map(|k| k.smtp_ca),
            Some(ca.clone()),
            "the CA is kept, sealed with the relay"
        );
        postman
            .post(&mail)
            .await
            .expect("mailed through the relay, under its CA");
        assert_eq!(took.load(std::sync::atomic::Ordering::SeqCst), 1);

        let e = keep(
            &root,
            &relay("-----BEGIN CERTIFICATE-----\nnot base64\n-----END CERTIFICATE-----\n"),
        )
        .expect_err("a CA that does not parse");
        assert!(
            e.contains("smtp_ca") && e.contains("nothing was kept"),
            "{e}"
        );
        assert_eq!(
            kept(&root).map(|k| k.smtp_ca),
            Some(ca),
            "the CA kept before stays"
        );
    }
}