mod common;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use erbridge_core::config::{ConnectConfig, ConnectTunnel, ServeConfig, ServeTunnel};
use erbridge_core::reverse::connect::{run_connect, run_connect_with_shutdown};
use erbridge_core::reverse::serve::{run_serve, run_serve_with_shutdown};
use erbridge_core::stats::Registry;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio_util::sync::CancellationToken;
#[tokio::test]
async fn reverse_tunnel_roundtrip() {
let target_port = common::free_port();
let control_port = common::free_port();
let external_port = common::free_port();
let target_addr = format!("127.0.0.1:{target_port}").parse().unwrap();
tokio::spawn(common::run_tcp_echo_server(target_addr));
tokio::time::sleep(Duration::from_millis(50)).await;
let serve_cfg = ServeConfig {
listen: format!("127.0.0.1:{control_port}"),
token: "test-token".into(),
tunnel: vec![ServeTunnel {
name: "web".into(),
external: format!("127.0.0.1:{external_port}"),
}],
..Default::default()
};
let connect_cfg = ConnectConfig {
server: format!("127.0.0.1:{control_port}"),
token: "test-token".into(),
tunnel: vec![ConnectTunnel {
name: "web".into(),
target: format!("127.0.0.1:{target_port}"),
}],
reconnect_min_secs: 1,
reconnect_max_secs: 2,
};
let serve_registry = Registry::new();
let connect_registry = Registry::new();
tokio::spawn(run_serve(serve_cfg, serve_registry.clone()));
tokio::time::sleep(Duration::from_millis(50)).await;
tokio::spawn(run_connect(connect_cfg, connect_registry.clone()));
let external_addr: std::net::SocketAddr = format!("127.0.0.1:{external_port}").parse().unwrap();
let echoed = tokio::time::timeout(
Duration::from_secs(5),
common::tcp_roundtrip(external_addr, b"through the reverse tunnel"),
)
.await
.expect("reverse roundtrip timed out");
assert_eq!(echoed, b"through the reverse tunnel");
assert_eq!(serve_registry.totals().total_connections, 1);
assert_eq!(connect_registry.totals().total_connections, 1);
}
#[tokio::test]
async fn multiple_streams_share_one_control_connection() {
let target_port = common::free_port();
let control_port = common::free_port();
let external_port = common::free_port();
let target_addr = format!("127.0.0.1:{target_port}").parse().unwrap();
tokio::spawn(common::run_tcp_echo_server(target_addr));
tokio::time::sleep(Duration::from_millis(50)).await;
let serve_cfg = ServeConfig {
listen: format!("127.0.0.1:{control_port}"),
token: "shared-token".into(),
tunnel: vec![ServeTunnel {
name: "web".into(),
external: format!("127.0.0.1:{external_port}"),
}],
..Default::default()
};
let connect_cfg = ConnectConfig {
server: format!("127.0.0.1:{control_port}"),
token: "shared-token".into(),
tunnel: vec![ConnectTunnel {
name: "web".into(),
target: format!("127.0.0.1:{target_port}"),
}],
reconnect_min_secs: 1,
reconnect_max_secs: 2,
};
tokio::spawn(run_serve(serve_cfg, Registry::new()));
tokio::time::sleep(Duration::from_millis(50)).await;
tokio::spawn(run_connect(connect_cfg, Registry::new()));
let external_addr: std::net::SocketAddr = format!("127.0.0.1:{external_port}").parse().unwrap();
let mut handles = Vec::new();
for i in 0..8 {
let payload = format!("stream-{i}").into_bytes();
handles.push(tokio::spawn(async move {
let echoed = tokio::time::timeout(
Duration::from_secs(5),
common::tcp_roundtrip(external_addr, &payload),
)
.await
.expect("roundtrip timed out");
assert_eq!(echoed, payload);
}));
}
for h in handles {
h.await.unwrap();
}
}
#[tokio::test]
async fn rejects_mismatched_psk() {
let control_port = common::free_port();
let external_port = common::free_port();
let serve_cfg = ServeConfig {
listen: format!("127.0.0.1:{control_port}"),
token: "correct-token".into(),
tunnel: vec![ServeTunnel {
name: "web".into(),
external: format!("127.0.0.1:{external_port}"),
}],
..Default::default()
};
let connect_cfg = ConnectConfig {
server: format!("127.0.0.1:{control_port}"),
token: "wrong-token".into(),
tunnel: vec![ConnectTunnel {
name: "web".into(),
target: "127.0.0.1:1".into(),
}],
reconnect_min_secs: 1,
reconnect_max_secs: 1,
};
let serve_registry = Registry::new();
tokio::spawn(run_serve(serve_cfg, serve_registry.clone()));
tokio::time::sleep(Duration::from_millis(50)).await;
tokio::spawn(run_connect(connect_cfg, Registry::new()));
tokio::time::sleep(Duration::from_millis(500)).await;
let log = serve_registry.recent_log();
assert!(
log.iter().any(|line| line.contains("Noise handshake")),
"expected a Noise handshake failure log line, got: {log:?}"
);
assert_eq!(serve_registry.totals().total_connections, 0);
}
#[tokio::test]
async fn carries_more_concurrent_connections_than_the_yamux_ack_backlog() {
const CONNECTIONS: usize = 300;
let target_port = common::free_port();
let control_port = common::free_port();
let external_port = common::free_port();
let accepted = Arc::new(AtomicUsize::new(0));
let target_addr = format!("127.0.0.1:{target_port}").parse().unwrap();
tokio::spawn(common::run_silent_tcp_server(target_addr, accepted.clone()));
tokio::time::sleep(Duration::from_millis(50)).await;
let serve_cfg = ServeConfig {
listen: format!("127.0.0.1:{control_port}"),
token: "burst-token".into(),
tunnel: vec![ServeTunnel {
name: "web".into(),
external: format!("127.0.0.1:{external_port}"),
}],
..Default::default()
};
let connect_cfg = ConnectConfig {
server: format!("127.0.0.1:{control_port}"),
token: "burst-token".into(),
tunnel: vec![ConnectTunnel {
name: "web".into(),
target: format!("127.0.0.1:{target_port}"),
}],
reconnect_min_secs: 1,
reconnect_max_secs: 2,
};
tokio::spawn(run_serve(serve_cfg, Registry::new()));
tokio::time::sleep(Duration::from_millis(50)).await;
tokio::spawn(run_connect(connect_cfg, Registry::new()));
let external_addr: std::net::SocketAddr = format!("127.0.0.1:{external_port}").parse().unwrap();
let mut clients = Vec::new();
for _ in 0..CONNECTIONS {
clients.push(tokio::spawn(async move {
let mut stream = tokio::net::TcpStream::connect(external_addr)
.await
.expect("connect to external listener");
stream
.write_all(b"client speaks first")
.await
.expect("write");
stream }));
}
let mut held = Vec::new();
for client in clients {
held.push(client.await.expect("client task"));
}
tokio::time::timeout(Duration::from_secs(20), async {
while accepted.load(Ordering::SeqCst) < CONNECTIONS {
tokio::time::sleep(Duration::from_millis(20)).await;
}
})
.await
.unwrap_or_else(|_| {
panic!(
"only {} of {CONNECTIONS} connections reached the target",
accepted.load(Ordering::SeqCst)
)
});
}
#[tokio::test]
async fn closes_the_client_when_b_cannot_reach_the_target() {
let control_port = common::free_port();
let external_port = common::free_port();
let dead_port = common::free_port();
let serve_cfg = ServeConfig {
listen: format!("127.0.0.1:{control_port}"),
token: "dead-target".into(),
tunnel: vec![ServeTunnel {
name: "web".into(),
external: format!("127.0.0.1:{external_port}"),
}],
..Default::default()
};
let connect_cfg = ConnectConfig {
server: format!("127.0.0.1:{control_port}"),
token: "dead-target".into(),
tunnel: vec![ConnectTunnel {
name: "web".into(),
target: format!("127.0.0.1:{dead_port}"),
}],
reconnect_min_secs: 1,
reconnect_max_secs: 2,
};
tokio::spawn(run_serve(serve_cfg, Registry::new()));
tokio::time::sleep(Duration::from_millis(50)).await;
tokio::spawn(run_connect(connect_cfg, Registry::new()));
tokio::time::sleep(Duration::from_millis(300)).await;
let external_addr: std::net::SocketAddr = format!("127.0.0.1:{external_port}").parse().unwrap();
let mut stream = tokio::net::TcpStream::connect(external_addr)
.await
.expect("connect to external listener");
let mut buf = [0u8; 1];
let read = tokio::time::timeout(Duration::from_secs(5), stream.read(&mut buf))
.await
.expect("A left the client hanging after B failed to reach the target")
.expect("reading from the external connection");
assert_eq!(read, 0, "expected the connection to be closed, got a byte");
}
#[tokio::test]
async fn shutdown_stops_both_roles_and_releases_ports() {
let target_port = common::free_port();
let control_port = common::free_port();
let external_port = common::free_port();
let target_addr = format!("127.0.0.1:{target_port}").parse().unwrap();
tokio::spawn(common::run_tcp_echo_server(target_addr));
tokio::time::sleep(Duration::from_millis(50)).await;
let serve_cfg = ServeConfig {
listen: format!("127.0.0.1:{control_port}"),
token: "shutdown-token".into(),
tunnel: vec![ServeTunnel {
name: "web".into(),
external: format!("127.0.0.1:{external_port}"),
}],
..Default::default()
};
let connect_cfg = ConnectConfig {
server: format!("127.0.0.1:{control_port}"),
token: "shutdown-token".into(),
tunnel: vec![ConnectTunnel {
name: "web".into(),
target: format!("127.0.0.1:{target_port}"),
}],
reconnect_min_secs: 1,
reconnect_max_secs: 2,
};
let shutdown = CancellationToken::new();
let serve = tokio::spawn(run_serve_with_shutdown(
serve_cfg,
Registry::new(),
shutdown.clone(),
));
tokio::time::sleep(Duration::from_millis(50)).await;
let connect = tokio::spawn(run_connect_with_shutdown(
connect_cfg,
Registry::new(),
shutdown.clone(),
));
let external_addr: std::net::SocketAddr = format!("127.0.0.1:{external_port}").parse().unwrap();
let mut held = tokio::net::TcpStream::connect(external_addr).await.unwrap();
held.write_all(b"ping").await.unwrap();
let mut buf = [0u8; 4];
tokio::time::timeout(Duration::from_secs(5), held.read_exact(&mut buf))
.await
.expect("echo timed out")
.unwrap();
shutdown.cancel();
tokio::time::timeout(Duration::from_secs(2), serve)
.await
.expect("serve did not stop")
.unwrap()
.unwrap();
tokio::time::timeout(Duration::from_secs(2), connect)
.await
.expect("connect did not stop")
.unwrap()
.unwrap();
let mut rest = Vec::new();
let closed = tokio::time::timeout(Duration::from_secs(2), held.read_to_end(&mut rest)).await;
assert!(closed.is_ok(), "held connection stayed open after shutdown");
std::net::TcpListener::bind(("127.0.0.1", external_port)).expect("external port still bound");
std::net::TcpListener::bind(("127.0.0.1", control_port)).expect("control port still bound");
}