Files
TrustTunnel/lib/tests/tunnel.rs
Andrey Yakushin 783908b315 Pull request 121: Clippy and github actions
Squashed commit of the following:

commit 6eae1e962a27b2c3bcb6362f53bb1d7d92a66983
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Fri Dec 26 11:46:12 2025 +0400

    Run lint on both macos and linux

commit 94254caec3ea166db80c6b3f4004b4126605a1b7
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Dec 25 22:44:10 2025 +0400

    Fix note again by adding lint hint

commit 5a67ae358a5676a22e85798683674607d2788a51
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Dec 25 22:42:02 2025 +0400

    Fix note

commit 937b178302244fe237d06b6f38ba0f29db6e0d7e
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Dec 25 22:39:45 2025 +0400

    Fix README

commit 769c5d9ebdc03e8500f9fc00d7f2b6f316924557
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Dec 25 22:39:15 2025 +0400

    Cargo update

commit 1e932e4037c2b9ffc4b12f398f1ef14c32b5481e
Merge: dcf6a53 2041edc
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Dec 25 22:37:24 2025 +0400

    Merge remote-tracking branch 'origin/master' into feature/TRUST-235

commit dcf6a53410e59411a3e05f798ed4be7f7c9994ce
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Dec 25 22:24:56 2025 +0400

    Get rid of rustls-pemfile and update sentry

commit cb2e26e47d4612d65ae990ec887875bb1ac94456
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Dec 25 19:08:23 2025 +0400

    Fix tests

commit a3cde3fdf16edfe2e2a574b8d729c2b9d59daf84
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Dec 25 18:33:46 2025 +0400

    Fix vulnerabilities

commit 35cb9c699a0ddf2eb344c7c475be3c36a26dbf83
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Dec 25 17:13:18 2025 +0400

    Don't install cargo-audit manually

commit 71a5411ac4fe31fc08c3bacb83d327bf6b7ab8c3
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Dec 25 17:07:30 2025 +0400

    Install stable rust for cargo-audit

commit b7f38a90054cda39d72760b0ebc3ce295fba95d2
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Dec 25 16:54:13 2025 +0400

    Fix yaml

commit fbbe78f68b2987280874f23d4ed05ef75ed42f46
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Dec 25 16:53:15 2025 +0400

    Try to lock cargo-audit version

commit 08f31734b49c70d9dc03c7977ac6182198d1cbde
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Dec 25 16:46:06 2025 +0400

    Update audit workflow

commit c202f186cd1610439a13928fc1fabac88e83097b
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Dec 25 16:30:55 2025 +0400

    Install rust tools and better rust cache

commit eccf2fa91efcc4c6e5684960e368892bc68e67cd
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Dec 25 16:30:25 2025 +0400

    Name for job

commit dccc19f13180e767b8390c8ea32fde4285c0cab8
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Dec 25 16:30:17 2025 +0400

    Update checkout step version

commit edbb4404bf6fc1927f0184433df9982767a9c762
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Dec 25 16:30:02 2025 +0400

    Run lint only on linux

commit b59ed893fa55edf030f9ffee2e442c8b947fa28f
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Dec 25 16:09:43 2025 +0400

    Lint in the same workflow as testing to avoid rebuilds

commit 8d8ecd51859c825d0437361f8c51bde6b46994bc
Author: Andrey Yakushin <a.yakushin@adguard.com>
Date:   Thu Dec 25 15:27:23 2025 +0400

    More clippy fixes

... and 6 more commits
2025-12-26 12:45:09 +03:00

550 lines
18 KiB
Rust

