gunnar-ssh 3.0.0

gunnar's SSH server: a russh listener with publickey identity, a strict git exec dispatch, and the repository path sandbox.
Documentation
//! **The listener outlives `EMFILE`.**
//!
//! MEASURED 2026-08-29 on the rig: once the process had no descriptor left,
//! one failed `accept` ended russh's accept loop and every later ssh client got
//! `Connection reset by peer` until reboot, while gRPC kept answering. Here the
//! process is starved of descriptors on purpose until the listener's `accept`
//! fails, the descriptors are given back, and a real client must still be able
//! to authenticate on the SAME listener.

mod common;

use std::net::TcpStream;
use std::sync::Arc;
use std::time::Duration;

use common::mint_key;
use gunnar_ssh::Access;
use rustix::process::{getrlimit, setrlimit, Resource};

fn open_fds() -> u64 {
    std::fs::read_dir("/proc/self/fd")
        .map(|d| d.count() as u64)
        .unwrap_or(0)
}

async fn authenticates(addr: std::net::SocketAddr, key: &russh::keys::PrivateKey) -> bool {
    struct Trusting;
    impl russh::client::Handler for Trusting {
        type Error = russh::Error;
        async fn check_server_key(
            &mut self,
            _: &russh::keys::PublicKeyOrCertificate,
        ) -> Result<bool, Self::Error> {
            Ok(true)
        }
    }
    let config = Arc::new(russh::client::Config::default());
    let connect = russh::client::connect(config, addr, Trusting);
    let Ok(Ok(mut session)) = tokio::time::timeout(Duration::from_secs(5), connect).await else {
        return false;
    };
    let auth = session.authenticate_publickey(
        "git",
        russh::keys::PrivateKeyWithHashAlg::new(Arc::new(key.clone()), None),
    );
    matches!(tokio::time::timeout(Duration::from_secs(5), auth).await, Ok(Ok(a)) if a.success())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_listener_survives_running_out_of_descriptors() {
    let key = mint_key();
    let fx = common::start_with_key(&key, Access::Read).await;
    let addr = fx.server.local_addr();
    assert!(
        authenticates(addr, &key).await,
        "the listener must work before the test starves it"
    );

    // ── starve the process ──────────────────────────────────────────────────
    let original = getrlimit(Resource::Nofile);
    let mut starved = original;
    starved.current = Some(open_fds() + 6);
    setrlimit(Resource::Nofile, starved).expect("lower the soft limit");

    // Open raw TCP connections until the process has no descriptor left. Each
    // one costs this side a descriptor AND, once accepted, the listener one;
    // the ones the listener cannot take sit in the backlog and its `accept`
    // fails with EMFILE — the rig's exact state.
    let mut held = Vec::new();
    let mut client_refused = None;
    for _ in 0..64 {
        match TcpStream::connect(addr) {
            Ok(s) => held.push(s),
            Err(e) => {
                client_refused = Some(e);
                break;
            }
        }
    }
    let refused = client_refused.expect("the process never ran out of descriptors");
    assert_eq!(
        refused.raw_os_error(),
        Some(24),
        "expected EMFILE, got {refused}"
    );
    // Give the accept loop time to hit the wall and log its WARN.
    tokio::time::sleep(Duration::from_millis(300)).await;

    // ── give them back ──────────────────────────────────────────────────────
    drop(held);
    setrlimit(Resource::Nofile, original).expect("restore the soft limit");
    tokio::time::sleep(Duration::from_millis(300)).await;

    // ── the SAME listener must still answer ─────────────────────────────────
    assert!(
        authenticates(addr, &key).await,
        "the listener went deaf after EMFILE — the accept loop died with the first failed accept"
    );
    fx.server.shutdown("test over").await.expect("shutdown");
}