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

nordisk/gunnar

crates/gunnar-front/tests/grpc_hole.rs

//! **The UI = CLI hole, end to end** (GRAND-PLAN-V2 §10, A6): the REAL gunnar
//! CLI (`gunnar_cli::run`, the binary's own dispatch, in its own process) runs
//! `gunnar control … --at https://localhost:<port>` against an in-process FRONT
//! (its :443, TLS with a fed certificate from a test CA) in front of a REAL
//! control plane (`gunnar_grpc::router` over real stores, on loopback, plain
//! h2c as gunnar's listener is). Every read and write a person makes goes
//! through it, each signed with the caller's OWN key and carried untouched:
//! the server says who called (an admin, an ordinary user, nobody), refuses
//! what that caller may not do, and the console's audit trail names each call
//! by the caller's own fingerprint. Past the limit a call is refused, and
//! HTTP/1 on the same :443 is still the face's.
//!
//! **And the hole keeps the face's rules** (RA1-1): it answers on the
//! console's own names only (never on a callback-only one), only to the
//! estate's own network unless the front is set `public`, and never carries
//! `Bootstrap.Enroll` or any other call that hands a fresh box to its first
//! caller: an UNENROLLED box behind the front cannot be claimed through any of
//! its names.
//!
//! `harness = false`: the test binary is ALSO the CLI. Started with
//! [`CLI_ENV`] set it is exactly `gunnar`'s `main` (the same `run`, the same
//! exit code and output), so each step is a real process whose stdout, stderr
//! and status are asserted. No network beyond loopback; the site is
//! `localhost`, the people are `admin` and `bob`.

use std::collections::BTreeMap;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

use gunnar_front::web::hole::{Forwarded, Hole};
use gunnar_server::stores::ServerStores;
use gunnar_server::CoreControlPlane;

/// Set in the CLI's own process.
const CLI_ENV: &str = "GUNNAR_FRONT_TEST_AS_CLI";

fn main() {
    if std::env::var_os(CLI_ENV).is_some() {
        // `gunnar`'s main, verbatim (crates/gunnar-cli/src/main.rs).
        let outcome = gunnar_cli::run();
        gunnar_common::logqueue::flush(Duration::from_secs(5));
        if let Err(err) = outcome {
            eprintln!("gunnar: {err:#}");
            std::process::exit(1);
        }
        return;
    }
    let rt = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .expect("a runtime");
    let n = rt.block_on(scenario());
    println!("grpc_hole: {n} checks passed");
}

/// What one CLI run printed, and how it ended.
struct Ran {
    ok: bool,
    out: String,
    err: String,
}

impl std::fmt::Debug for Ran {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "ok={} stdout:\n{}\nstderr:\n{}",
            self.ok, self.out, self.err
        )
    }
}

struct Site {
    at: String,
    home: PathBuf,
    ca: PathBuf,
}

impl Site {
    /// `gunnar control <args…> --at https://localhost:<port>`, as a process.
    async fn gunnar(&self, args: &[&str]) -> Ran {
        let mut cmd = Command::new(std::env::current_exe().expect("this binary"));
        cmd.env(CLI_ENV, "1")
            .env("HOME", &self.home)
            .env("GUNNAR_SITE_CA", &self.ca)
            .env("RUST_LOG", "warn")
            .env_remove("GUNNAR_CLIENT_CERT")
            .env_remove("GUNNAR_CLIENT_KEY")
            .env_remove("GUNNAR_CA")
            .env_remove("GUNNAR_CONTROL_ADDR")
            .arg("control")
            .args(args)
            .arg("--at")
            .arg(&self.at);
        let out = tokio::task::spawn_blocking(move || cmd.output().expect("run the CLI"))
            .await
            .expect("joined");
        Ran {
            ok: out.status.success(),
            out: String::from_utf8_lossy(&out.stdout).into_owned(),
            err: String::from_utf8_lossy(&out.stderr).into_owned(),
        }
    }
}

