mod common;
use std::net::SocketAddr;
use std::pin::Pin;
use std::sync::Arc;
use std::time::{Duration, Instant};
use common::*;
use gunnar_ssh::seam::{DuplexStream, GitTransport, ServiceRequest, TransportError};
use gunnar_ssh::{Access, GunnarSsh, SshConfig};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::watch;
struct Flood;
impl GitTransport for Flood {
fn serve<'a>(
&'a self,
_request: ServiceRequest,
mut stream: Box<dyn DuplexStream>,
) -> Pin<Box<dyn std::future::Future<Output = Result<(), TransportError>> + Send + 'a>> {
Box::pin(async move {
let chunk = vec![0x5au8; 64 * 1024];
for _ in 0..4096 {
stream.write_all(&chunk).await?;
}
stream.shutdown().await?;
Ok(())
})
}
}
struct Building;
impl GitTransport for Building {
fn serve<'a>(
&'a self,
request: ServiceRequest,
mut stream: Box<dyn DuplexStream>,
) -> Pin<Box<dyn std::future::Future<Output = Result<(), TransportError>> + Send + 'a>> {
Box::pin(async move {
tokio::select! {
() = tokio::time::sleep(Duration::from_secs(120)) => {}
() = request.cancel.cancelled() => {
return Err(TransportError::Io(std::io::Error::new(std::io::ErrorKind::ConnectionAborted, "told to stop mid-build")));
}
}
let chunk = vec![0x5au8; 64 * 1024];
for _ in 0..4096 {
stream.write_all(&chunk).await?;
}
stream.shutdown().await?;
Ok(())
})
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Mode {
Forward,
Silent,
Close,
}
async fn proxy(upstream: SocketAddr) -> (u16, watch::Sender<Mode>) {
let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0))
.await
.expect("bind proxy");
let port = listener.local_addr().expect("addr").port();
let (tx, rx) = watch::channel(Mode::Forward);
tokio::spawn(async move {
loop {
let Ok((client, _)) = listener.accept().await else {
continue;
};
let Ok(server) = tokio::net::TcpStream::connect(upstream).await else {
continue;
};
let (cr, cw) = client.into_split();
let (sr, sw) = server.into_split();
tokio::spawn(pump(cr, sw, rx.clone()));
tokio::spawn(pump(sr, cw, rx.clone()));
}
});
(port, tx)
}
async fn pump(
mut src: tokio::net::tcp::OwnedReadHalf,
mut dst: tokio::net::tcp::OwnedWriteHalf,
mut mode: watch::Receiver<Mode>,
) {
let mut buf = vec![0u8; 64 * 1024];
loop {
let now = *mode.borrow_and_update();
match now {
Mode::Forward => {}
Mode::Close => return,
Mode::Silent => loop {
if mode.changed().await.is_err() {
return;
}
let next = *mode.borrow_and_update();
if next == Mode::Close {
return;
}
},
}
tokio::select! {
r = src.read(&mut buf) => match r {
Ok(0) | Err(_) => return,
Ok(n) => {
if *mode.borrow() == Mode::Forward && dst.write_all(&buf[..n]).await.is_err() {
return;
}
}
},
_ = mode.changed() => {}
}
}
}
struct Patient;
impl russh::client::Handler for Patient {
type Error = russh::Error;
async fn check_server_key(
&mut self,
_: &russh::keys::PublicKeyOrCertificate,
) -> Result<bool, Self::Error> {
Ok(true)
}
}
async fn wait_for(bound: Duration, mut f: impl FnMut() -> bool) -> bool {
let until = Instant::now() + bound;
while Instant::now() < until {
if f() {
return true;
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
f()
}
async fn parked_fetch(
port: u16,
key: &russh::keys::ssh_key::PrivateKey,
) -> russh::client::Handle<Patient> {
let config = Arc::new(russh::client::Config::default());
let mut session = russh::client::connect(config, ("127.0.0.1", port), Patient)
.await
.expect("connect");
let ok = session
.authenticate_publickey(
"git",
russh::keys::PrivateKeyWithHashAlg::new(Arc::new(key.clone()), None),
)
.await
.expect("auth");
assert!(ok.success(), "the key is registered");
let channel = session.channel_open_session().await.expect("channel");
channel
.exec(true, "git-upload-pack 'nordisk/osm-katana.git'")
.await
.expect("exec");
std::mem::forget(channel);
session
}
async fn a_dead_client_releases_its_transfer(kill: Mode, bound: Duration) {
let key = mint_key();
let mut config = SshConfig::ephemeral_loopback();
config.keepalive_interval = Some(Duration::from_secs(1));
let server = GunnarSsh::new(
config,
plane_with_key(&key, Access::Read).control,
Arc::new(Flood),
)
.bind(mint_key())
.await
.expect("bind");
let (port, mode) = proxy(server.local_addr()).await;
let _client = parked_fetch(port, &key).await;
assert!(
wait_for(Duration::from_secs(10), || server
.metrics()
.transfers_in_flight
== 1)
.await,
"the flood never started"
);
if kill == Mode::Close {
tokio::time::sleep(Duration::from_millis(1000)).await;
assert_eq!(
server.metrics().transfers_in_flight,
1,
"parked, still in flight"
);
}
let killed = Instant::now();
mode.send_replace(kill);
let released = wait_for(bound, || server.metrics().transfers_in_flight == 0).await;
assert!(
released,
"{kill:?}: the transfer to a dead client was still in flight {:?} after the link died \
(fan worker, admission slot and store handle held for good)",
killed.elapsed()
);
eprintln!(
"{kill:?}: transfer released {:?} after the link died",
killed.elapsed()
);
mode.send_replace(Mode::Close);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_client_that_hangs_up_mid_clone_releases_its_transfer_at_once() {
a_dead_client_releases_its_transfer(Mode::Close, Duration::from_millis(2500)).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_client_whose_link_goes_silent_mid_clone_releases_its_transfer() {
a_dead_client_releases_its_transfer(Mode::Silent, Duration::from_secs(25)).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_client_that_goes_silent_while_the_pack_is_built_releases_its_transfer() {
let key = mint_key();
let mut config = SshConfig::ephemeral_loopback();
config.keepalive_interval = Some(Duration::from_secs(1));
let server = GunnarSsh::new(
config,
plane_with_key(&key, Access::Read).control,
Arc::new(Building),
)
.bind(mint_key())
.await
.expect("bind");
let (port, mode) = proxy(server.local_addr()).await;
let _client = parked_fetch(port, &key).await;
assert!(
wait_for(Duration::from_secs(10), || server
.metrics()
.transfers_in_flight
== 1)
.await,
"the build never started"
);
let killed = Instant::now();
mode.send_replace(Mode::Silent);
let released = wait_for(Duration::from_secs(15), || {
server.metrics().transfers_in_flight == 0
})
.await;
assert!(
released,
"a client silent while the pack is still being built kept its transfer {:?} after the link died",
killed.elapsed()
);
mode.send_replace(Mode::Close);
}
#[test]
fn the_shipped_server_keepalive_is_fifteen_seconds() {
assert_eq!(
SshConfig::default().keepalive_interval,
Some(Duration::from_secs(15))
);
}