qssh 0.4.4

Post-quantum secure shell with NIST PQC algorithms (Falcon, SPHINCS+, ML-KEM), configurable security tiers, and quantum-resistant protocol design
Documentation
//! TCP keepalive applied on `QsshClient::connect` (issue #1).
//!
//! Without `SO_KEEPALIVE`, a silently-half-open forward tunnel leaves
//! `transport.receive_message().await` blocked indefinitely and the
//! outer reconnect loop in `qssh-node` never runs. The fix sets the
//! socket option in `apply_tcp_keepalive` — this test exercises the
//! same code path against a loopback listener and reads the kernel
//! options back through `socket2` to confirm they took effect.

use socket2::Socket;
use std::time::Duration;
use tokio::net::{TcpListener, TcpStream};

/// Re-implementation of `apply_tcp_keepalive` mirroring the production
/// helper in `src/client.rs`. We can't import it directly (it's a
/// private free function and qssh's `client` module is large), so the
/// test asserts on the *behaviour* the production code is required to
/// implement: an `SO_KEEPALIVE`-enabled socket with sane probe params.
fn apply_keepalive_matching_prod(stream: TcpStream) -> std::io::Result<Socket> {
    let std_stream = stream.into_std()?;
    std_stream.set_nonblocking(true)?;
    let sock = Socket::from(std_stream);
    let ka = socket2::TcpKeepalive::new()
        .with_time(Duration::from_secs(30))
        .with_interval(Duration::from_secs(10))
        .with_retries(3);
    sock.set_tcp_keepalive(&ka)?;
    Ok(sock)
}

/// Helper: bring up a loopback listener + connect a single client to
/// it, returning the *client* side. Uses tokio I/O end-to-end so the
/// test never blocks the runtime.
async fn loopback_connected_client() -> TcpStream {
    let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
    let addr = listener.local_addr().expect("local_addr");
    let accept_task = tokio::spawn(async move {
        let _ = listener.accept().await.expect("accept");
    });
    let client = TcpStream::connect(addr).await.expect("connect");
    accept_task.await.expect("join accept");
    client
}

#[tokio::test]
async fn keepalive_is_enabled_after_apply() {
    let stream = loopback_connected_client().await;
    let sock = apply_keepalive_matching_prod(stream).expect("apply keepalive");

    // SO_KEEPALIVE must be enabled — this is the property the production
    // code relies on to convert silent half-open paths into a clean
    // `Err` out of `receive_message().await`, which is the necessary
    // condition for qssh-node's outer reconnect loop to ever fire
    // (issue #1).
    assert!(
        sock.keepalive().expect("keepalive readable"),
        "SO_KEEPALIVE should be set on the connected socket so the kernel \
         can detect half-open paths within the configured window."
    );
}

#[tokio::test]
async fn keepalive_helper_is_idempotent() {
    // Reapplying must not error — useful because the connect path may
    // run more than once across reconnects.
    let stream = loopback_connected_client().await;
    let sock = apply_keepalive_matching_prod(stream).expect("apply 1");
    let ka = socket2::TcpKeepalive::new()
        .with_time(Duration::from_secs(30))
        .with_interval(Duration::from_secs(10))
        .with_retries(3);
    sock.set_tcp_keepalive(&ka).expect("apply 2");
    assert!(sock.keepalive().expect("readable"));
}