fn mint(dir: &Path, name: &str) -> (PathBuf, String, String) {
    let pem = gunnar_core::mint_ed25519().expect("mint");
    let text = String::from_utf8(pem).expect("utf8");
    let key = gunnar_core::ssh_key::PrivateKey::from_openssh(&text).expect("parse");
    let line = key.public_key().to_openssh().expect("line");
    let fp = key
        .public_key()
        .fingerprint(gunnar_core::ssh_key::HashAlg::Sha256)
        .to_string();
    let path = dir.join(name);
    std::fs::write(&path, &text).expect("write the key");
    {
        use std::os::unix::fs::PermissionsExt as _;
        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).expect("0600");
    }
    std::fs::write(dir.join(format!("{name}.pub")), format!("{line}\n")).expect("write the .pub");
    (path, line, fp)
}

/// The front's second name: a payment vendor's webhook name (callback-only).
const PAY: &str = "pay.localhost";

/// A test CA and a leaf for `localhost` (and [`PAY`]) it signed: (CA PEM, chain PEM, key PEM).
fn site_certificate() -> (String, String, String) {
    let ca_key = rcgen::KeyPair::generate().unwrap();
    let mut p = rcgen::CertificateParams::new(Vec::<String>::new()).unwrap();
    p.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
    p.distinguished_name
        .push(rcgen::DnType::CommonName, "grpc hole test CA");
    let ca_cert = p.self_signed(&ca_key).unwrap();
    let ca = rcgen::Issuer::new(p, ca_key);
    let leaf_key = rcgen::KeyPair::generate().unwrap();
    let mut lp =
        rcgen::CertificateParams::new(vec!["localhost".to_string(), PAY.to_string()]).unwrap();
    lp.distinguished_name
        .push(rcgen::DnType::CommonName, "localhost");
    let now = time::OffsetDateTime::now_utc();
    lp.not_before = now - time::Duration::days(1);
    lp.not_after = now + time::Duration::days(80);
    let leaf = lp.signed_by(&leaf_key, &ca).unwrap();
    (
        ca_cert.pem(),
        format!("{}{}", leaf.pem(), ca_cert.pem()),
        leaf_key.serialize_pem(),
    )
}

fn free_addr() -> SocketAddr {
    std::net::TcpListener::bind("127.0.0.1:0")
        .expect("bind")
        .local_addr()
        .expect("addr")
}