use bytes::BufMut;
use futures::{future, FutureExt, StreamExt};
use http::Request;
use log::info;
use std::future::Future;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::pin::Pin;
use std::thread;
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::net::{TcpListener, UdpSocket};
use trusttunnel::net_utils;
#[allow(dead_code)]
mod common;
const TCP_CONTENT_SIZE: usize = 2 * 1024 * 1024;
const UDP_CHUNK_SIZE: usize = 1024;
const UDP_CONTENT_SIZE: usize = 8 * UDP_CHUNK_SIZE;
const MANGLED_UDP_HEADER_LENGTH: usize = 4 + 2 * (16 + 2);
const EXPECTED_MANGLED_UDP_LENGTH: usize =
UDP_CONTENT_SIZE + (UDP_CONTENT_SIZE / UDP_CHUNK_SIZE) * MANGLED_UDP_HEADER_LENGTH;
macro_rules! tcp_download_tests {
($($name:ident: $make_tunnel_fn:expr,)*) => {
$(
#[tokio::test]
async fn $name() {
common::set_up_logger();
let endpoint_address = common::make_endpoint_address();
let client_task = async {
let server_address = run_tcp_server(true);
tokio::time::sleep(Duration::from_secs(1)).await;
let (conn_driver, io) = $make_tunnel_fn(endpoint_address, server_address.to_string()).await;
let exchange = async {
let mut io = io.await;
let mut total = 0;
let mut buf = [0; 64 * 1024];
while total < TCP_CONTENT_SIZE {
match io.read(&mut buf).await.unwrap() {
0 => break,
n => total += n,
}
}
assert_eq!(total, TCP_CONTENT_SIZE);
};
futures::pin_mut!(exchange);
match future::select(conn_driver, exchange).await {
future::Either::Left((r, exchange)) => {
info!("HTTP connection closed with result: {:?}", r);
exchange.await
}
future::Either::Right(_) => (),
}
};
tokio::select! {
_ = common::run_endpoint(&endpoint_address) => unreachable!(),
_ = client_task => (),
_ = tokio::time::sleep(Duration::from_secs(10)) => panic!("Timed out"),
}
}
)*
}
}
macro_rules! tcp_upload_tests {
($($name:ident: $make_tunnel_fn:expr,)*) => {
$(
#[tokio::test]
async fn $name() {
common::set_up_logger();
let endpoint_address = common::make_endpoint_address();
let client_task = async {
let server_address = run_tcp_server(false);
tokio::time::sleep(Duration::from_secs(1)).await;
let (conn_driver, io) = $make_tunnel_fn(endpoint_address, server_address.to_string()).await;
let exchange = async {
let mut io = io.await;
let mut content = common::make_stream_of_chunks(TCP_CONTENT_SIZE, None);
while let Some(chunk) = content.next().await {
io.write_all(chunk).await.unwrap();
}
io.flush().await.unwrap();
let mut ack = [0; 1];
assert_eq!(io.read(&mut ack).await.unwrap(), 1);
};
futures::pin_mut!(exchange);
match future::select(conn_driver, exchange).await {
future::Either::Left((r, exchange)) => {
info!("HTTP connection closed with result: {:?}", r);
exchange.await
}
future::Either::Right(_) => (),
}
};
tokio::select! {
_ = common::run_endpoint(&endpoint_address) => unreachable!(),
_ = client_task => (),
_ = tokio::time::sleep(Duration::from_secs(10)) => panic!("Timed out"),
}
}
)*
}
}
tcp_download_tests! {
h1_tcp_download: make_h1_tunnel,
h2_tcp_download: make_h2_tunnel,
}
tcp_upload_tests! {
h1_tcp_upload: make_h1_tunnel,
h2_tcp_upload: make_h2_tunnel,
}
#[tokio::test]
async fn h2_udp_download() {
common::set_up_logger();
let endpoint_address = common::make_endpoint_address();
let client_task = async {
let server_address = run_udp_server(true);
tokio::time::sleep(Duration::from_secs(1)).await;
let (conn_driver, io) = make_h2_tunnel(endpoint_address, "_udp2".to_string()).await;
let exchange = async {
let mut io = io.await;
let hole_puncher = encode_udp_chunk(&server_address, &[1]);
io.write_all(&hole_puncher).await.unwrap();
let mut total = 0;
let mut buf = [0; 64 * 1024];
while total < EXPECTED_MANGLED_UDP_LENGTH {
match io.read(&mut buf).await.unwrap() {
0 => break,
n => total += n,
}
}
assert_eq!(total, EXPECTED_MANGLED_UDP_LENGTH);
};
futures::pin_mut!(exchange);
match future::select(conn_driver, exchange).await {
future::Either::Left((r, exchange)) => {
info!("HTTP connection closed with result: {:?}", r);
exchange.await
}
future::Either::Right(_) => (),
}
};
tokio::select! {
_ = common::run_endpoint(&endpoint_address) => unreachable!(),
_ = client_task => (),
_ = tokio::time::sleep(Duration::from_secs(10)) => panic!("Timed out"),
}
}
#[tokio::test]
async fn h2_udp_upload() {
common::set_up_logger();
let endpoint_address = common::make_endpoint_address();
let client_task = async {
let server_address = run_udp_server(false);
tokio::time::sleep(Duration::from_secs(1)).await;
let (conn_driver, io) = make_h2_tunnel(endpoint_address, "_udp2".to_string()).await;
let exchange = async {
let mut io = io.await;
let mut content = common::make_stream_of_chunks(UDP_CONTENT_SIZE, Some(UDP_CHUNK_SIZE))
.map(|x| encode_udp_chunk(&server_address, x));
while let Some(chunk) = content.next().await {
io.write_all(&chunk).await.unwrap();
}
let mut ack = [0; UDP_CHUNK_SIZE];
assert_eq!(
io.read(&mut ack).await.unwrap(),
MANGLED_UDP_HEADER_LENGTH + 1
);
};
futures::pin_mut!(exchange);
match future::select(conn_driver, exchange).await {
future::Either::Left((r, exchange)) => {
info!("HTTP connection closed with result: {:?}", r);
exchange.await
}
future::Either::Right(_) => (),
}
};
tokio::select! {
_ = common::run_endpoint(&endpoint_address) => unreachable!(),
_ = client_task => (),
_ = tokio::time::sleep(Duration::from_secs(10)) => panic!("Timed out"),
}
}
#[tokio::test]
async fn h3_tcp_download() {
common::set_up_logger();
let endpoint_address = common::make_endpoint_address();
let client_task = async {
let server_address = run_tcp_server(true);
tokio::time::sleep(Duration::from_secs(1)).await;
let mut conn =
common::Http3Session::connect(&endpoint_address, common::MAIN_DOMAIN_NAME, None).await;
let (response, _) = conn
.exchange(
Request::connect(server_address.to_string())
.body(hyper::Body::empty())
.unwrap(),
)
.await;
assert_eq!(response.status, http::StatusCode::OK);
let mut total = 0;
let mut buf = [0; 64 * 1024];
while total < TCP_CONTENT_SIZE {
match conn.recv(&mut buf).await {
0 => break,
n => total += n,
}
}
assert_eq!(total, TCP_CONTENT_SIZE);
};
tokio::select! {
_ = common::run_endpoint(&endpoint_address) => unreachable!(),
_ = client_task => (),
_ = tokio::time::sleep(Duration::from_secs(10)) => panic!("Timed out"),
}
}
#[tokio::test]
async fn h3_tcp_upload() {
common::set_up_logger();
let endpoint_address = common::make_endpoint_address();
let client_task = async {
let server_address = run_tcp_server(false);
tokio::time::sleep(Duration::from_secs(1)).await;
let mut conn =
common::Http3Session::connect(&endpoint_address, common::MAIN_DOMAIN_NAME, None).await;
let (response, _) = conn
.exchange(
Request::connect(server_address.to_string())
.body(hyper::Body::empty())
.unwrap(),
)
.await;
assert_eq!(response.status, http::StatusCode::OK);
conn.send(common::make_stream_of_chunks(TCP_CONTENT_SIZE, None))
.await;
let mut ack = [0; 1];
assert_eq!(conn.recv(&mut ack).await, 1);
};
tokio::select! {
_ = common::run_endpoint(&endpoint_address) => unreachable!(),
_ = client_task => (),
_ = tokio::time::sleep(Duration::from_secs(10)) => panic!("Timed out"),
}
}
#[tokio::test]
async fn h3_udp_download() {
common::set_up_logger();
let endpoint_address = common::make_endpoint_address();
let client_task = async {
let server_address = run_udp_server(true);
tokio::time::sleep(Duration::from_secs(1)).await;
let mut conn =
common::Http3Session::connect(&endpoint_address, common::MAIN_DOMAIN_NAME, None).await;
let (response, _) = conn
.exchange(
Request::connect("_udp2")
.body(hyper::Body::empty())
.unwrap(),
)
.await;
assert_eq!(response.status, http::StatusCode::OK);
let hole_puncher = encode_udp_chunk(&server_address, &[1]);
conn.send(futures::stream::iter(std::iter::once(hole_puncher)))
.await;
let mut total = 0;
let mut buf = [0; 64 * 1024];
while total < EXPECTED_MANGLED_UDP_LENGTH {
match conn.recv(&mut buf).await {
0 => break,
n => total += n,
}
}
assert_eq!(total, EXPECTED_MANGLED_UDP_LENGTH);
};
tokio::select! {
_ = common::run_endpoint(&endpoint_address) => unreachable!(),
_ = client_task => (),
_ = tokio::time::sleep(Duration::from_secs(10)) => panic!("Timed out"),
}
}
#[tokio::test]
async fn h3_udp_upload() {
common::set_up_logger();
let endpoint_address = common::make_endpoint_address();
let client_task = async {
let server_address = run_udp_server(false);
tokio::time::sleep(Duration::from_secs(1)).await;
let mut conn =
common::Http3Session::connect(&endpoint_address, common::MAIN_DOMAIN_NAME, None).await;
let (response, _) = conn
.exchange(
Request::connect("_udp2")
.body(hyper::Body::empty())
.unwrap(),
)
.await;
assert_eq!(response.status, http::StatusCode::OK);
conn.send(
common::make_stream_of_chunks(UDP_CONTENT_SIZE, Some(UDP_CHUNK_SIZE))
.map(|x| encode_udp_chunk(&server_address, x)),
)
.await;
let mut ack = [0; UDP_CHUNK_SIZE];
assert_eq!(conn.recv(&mut ack).await, MANGLED_UDP_HEADER_LENGTH + 1);
};
tokio::select! {
_ = common::run_endpoint(&endpoint_address) => unreachable!(),
_ = client_task => (),
_ = tokio::time::sleep(Duration::from_secs(10)) => panic!("Timed out"),
}
}
async fn make_h1_tunnel(
endpoint_address: SocketAddr,
server_address: String,
) -> (
Pin<Box<dyn Future<Output = ()>>>,
Pin<Box<dyn Future<Output = impl AsyncRead + AsyncWrite + Unpin + Send>>>,
) {
let stream =
common::establish_tls_connection(common::MAIN_DOMAIN_NAME, &endpoint_address, None).await;
let (mut request, conn) = hyper::client::conn::Builder::new()
.handshake(stream)
.await
.unwrap();
let conn_driver = async move { conn.await.unwrap() }.boxed();
let exchange = async move {
let rr = Request::builder()
.version(http::Version::HTTP_11)
.method(http::Method::CONNECT)
.uri(server_address)
.body(hyper::Body::empty())
.unwrap();
let response = request.send_request(rr).await.unwrap();
info!("CONNECT response: {:?}", response);
assert_eq!(response.status(), http::StatusCode::OK);
hyper::upgrade::on(response).await.unwrap()
}
.boxed();
(conn_driver, exchange)
}
async fn make_h2_tunnel(
endpoint_address: SocketAddr,
server_address: String,
) -> (
Pin<Box<dyn Future<Output = ()>>>,
Pin<Box<dyn Future<Output = impl AsyncRead + AsyncWrite + Unpin + Send>>>,
) {
let stream = common::establish_tls_connection(
common::MAIN_DOMAIN_NAME,
&endpoint_address,
Some(net_utils::HTTP2_ALPN.as_bytes()),
)
.await;
let (mut request, conn) = hyper::client::conn::Builder::new()
.http2_only(true)
.handshake(stream)
.await
.unwrap();
let conn_driver = async move { conn.await.unwrap() }.boxed();
let exchange = async move {
let rr = Request::builder()
.version(http::Version::HTTP_2)
.method(http::Method::CONNECT)
.uri(server_address)
.body(hyper::Body::empty())
.unwrap();
let response = request.send_request(rr).await.unwrap();
info!("CONNECT response: {:?}", response);
assert_eq!(response.status(), http::StatusCode::OK);
hyper::upgrade::on(response).await.unwrap()
}
.boxed();
(conn_driver, exchange)
}
fn run_tcp_server(is_download: bool) -> SocketAddr {
let server = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
let _ = server.set_nonblocking(true);
let server_addr = server.local_addr().unwrap();
thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async move {
let server = TcpListener::from_std(server).unwrap();
let (mut socket, peer) = server.accept().await.unwrap();
info!("New connection from {}", peer);
if is_download {
let mut content = common::make_stream_of_chunks(TCP_CONTENT_SIZE, None);
while let Some(chunk) = content.next().await {
socket.write_all(chunk).await.unwrap();
}
} else {
let mut total = 0;
let mut buf = [0; 64 * 1024];
while total < TCP_CONTENT_SIZE {
match socket.read(&mut buf).await.unwrap() {
0 => break,
n => total += n,
}
}
assert_eq!(total, TCP_CONTENT_SIZE);
let ack = 1_u8;
socket.write_all(&[ack]).await.unwrap();
}
socket.flush().await.unwrap();
});
});
server_addr
}
fn run_udp_server(is_download: bool) -> SocketAddr {
let server = std::net::UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
let _ = server.set_nonblocking(true);
let server_addr = server.local_addr().unwrap();
thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async move {
let server = UdpSocket::from_std(server).unwrap();
if is_download {
let mut buf = [0; UDP_CHUNK_SIZE];
let (n, peer) = server.recv_from(&mut buf).await.unwrap();
assert_eq!(n, 1);
let mut content =
common::make_stream_of_chunks(UDP_CONTENT_SIZE, Some(UDP_CHUNK_SIZE));
while let Some(chunk) = content.next().await {
server.send_to(chunk, peer).await.unwrap();
}
} else {
let mut peer = None;
let mut total = 0;
let mut buf = [0; UDP_CHUNK_SIZE];
while total < UDP_CONTENT_SIZE {
let (n, p) = server.recv_from(&mut buf).await.unwrap();
assert_eq!(*peer.get_or_insert(p), p);
total += n;
}
assert_eq!(total, UDP_CONTENT_SIZE);
let ack = 1_u8;
server.send_to(&[ack], peer.unwrap()).await.unwrap();
}
});
});
server_addr
}
fn encode_udp_chunk(destination: &SocketAddr, payload: &[u8]) -> Vec<u8> {
const APP_NAME: &str = "test";
const SOURCE_IP: Ipv4Addr = Ipv4Addr::LOCALHOST;
const SOURCE_PORT: u16 = 1234;
let mut buffer = vec![];
buffer.put_u32((2 * (16 + 2) + 1 + APP_NAME.len() + payload.len()) as u32);
buffer.put_slice(&[0; 12]);
buffer.put_slice(&SOURCE_IP.octets());
buffer.put_u16(SOURCE_PORT);
buffer.put_slice(&[0; 12]);
buffer.put_slice(&match destination.ip() {
IpAddr::V4(ip) => ip.octets(),
_ => unreachable!(),
});
buffer.put_u16(destination.port());
buffer.put_u8(APP_NAME.len() as u8);
buffer.put_slice(APP_NAME.as_bytes());
buffer.put_slice(payload);
buffer
}