/// The control plane's services, as gunnar-proto declares them.
fn proto_services() -> Vec<String> {
    let mut out = Vec::new();
    let mut stack = vec![PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../gunnar-proto/proto")];
    while let Some(d) = stack.pop() {
        for e in std::fs::read_dir(&d).expect("the proto dir").flatten() {
            let p = e.path();
            if p.is_dir() {
                stack.push(p);
            } else if p.extension().is_some_and(|x| x == "proto") {
                let text = std::fs::read_to_string(&p).unwrap();
                let package = text
                    .lines()
                    .find_map(|l| {
                        l.trim()
                            .strip_prefix("package ")
                            .map(|s| s.trim_end_matches(';').trim().to_string())
                    })
                    .unwrap_or_default();
                for l in text.lines() {
                    if let Some(rest) = l.strip_prefix("service ") {
                        out.push(format!(
                            "{package}.{}",
                            rest.split_whitespace().next().unwrap()
                        ));
                    }
                }
            }
        }
    }
    out.sort();
    out
}

/// An HTTP/1.1 GET on the front's :443 (no ALPN h2).
async fn http1_get(addr: SocketAddr, ca_pem: &str, path: &str) -> String {
    use rustls::pki_types::pem::PemObject as _;
    use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
    let mut roots = rustls::RootCertStore::empty();
    for c in rustls::pki_types::CertificateDer::pem_slice_iter(ca_pem.as_bytes()) {
        roots.add(c.unwrap()).unwrap();
    }
    let cfg = rustls::ClientConfig::builder_with_provider(Arc::new(
        rustls::crypto::ring::default_provider(),
    ))
    .with_safe_default_protocol_versions()
    .unwrap()
    .with_root_certificates(roots)
    .with_no_client_auth();
    let tcp = tokio::net::TcpStream::connect(addr).await.unwrap();
    let sni = rustls::pki_types::ServerName::try_from("localhost".to_string()).unwrap();
    let mut tls = tokio_rustls::TlsConnector::from(Arc::new(cfg))
        .connect(sni, tcp)
        .await
        .unwrap();
    tls.write_all(format!("POST {path} HTTP/1.1\r\nHost: localhost\r\ncontent-type: application/grpc\r\ncontent-length: 0\r\nConnection: close\r\n\r\n").as_bytes()).await.unwrap();
    let mut out = String::new();
    let _ = tls.read_to_string(&mut out).await;
    out
}

/// What one raw gRPC call through the front got: its status and message.
#[derive(Debug)]
struct Raw {
    status: Option<u32>,
    message: String,
}

/// **One raw gRPC call on the front's :443** over HTTP/2 (TLS for `localhost`):
/// `path` with an empty message, asked for `authority` (the name the front
/// reads), with `x-forwarded-for` when given (believed from loopback, the way
/// the console reads a client address). No credential.
async fn raw_grpc(
    addr: SocketAddr,
    ca_pem: &str,
    authority: &str,
    path: &str,
    xff: Option<&str>,
) -> Raw {
    use http_body_util::BodyExt as _;
    use rustls::pki_types::pem::PemObject as _;
    let mut roots = rustls::RootCertStore::empty();
    for c in rustls::pki_types::CertificateDer::pem_slice_iter(ca_pem.as_bytes()) {
        roots.add(c.unwrap()).unwrap();
    }
    let mut cfg = rustls::ClientConfig::builder_with_provider(Arc::new(
        rustls::crypto::ring::default_provider(),
    ))
    .with_safe_default_protocol_versions()
    .unwrap()
    .with_root_certificates(roots)
    .with_no_client_auth();
    cfg.alpn_protocols = vec![b"h2".to_vec()];
    let tcp = tokio::net::TcpStream::connect(addr).await.unwrap();
    let sni = rustls::pki_types::ServerName::try_from("localhost".to_string()).unwrap();
    let tls = tokio_rustls::TlsConnector::from(Arc::new(cfg))
        .connect(sni, tcp)
        .await
        .unwrap();
    let (mut send, conn) = hyper::client::conn::http2::handshake(
        hyper_util::rt::TokioExecutor::new(),
        hyper_util::rt::TokioIo::new(tls),
    )
    .await
    .expect("h2");
    tokio::spawn(async move {
        let _ = conn.await;
    });
    let mut b = http::Request::builder()
        .method("POST")
        .uri(format!("https://{authority}:{}{path}", addr.port()))
        .header("content-type", "application/grpc")
        .header("te", "trailers");
    if let Some(x) = xff {
        b = b.header("x-forwarded-for", x);
    }
    let req = b
        .body(http_body_util::Full::new(bytes::Bytes::from_static(&[
            0, 0, 0, 0, 0,
        ])))
        .unwrap();
    let resp = send.send_request(req).await.expect("an answer");
    let head = resp.headers().clone();
    let body = resp.into_body().collect().await.ok();
    let trailers = body.and_then(|b| b.trailers().cloned());
    let pick = |name: &str| {
        head.get(name)
            .or_else(|| trailers.as_ref().and_then(|t| t.get(name)))
            .and_then(|v| v.to_str().ok())
            .map(str::to_string)
    };
    Raw {
        status: pick("grpc-status").and_then(|s| s.parse().ok()),
        message: pick("grpc-message").unwrap_or_default(),
    }
}

async fn scenario() -> usize {
    let mut checks = 0;
    let check = |checks: &mut usize, ok: bool, what: &str| {
        assert!(ok, "{what}");
        *checks += 1;
        println!("ok  {what}");
    };

    // The allow-list is the control plane's own services, no more, no fewer.
    let mut listed: Vec<String> = gunnar_front::web::hole::SERVICES
        .iter()
        .map(|s| s.to_string())
        .collect();
    listed.sort();
    check(
        &mut checks,
        listed == proto_services(),
        "the hole's services are gunnar-proto's services",
    );

    let dir = tempfile::tempdir().expect("tempdir");
    let keys = dir.path().join("keys");
    std::fs::create_dir_all(&keys).unwrap();
    let (admin_key, admin_line, admin_fp) = mint(&keys, "admin");
    let (bob_key, _bob_line, bob_fp) = mint(&keys, "bob");
    let (_, front_line, front_fp) = mint(&keys, "front-console");

    // ── a REAL control plane: the admin enrolled, and the front's own console key
    //    an admin too (as on an estate): a call carried as the front's would pass.
    let stores = Arc::new(ServerStores::open(dir.path().join("gunnar")).expect("stores"));
    stores
        .auth()
        .bootstrap_admin(&admin_line)
        .expect("the first admin");
    stores
        .auth()
        .register_key(gunnar_core::BOOTSTRAP_ADMIN_ID, &front_line, None)
        .expect("the front's console key, an admin's");
    let control = free_addr();
    let ctx = gunnar_grpc::ServiceContext::new(
        Arc::new(CoreControlPlane::new(Arc::clone(&stores))),
        Arc::new(gunnar_grpc::EventBus::new(16)),
        gunnar_grpc::ServerConfig::default(),
    );
    tokio::spawn(async move {
        let _ = gunnar_grpc::router(ctx, || 0).serve(control).await;
    });
    let t0 = Instant::now();
    while std::net::TcpStream::connect(control).is_err() {
        assert!(t0.elapsed() < Duration::from_secs(10), "gunnar listens");
        tokio::time::sleep(Duration::from_millis(25)).await;
    }

    // ── the FRONT: its :443 with a fed certificate for `localhost`, a face, the hole.
    let (ca_pem, chain, key) = site_certificate();
    let ca = dir.path().join("site-ca.pem");
    std::fs::write(&ca, &ca_pem).unwrap();
    let feed = BTreeMap::from([
        (
            gunnar_front::web::FEED_WEB_HOSTS.to_string(),
            format!("localhost,{PAY}"),
        ),
        (
            gunnar_front::web::FEED_WEB_CALLBACK_ONLY.to_string(),
            PAY.to_string(),
        ),
        (gunnar_front::web::certs::fed_cert_key("localhost"), chain),
        (gunnar_front::web::certs::fed_key_key("localhost"), key),
    ]);
    let front_dir = dir.path().join("front");
    std::fs::create_dir_all(&front_dir).unwrap();
    let l80 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
    let l443 = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
    let a443 = l443.local_addr().unwrap();
    let web = gunnar_front::web::start_on_with(
        &front_dir,
        "localhost".into(),
        Arc::new(move || feed.clone()),
        l80,
        l443,
        false,
    );
    web.app
        .set(axum::Router::new().fallback(|| async { "the console\n" }));
    let hole: Hole = web.app.hole();
    let audit: Arc<Mutex<Vec<Forwarded>>> = Arc::new(Mutex::new(Vec::new()));
    let a2 = Arc::clone(&audit);
    hole.set_audit(Arc::new(move |f: &Forwarded| {
        a2.lock().unwrap().push(f.clone())
    }));
    hole.retarget(Some(control.to_string()));
    let t0 = Instant::now();
    while !web.resolver.any() {
        assert!(
            t0.elapsed() < Duration::from_secs(20),
            "the fed certificate is served"
        );
        tokio::time::sleep(Duration::from_millis(50)).await;
    }

    let home = dir.path().join("home");
    std::fs::create_dir_all(&home).unwrap();
    let site = Site {
        at: format!("https://localhost:{}", a443.port()),
        home,
        ca,
    };
    let admin = admin_key.to_str().unwrap();
    let bob = bob_key.to_str().unwrap();
    let bob_pub = format!("@{}", keys.join("bob.pub").display());

    // ── reads and writes, each as its own caller, through the front.
    let r = site.gunnar(&["status"]).await;
    check(
        &mut checks,
        r.ok && r.out.contains("principals"),
        &format!("`control status` (no credential) answers through the front: {r:?}"),
    );

    let r = site.gunnar(&["login", "-i", admin]).await;
    check(
        &mut checks,
        r.ok && r.out.contains("gunnar-admin"),
        &format!("the admin logs in through the front and is the admin: {r:?}"),
    );

    let r = site
        .gunnar(&["user", "add", "bob", "--key", &bob_pub, "-i", admin])
        .await;
    check(
        &mut checks,
        r.ok,
        &format!("the admin adds bob through the front (a write): {r:?}"),
    );

    let r = site.gunnar(&["login", "-i", bob]).await;
    check(
        &mut checks,
        r.ok && r.out.contains("ordinary user"),
        &format!("bob logs in through the front as bob, not as the front's admin key: {r:?}"),
    );

    let r = site.gunnar(&["repo", "create", "bob/one", "-i", bob]).await;
    check(
        &mut checks,
        r.ok,
        &format!("bob creates bob/one through the front (a write): {r:?}"),
    );

    let r = site
        .gunnar(&["repo", "create", "admin/not-bobs", "-i", bob])
        .await;
    check(&mut checks, !r.ok, &format!("bob may not create in the admin's account: the server refuses bob, carried as bob: {r:?}"));

    let r = site.gunnar(&["repo", "list", "-i", bob]).await;
    check(
        &mut checks,
        r.ok && r.out.contains("bob/one") && !r.out.contains("not-bobs"),
        &format!("bob lists repositories through the front (a read): {r:?}"),
    );

    let r = site.gunnar(&["repo", "get", "bob/one", "-i", bob]).await;
    check(
        &mut checks,
        r.ok && r.out.contains("bob/one"),
        &format!("bob reads bob/one through the front: {r:?}"),
    );

    let r = site.gunnar(&["user", "list", "-i", admin]).await;
    check(
        &mut checks,
        r.ok && r.out.contains("bob"),
        &format!("the admin lists users through the front: {r:?}"),
    );

    // ── the audit trail: every call, named by the caller's own key, never the front's.
    let lines = audit.lock().unwrap().clone();
    check(
        &mut checks,
        lines.iter().all(|f| f.answered_by == "main"),
        "every call so far was answered by the main",
    );
    check(
        &mut checks,
        lines
            .iter()
            .any(|f| f.caller == bob_fp && f.method == "Repos.Create" && f.status == Some(0)),
        &format!("bob's create is audited as bob, OK: {lines:?}"),
    );
    check(
        &mut checks,
        lines.iter().any(|f| {
            f.caller == bob_fp && f.method == "Repos.Create" && f.status.is_some_and(|s| s != 0)
        }),
        "bob's refused create is audited as bob, with the server's refusal",
    );
    check(
        &mut checks,
        lines.iter().any(|f| f.caller == admin_fp),
        "the admin's calls are audited as the admin",
    );
    check(
        &mut checks,
        lines.iter().all(|f| f.caller != front_fp),
        "no call is the front's",
    );
    check(
        &mut checks,
        lines
            .iter()
            .any(|f| f.caller == "anonymous" && f.method.starts_with("Bootstrap.")),
        "the uncredentialled status call is audited as anonymous",
    );

    // ── HTTP/1 on the same :443 is the face's, even with a gRPC content type.
    let page = http1_get(a443, &ca_pem, "/gunnar.v1.Repos/List").await;
    check(
        &mut checks,
        page.starts_with("HTTP/1.1 200") && page.contains("the console"),
        &format!("HTTP/1 is untouched: {page}"),
    );

    // ── the limit: past it the CLI is refused, RESOURCE_EXHAUSTED, and audited.
    hole.set_per_minute(1);
    let first = site.gunnar(&["repo", "list", "-i", bob]).await;
    let second = site.gunnar(&["repo", "list", "-i", bob]).await;
    check(
        &mut checks,
        first.ok,
        &format!("within the limit the call passes: {first:?}"),
    );
    let said = second.err.to_ascii_lowercase();
    check(
        &mut checks,
        !second.ok && (said.contains("too many calls") || said.contains("resource")),
        &format!("past the limit the CLI is refused: {second:?}"),
    );
    check(
        &mut checks,
        audit
            .lock()
            .unwrap()
            .iter()
            .any(|f| f.answered_by == "rate-limited" && f.status == Some(8)),
        "the refusal is audited as rate-limited",
    );
    hole.set_per_minute(gunnar_front::web::hole::PER_CLIENT_PER_MINUTE);

    // ── RA1-1: the hole keeps the face's rules ─────────────────────────────
    // The callback-only name and a name the front does not serve carry nothing.
    for host in [PAY, "127.0.0.1"] {
        let r = raw_grpc(a443, &ca_pem, host, "/gunnar.v1.Repos/List", None).await;
        check(
            &mut checks,
            r.status == Some(7) && r.message.contains("front-hole-name"),
            &format!("{host} carries no control plane: {r:?}"),
        );
    }
    check(
        &mut checks,
        audit
            .lock()
            .unwrap()
            .iter()
            .any(|f| f.answered_by == "refused-name"),
        "the refusal is audited as refused-name",
    );

    // Own-network, the default: a public client (as the console reads it) is
    // refused at the front; `public` carries it to the main.
    check(
        &mut checks,
        hole.reach() == gunnar_front::web::hole::Reach::OwnNetwork,
        "the hole is own-network by default",
    );
    let r = raw_grpc(
        a443,
        &ca_pem,
        "localhost",
        "/gunnar.v1.Repos/List",
        Some("203.0.113.7"),
    )
    .await;
    check(
        &mut checks,
        r.status == Some(7) && r.message.contains("front-hole-network"),
        &format!("a public client is refused by default: {r:?}"),
    );
    let r = raw_grpc(
        a443,
        &ca_pem,
        "localhost",
        "/gunnar.v1.Repos/List",
        Some("10.13.8.20"),
    )
    .await;
    check(
        &mut checks,
        r.status != Some(7),
        &format!("a client on the estate's own network is carried: {r:?}"),
    );
    hole.set_reach(gunnar_front::web::hole::Reach::Public);
    let n = audit.lock().unwrap().len();
    let r = raw_grpc(
        a443,
        &ca_pem,
        "localhost",
        "/gunnar.v1.Repos/List",
        Some("203.0.113.7"),
    )
    .await;
    check(
        &mut checks,
        r.status != Some(7),
        &format!("set `public`, a public client is carried: {r:?}"),
    );
    check(
        &mut checks,
        audit.lock().unwrap()[n..]
            .iter()
            .any(|f| f.answered_by == "main" && f.client == "203.0.113.7"),
        "and the main answered it",
    );

    // An UNENROLLED box behind the front: nobody can claim it through any of its names.
    let fresh_stores =
        Arc::new(ServerStores::open(dir.path().join("fresh")).expect("fresh stores"));
    let fresh = free_addr();
    let ctx = gunnar_grpc::ServiceContext::new(
        Arc::new(CoreControlPlane::new(Arc::clone(&fresh_stores))),
        Arc::new(gunnar_grpc::EventBus::new(16)),
        gunnar_grpc::ServerConfig::default(),
    );
    tokio::spawn(async move {
        let _ = gunnar_grpc::router(ctx, || 0).serve(fresh).await;
    });
    let t0 = Instant::now();
    while std::net::TcpStream::connect(fresh).is_err() {
        assert!(
            t0.elapsed() < Duration::from_secs(10),
            "the fresh box listens"
        );
        tokio::time::sleep(Duration::from_millis(25)).await;
    }
    hole.retarget(Some(fresh.to_string()));
    let r = site.gunnar(&["status"]).await;
    check(&mut checks, r.ok && r.out.contains("bootstrap      OPEN"), &format!("the fresh box's window is open, and `control status` reads it through the front: {r:?}"));
    let r = site
        .gunnar(&["bootstrap", "mallory", "--key", &bob_pub])
        .await;
    check(
        &mut checks,
        !r.ok && r.err.contains("front-hole-method"),
        &format!("`control bootstrap` through the front is refused at the front, by name: {r:?}"),
    );
    for host in ["localhost", PAY, "127.0.0.1"] {
        for from in [None, Some("203.0.113.7")] {
            for method in [
                "Enroll",
                "BecomeTwin",
                "Restore",
                "AssignTunnelAddress",
                "TwinCsr",
                "InstallTwinCertificate",
            ] {
                let r = raw_grpc(
                    a443,
                    &ca_pem,
                    host,
                    &format!("/gunnar.v1.Bootstrap/{method}"),
                    from,
                )
                .await;
                check(
                    &mut checks,
                    r.status == Some(7),
                    &format!(
                        "Bootstrap.{method} on {host} from {from:?} is refused at the front: {r:?}"
                    ),
                );
            }
        }
    }
    check(
        &mut checks,
        audit
            .lock()
            .unwrap()
            .iter()
            .filter(|f| f.method.starts_with("Bootstrap.") && f.method != "Bootstrap.Status")
            .all(|f| f.answered_by != "main"),
        "no claiming call reached the box",
    );
    let r = site.gunnar(&["status"]).await;
    check(
        &mut checks,
        r.ok && r.out.contains("bootstrap      OPEN") && r.out.contains("principals     0"),
        &format!("after every try the box is still unclaimed: {r:?}"),
    );
    checks
}