checkpoint: implement wgcloak v1 foundations
This commit is contained in:
commit
313ab00765
45 files changed
+12058
No files matched your search
@@ -0,0 +1,45 @@
|
||||
#[path = "../src/control.rs"]
|
||||
mod control;
|
||||
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use tempfile::tempdir;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
#[tokio::test]
|
||||
async fn control_commands_and_socket_permissions() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("control.sock");
|
||||
let state = control::ControlState::default();
|
||||
state.set_status("ready").await;
|
||||
state.set_stats_json("{\"packets\":3}").await;
|
||||
let server = control::ControlServer::bind(&path, state.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
std::fs::metadata(&path).unwrap().permissions().mode() & 0o777,
|
||||
0o600
|
||||
);
|
||||
let task = tokio::spawn(server.run());
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
assert!(control::send_command(&path, "status")
|
||||
.await
|
||||
.unwrap()
|
||||
.contains("ready"));
|
||||
assert!(control::send_command(&path, "stats")
|
||||
.await
|
||||
.unwrap()
|
||||
.contains("packets"));
|
||||
assert!(control::send_command(&path, "reconnect")
|
||||
.await
|
||||
.unwrap()
|
||||
.contains("true"));
|
||||
assert!(control::send_command(&path, "reload")
|
||||
.await
|
||||
.unwrap()
|
||||
.contains("true"));
|
||||
assert!(control::send_command(&path, "shutdown")
|
||||
.await
|
||||
.unwrap()
|
||||
.contains("true"));
|
||||
task.await.unwrap().unwrap();
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
#[path = "../src/config.rs"]
|
||||
mod config;
|
||||
#[path = "../src/datagram.rs"]
|
||||
mod datagram;
|
||||
#[path = "../src/protocol/mod.rs"]
|
||||
mod protocol;
|
||||
#[path = "../src/stats.rs"]
|
||||
mod stats;
|
||||
#[path = "../src/transport/mod.rs"]
|
||||
mod transport;
|
||||
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use base64::Engine;
|
||||
use bytes::Bytes;
|
||||
use config::Config;
|
||||
use tokio::{net::UdpSocket, time::timeout};
|
||||
use transport::{
|
||||
direct_udp::{DirectUdpServer, DirectUdpSession},
|
||||
TransportError, TransportSession,
|
||||
};
|
||||
|
||||
const SECRET: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
|
||||
fn configs(port: u16, backend: u16) -> (config::ServerConfig, config::ClientConfig) {
|
||||
let server = Config::from_str(&format!(
|
||||
"[server]\nlisten = \"127.0.0.1\"\n[backend]\ntarget = \"127.0.0.1:{backend}\"\n[transports.direct_udp]\nport = {port}\n[[auth.clients]]\nid = \"test\"\nsecret = \"{SECRET}\"\n"
|
||||
)).unwrap().into_server().unwrap();
|
||||
let client = Config::from_str(&format!(
|
||||
"[client]\nserver = \"127.0.0.1\"\nid = \"test\"\nsecret = \"{SECRET}\"\n[transport.direct_udp]\nport = {port}\n[network]\nmax_datagram_size = 1200\n"
|
||||
)).unwrap().into_client().unwrap();
|
||||
(server, client)
|
||||
}
|
||||
|
||||
fn multi_configs(
|
||||
port: u16,
|
||||
backend: u16,
|
||||
) -> (
|
||||
config::ServerConfig,
|
||||
config::ClientConfig,
|
||||
config::ClientConfig,
|
||||
) {
|
||||
let server = Config::from_str(&format!(
|
||||
"[server]\nlisten = \"127.0.0.1\"\n[backend]\ntarget = \"127.0.0.1:{backend}\"\n[transports.direct_udp]\nport = {port}\n[[auth.clients]]\nid = \"one\"\nsecret = \"{SECRET}\"\n[[auth.clients]]\nid = \"two\"\nsecret = \"{SECRET}\"\n"
|
||||
)).unwrap().into_server().unwrap();
|
||||
let client = |id: &str| {
|
||||
Config::from_str(&format!(
|
||||
"[client]\nserver = \"127.0.0.1\"\nid = \"{id}\"\nsecret = \"{SECRET}\"\n[transport.direct_udp]\nport = {port}\n[network]\nmax_datagram_size = 1200\n"
|
||||
)).unwrap().into_client().unwrap()
|
||||
};
|
||||
(server, client("one"), client("two"))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn direct_udp_round_trip_and_auth_rejection() {
|
||||
let backend = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||
let backend_addr = backend.local_addr().unwrap();
|
||||
let port = 45781;
|
||||
let (server_config, client_config) = configs(port, backend_addr.port());
|
||||
let server = DirectUdpServer::bind(server_config).await.unwrap();
|
||||
let stats = server.stats();
|
||||
let server_task = tokio::spawn(server.run());
|
||||
let backend_task = tokio::spawn(async move {
|
||||
let mut buf = [0u8; 2048];
|
||||
let (len, peer) = backend.recv_from(&mut buf).await.unwrap();
|
||||
backend.send_to(&buf[..len], peer).await.unwrap();
|
||||
});
|
||||
let session = DirectUdpSession::connect(&client_config).await.unwrap();
|
||||
session
|
||||
.send_datagram(Bytes::from_static(b"opaque"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
timeout(Duration::from_secs(2), session.recv_datagram())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
Bytes::from_static(b"opaque")
|
||||
);
|
||||
let mut wrong = client_config.clone();
|
||||
wrong.client.secret.secret =
|
||||
Some("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_owned());
|
||||
let result = DirectUdpSession::connect(&wrong).await;
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(TransportError::AuthenticationRejected(_))
|
||||
));
|
||||
session.close().await.unwrap();
|
||||
backend_task.await.unwrap();
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
assert_eq!(stats.snapshot().expired_sessions, 0);
|
||||
server_task.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn direct_udp_enforces_payload_limit_and_expires_idle_session() {
|
||||
let backend = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||
let backend_addr = backend.local_addr().unwrap();
|
||||
let (mut server_config, client_config) = configs(45782, backend_addr.port());
|
||||
server_config.session.idle_timeout_secs = 1;
|
||||
let server = DirectUdpServer::bind(server_config).await.unwrap();
|
||||
let stats = server.stats();
|
||||
let server_task = tokio::spawn(server.run());
|
||||
let session = DirectUdpSession::connect(&client_config).await.unwrap();
|
||||
assert!(matches!(
|
||||
session.send_datagram(Bytes::from(vec![0u8; 1201])).await,
|
||||
Err(TransportError::Oversized { maximum: 1200 })
|
||||
));
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
assert_eq!(stats.snapshot().expired_sessions, 1);
|
||||
session.close().await.unwrap();
|
||||
server_task.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn concurrent_clients_use_independent_backend_sockets() {
|
||||
let backend = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||
let backend_addr = backend.local_addr().unwrap();
|
||||
let (server_config, mut client_one, mut client_two) = multi_configs(45783, backend_addr.port());
|
||||
let server = DirectUdpServer::bind(server_config).await.unwrap();
|
||||
let stats = server.stats();
|
||||
let server_task = tokio::spawn(server.run());
|
||||
client_one.transport.direct_udp.port = 45783;
|
||||
client_two.transport.direct_udp.port = 45783;
|
||||
let backend_task = tokio::spawn(async move {
|
||||
let mut peers = Vec::new();
|
||||
let mut buf = [0u8; 2048];
|
||||
for _ in 0..2 {
|
||||
let (len, peer) = backend.recv_from(&mut buf).await.unwrap();
|
||||
peers.push(peer);
|
||||
backend.send_to(&buf[..len], peer).await.unwrap();
|
||||
}
|
||||
peers
|
||||
});
|
||||
let first = DirectUdpSession::connect(&client_one).await.unwrap();
|
||||
let second = DirectUdpSession::connect(&client_two).await.unwrap();
|
||||
assert!(stats.snapshot().handshake_attempts >= 2);
|
||||
first
|
||||
.send_datagram(Bytes::from_static(b"first"))
|
||||
.await
|
||||
.unwrap();
|
||||
second
|
||||
.send_datagram(Bytes::from_static(b"second"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
timeout(Duration::from_secs(2), first.recv_datagram())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
Bytes::from_static(b"first")
|
||||
);
|
||||
assert_eq!(
|
||||
timeout(Duration::from_secs(2), second.recv_datagram())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
Bytes::from_static(b"second")
|
||||
);
|
||||
let peers = backend_task.await.unwrap();
|
||||
assert_ne!(
|
||||
peers[0], peers[1],
|
||||
"each authenticated client must have its own backend socket"
|
||||
);
|
||||
first.close().await.unwrap();
|
||||
second.close().await.unwrap();
|
||||
server_task.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn malformed_and_unauthenticated_packets_do_not_kill_server() {
|
||||
let backend = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||
let backend_addr = backend.local_addr().unwrap();
|
||||
let (server_config, client_config) = configs(45784, backend_addr.port());
|
||||
let server = DirectUdpServer::bind(server_config).await.unwrap();
|
||||
let stats = server.stats();
|
||||
let server_task = tokio::spawn(server.run());
|
||||
let raw = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||
raw.send_to(&[1, 2, 3], "127.0.0.1:45784").await.unwrap();
|
||||
raw.send_to(&[0u8; 64], "127.0.0.1:45784").await.unwrap();
|
||||
let mut wrong = client_config.clone();
|
||||
wrong.client.secret.secret = Some("B".repeat(64));
|
||||
assert!(matches!(
|
||||
DirectUdpSession::connect(&wrong).await,
|
||||
Err(TransportError::AuthenticationRejected(_))
|
||||
));
|
||||
let backend_task = tokio::spawn(async move {
|
||||
let mut buf = [0u8; 2048];
|
||||
let (len, peer) = backend.recv_from(&mut buf).await.unwrap();
|
||||
backend.send_to(&buf[..len], peer).await.unwrap();
|
||||
});
|
||||
let session = DirectUdpSession::connect(&client_config).await.unwrap();
|
||||
session
|
||||
.send_datagram(Bytes::from_static(b"still-alive"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
timeout(Duration::from_secs(2), session.recv_datagram())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
Bytes::from_static(b"still-alive")
|
||||
);
|
||||
assert!(stats.snapshot().datagrams_dropped >= 3);
|
||||
session.close().await.unwrap();
|
||||
backend_task.await.unwrap();
|
||||
server_task.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authenticated_source_port_change_updates_return_peer() {
|
||||
let backend = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||
let backend_addr = backend.local_addr().unwrap();
|
||||
let (server_config, _) = configs(45785, backend_addr.port());
|
||||
let server = DirectUdpServer::bind(server_config).await.unwrap();
|
||||
let server_task = tokio::spawn(server.run());
|
||||
let first = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||
let second = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||
let secret = base64::engine::general_purpose::STANDARD
|
||||
.decode(SECRET)
|
||||
.unwrap();
|
||||
let mut nonce = [0u8; 16];
|
||||
nonce[0] = 1;
|
||||
let hello = protocol::HelloPacket::authenticated(
|
||||
&secret,
|
||||
Bytes::from_static(b"test"),
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
nonce,
|
||||
)
|
||||
.unwrap();
|
||||
first
|
||||
.send_to(&hello.encode().unwrap(), "127.0.0.1:45785")
|
||||
.await
|
||||
.unwrap();
|
||||
let mut wire = [0u8; 2048];
|
||||
let (len, _) = timeout(Duration::from_secs(2), first.recv_from(&mut wire))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let accept = match protocol::parse_packet(&wire[..len]).unwrap() {
|
||||
protocol::Packet::Accept(packet) => packet,
|
||||
other => panic!("unexpected handshake response: {other:?}"),
|
||||
};
|
||||
let key =
|
||||
protocol::derive_session_key(&secret, &nonce, &accept.server_nonce, accept.session_id);
|
||||
let data = protocol::DataPacket::authenticated(
|
||||
&key,
|
||||
accept.session_id,
|
||||
1,
|
||||
Bytes::from_static(b"rebound"),
|
||||
);
|
||||
second
|
||||
.send_to(&data.encode().unwrap(), "127.0.0.1:45785")
|
||||
.await
|
||||
.unwrap();
|
||||
let backend_task = tokio::spawn(async move {
|
||||
let mut buf = [0u8; 2048];
|
||||
let (len, peer) = backend.recv_from(&mut buf).await.unwrap();
|
||||
backend.send_to(&buf[..len], peer).await.unwrap();
|
||||
});
|
||||
let (len, _) = timeout(Duration::from_secs(2), second.recv_from(&mut wire))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
match protocol::parse_packet(&wire[..len]).unwrap() {
|
||||
protocol::Packet::Data(packet) => {
|
||||
assert_eq!(packet.payload, Bytes::from_static(b"rebound"))
|
||||
}
|
||||
other => panic!("unexpected rebound response: {other:?}"),
|
||||
}
|
||||
backend_task.await.unwrap();
|
||||
server_task.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unavailable_backend_does_not_terminate_listener() {
|
||||
let (server_config, client_config) = configs(45786, 9);
|
||||
let server = DirectUdpServer::bind(server_config).await.unwrap();
|
||||
let server_task = tokio::spawn(server.run());
|
||||
let session = DirectUdpSession::connect(&client_config).await.unwrap();
|
||||
session
|
||||
.send_datagram(Bytes::from_static(b"backend-may-be-down"))
|
||||
.await
|
||||
.unwrap();
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
assert!(
|
||||
!server_task.is_finished(),
|
||||
"backend failure must not terminate the public listener"
|
||||
);
|
||||
session.close().await.unwrap();
|
||||
server_task.abort();
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
#[path = "../src/transport/http3.rs"]
|
||||
mod http3;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use http3::{
|
||||
decode_capsule, decode_http_datagram, encode_auth_challenge, encode_auth_response,
|
||||
encode_capsule, encode_http_datagram, validate_connect_udp_request, AuthChallenge,
|
||||
ConnectUdpRequest, Http3Error, CONNECT_UDP_CONTEXT_ID,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn validates_extended_connect_request() {
|
||||
validate_connect_udp_request("CONNECT", "connect-udp", "/tunnel", "/tunnel").unwrap();
|
||||
ConnectUdpRequest::new("/tunnel")
|
||||
.validate("/tunnel")
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
validate_connect_udp_request("POST", "connect-udp", "/tunnel", "/tunnel"),
|
||||
Err(Http3Error::InvalidRequest("method must be CONNECT"))
|
||||
));
|
||||
assert!(matches!(
|
||||
validate_connect_udp_request("CONNECT", "webtransport", "/tunnel", "/tunnel"),
|
||||
Err(Http3Error::InvalidRequest(":protocol must be connect-udp"))
|
||||
));
|
||||
assert!(validate_connect_udp_request("CONNECT", "connect-udp", "/other", "/tunnel").is_err());
|
||||
assert!(validate_connect_udp_request("CONNECT", "connect-udp", "tunnel", "tunnel").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_datagram_uses_context_id_and_preserves_opaque_payload() {
|
||||
for context in [0, 63, 64, 16_383, 16_384, (1 << 30) - 1, 1 << 30] {
|
||||
let encoded = encode_http_datagram(context, b"opaque", 1200).unwrap();
|
||||
assert_eq!(
|
||||
decode_http_datagram(&encoded, context, 1200).unwrap(),
|
||||
b"opaque"[..]
|
||||
);
|
||||
}
|
||||
assert!(matches!(
|
||||
decode_http_datagram(
|
||||
&encode_http_datagram(7, b"x", 1200).unwrap(),
|
||||
CONNECT_UDP_CONTEXT_ID,
|
||||
1200
|
||||
),
|
||||
Err(Http3Error::ContextMismatch { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
encode_http_datagram(CONNECT_UDP_CONTEXT_ID, &[0; 1201], 1200),
|
||||
Err(Http3Error::Oversized {
|
||||
length: 1201,
|
||||
maximum: 1200
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capsule_round_trip_and_truncation_are_checked() {
|
||||
let encoded = encode_capsule(0x2a, b"challenge").unwrap();
|
||||
assert_eq!(
|
||||
decode_capsule(&encoded).unwrap(),
|
||||
(0x2a, bytes::Bytes::from_static(b"challenge"))
|
||||
);
|
||||
assert!(matches!(
|
||||
decode_capsule(&encoded[..encoded.len() - 1]),
|
||||
Err(Http3Error::Truncated)
|
||||
));
|
||||
let mut extra = encoded.to_vec();
|
||||
extra.push(0);
|
||||
assert!(matches!(
|
||||
decode_capsule(&extra),
|
||||
Err(Http3Error::InvalidCapsule)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_is_completed_before_session_activation() {
|
||||
let challenge = AuthChallenge::random();
|
||||
let nonce = [5u8; 16];
|
||||
let secret = [7u8; 32];
|
||||
let challenge_capsule = encode_auth_challenge(&challenge).unwrap();
|
||||
assert_eq!(
|
||||
decode_capsule(&challenge_capsule).unwrap().0,
|
||||
http3::AUTH_CHALLENGE_CAPSULE
|
||||
);
|
||||
let response = encode_auth_response(&challenge, "laptop", &nonce, &secret).unwrap();
|
||||
let mut clients = HashMap::new();
|
||||
clients.insert("laptop".to_owned(), secret.to_vec());
|
||||
let client = http3::verify_auth_response(&challenge, &response, &clients).unwrap();
|
||||
assert_eq!(client.id, "laptop");
|
||||
assert_eq!(client.nonce, nonce);
|
||||
|
||||
let bad = encode_auth_response(&challenge, "laptop", &nonce, &[8u8; 32]).unwrap();
|
||||
assert!(http3::verify_auth_response(&challenge, &bad, &clients).is_err());
|
||||
assert!(http3::is_auth_accept(&http3::encode_auth_accept().unwrap()).unwrap());
|
||||
assert!(!http3::is_auth_accept(&http3::encode_auth_reject().unwrap()).unwrap());
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
#[path = "../src/config.rs"]
|
||||
mod config;
|
||||
#[path = "../src/datagram.rs"]
|
||||
mod datagram;
|
||||
#[path = "../src/protocol/mod.rs"]
|
||||
mod protocol;
|
||||
#[path = "../src/stats.rs"]
|
||||
mod stats;
|
||||
#[path = "../src/transport/mod.rs"]
|
||||
mod transport;
|
||||
|
||||
use std::{process::Command, sync::Arc, time::Duration};
|
||||
|
||||
use bytes::Bytes;
|
||||
use config::Config;
|
||||
use rustls::RootCertStore;
|
||||
use tempfile::tempdir;
|
||||
use tokio::time::timeout;
|
||||
use transport::{TlsTcpConnector, TlsTcpServer, TransportConnector, TransportSession};
|
||||
|
||||
const SECRET: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
|
||||
fn free_port() -> u16 {
|
||||
std::net::TcpListener::bind("127.0.0.1:0")
|
||||
.unwrap()
|
||||
.local_addr()
|
||||
.unwrap()
|
||||
.port()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tls_tcp_authenticates_and_round_trips_opaque_datagrams() {
|
||||
if Command::new("openssl").arg("version").output().is_err() {
|
||||
return;
|
||||
}
|
||||
let dir = tempdir().unwrap();
|
||||
let cert = dir.path().join("cert.pem");
|
||||
let key = dir.path().join("key.pem");
|
||||
let generated = Command::new("openssl")
|
||||
.args([
|
||||
"req",
|
||||
"-x509",
|
||||
"-newkey",
|
||||
"rsa:2048",
|
||||
"-nodes",
|
||||
"-subj",
|
||||
"/CN=localhost",
|
||||
"-days",
|
||||
"1",
|
||||
"-keyout",
|
||||
])
|
||||
.arg(&key)
|
||||
.args(["-out"])
|
||||
.arg(&cert)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
generated.status.success(),
|
||||
"openssl failed: {:?}",
|
||||
generated
|
||||
);
|
||||
let port = free_port();
|
||||
let server_config = Config::from_str(&format!(
|
||||
"[server]\nlisten = \"127.0.0.1\"\n[backend]\ntarget = \"127.0.0.1:51820\"\n[transports.direct_udp]\nenabled = false\n[transports.quic]\nenabled = false\n[transports.http3]\nenabled = false\n[transports.tls_tcp]\nenabled = true\nport = {port}\n[tls]\ncertificate = \"{}\"\nprivate_key = \"{}\"\n[[auth.clients]]\nid = \"test\"\nsecret = \"{SECRET}\"\n",
|
||||
cert.display(),
|
||||
key.display()
|
||||
))
|
||||
.unwrap()
|
||||
.into_server()
|
||||
.unwrap();
|
||||
let client_config = Config::from_str(&format!(
|
||||
"[client]\nserver = \"localhost\"\nid = \"test\"\nsecret = \"{SECRET}\"\n[transport]\nmode = \"tls_tcp\"\n[transport.tls_tcp]\nport = {port}\n[network]\nmax_datagram_size = 1200\n"
|
||||
))
|
||||
.unwrap()
|
||||
.into_client()
|
||||
.unwrap();
|
||||
let certs = transport::tls_tcp::load_certificates(&cert).unwrap();
|
||||
let mut roots = RootCertStore::empty();
|
||||
roots.add(certs[0].clone()).unwrap();
|
||||
let client_tls = (*transport::tls_tcp::client_tls_config(roots)).clone();
|
||||
let server = Arc::new(TlsTcpServer::bind(&server_config).await.unwrap());
|
||||
let accept = {
|
||||
let server = server.clone();
|
||||
tokio::spawn(async move { server.accept_session().await })
|
||||
};
|
||||
let client = TlsTcpConnector::with_client_config(client_config, client_tls);
|
||||
let client_session = client.connect().await.unwrap();
|
||||
let server_session = accept.await.unwrap().unwrap();
|
||||
client_session
|
||||
.send_datagram(Bytes::from_static(b"tls-opaque"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
timeout(Duration::from_secs(2), server_session.recv_datagram())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
Bytes::from_static(b"tls-opaque")
|
||||
);
|
||||
client_session.close().await.unwrap();
|
||||
server_session.close().await.unwrap();
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
#[path = "../src/config.rs"]
|
||||
mod config;
|
||||
#[path = "../src/datagram.rs"]
|
||||
mod datagram;
|
||||
#[path = "../src/protocol/mod.rs"]
|
||||
mod protocol;
|
||||
#[path = "../src/stats.rs"]
|
||||
mod stats;
|
||||
#[path = "../src/transport/mod.rs"]
|
||||
mod transport;
|
||||
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
|
||||
use base64::Engine;
|
||||
use bytes::Bytes;
|
||||
use quinn::{ClientConfig as QuinnClientConfig, Endpoint, ServerConfig as QuinnServerConfig};
|
||||
use rustls::pki_types::{CertificateDer, PrivatePkcs8KeyDer};
|
||||
use tokio::time::timeout;
|
||||
use transport::{
|
||||
quic::{QuicClientConnector, QuicServer},
|
||||
TransportError, TransportSession,
|
||||
};
|
||||
|
||||
const SECRET: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
|
||||
// A test-only localhost certificate, valid for ten years. Keeping the DER
|
||||
// here avoids adding a certificate-generation crate to the application.
|
||||
const CERT_DER_B64: &str = "MIIDHDCCAgSgAwIBAgIUJnI/VlHMWu6j0dXr/t1OmMQrLgowDQYJKoZIhvcNAQELBQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDkxNDIwNDEyMFoXDTM2MDkxMTIwNDEyMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm4d4r5Py4GdfQrLit2oytLv20L+y7oM/VTDzzJfFMPmewOh0XyS0BI4lyRbCgOuUh686UPvF/BU8uJq24OO7wt/2CrE3FfwYX7BDjIWMuFevHuv896imabbX4IJ7lzVugI3862QSW09QSEQk3ggYs3z/F16cnSRZm9aIURgdPQsLmMzOwcPtDGwxp2jf0Rky0kFRxhUnqfIWko4hny7GjWL8VG0WzYJY6aUevjPlN/Oq/akr9xUAr2QJenEesP77do4X31LIZmsvB6GqKwaSQ0AbwNHW7rDiaE4dkZMIk5l95YgYnrzyOC8IhUwaSZt0wODxk/euXPYr+RFJv2FuOQIDAQABo2YwZDAdBgNVHQ4EFgQUhViBEXLkp7YUVZaWzLi2SfRmfmYwHwYDVR0jBBgwFoAUhViBEXLkp7YUVZaWzLi2SfRmfmYwFAYDVR0RBA0wC4IJbG9jYWxob3N0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggEBAEFC7da0bK78IUL0k6lZ82pqFImX2bNteMh/r1IJG0xqB/O2dOLUDHybpF7HToommALgRu6zhlT0d1eHg0cMYEgrKyq4BAAXGHqOt3OdKKL5nAWmI04dlWsrG9aTU3hTQhe0++hnTbVm+YJnYAp+ufOxx858FeothjLWu1Lr81Nm01FnmZbXcFno3nosyl1GPxSRVZl2XH9NsfMSZJUbSU6mHXTO1pEH6mjYmgD362CeJc+0DOzevdLsDozxUoWmdEuqlldauESW6XVK0WxRSJ1i2/B/mtYViHr5R/j5NYgG5STnNnwaNwjYK+0AhxvTtH3jAp0Bsluw4d+xKdsV/YI=";
|
||||
const KEY_DER_B64: &str = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCbh3ivk/LgZ19CsuK3ajK0u/bQv7Lugz9VMPPMl8Uw+Z7A6HRfJLQEjiXJFsKA65SHrzpQ+8X8FTy4mrbg47vC3/YKsTcV/BhfsEOMhYy4V68e6/z3qKZpttfggnuXNW6AjfzrZBJbT1BIRCTeCBizfP8XXpydJFmb1ohRGB09CwuYzM7Bw+0MbDGnaN/RGTLSQVHGFSep8haSjiGfLsaNYvxUbRbNgljppR6+M+U386r9qSv3FQCvZAl6cR6w/vt2jhffUshmay8HoaorBpJDQBvA0dbusOJoTh2RkwiTmX3liBievPI4LwiFTBpJm3TA4PGT965c9iv5EUm/YW45AgMBAAECggEAMs0EtCD4t6i8Ol6Q5EUZa+RrgZLkILkZEjwLnjvCqrUWUD99Z2hnLoe5ut2myZJpcQpg23vpluXe8BOFAZ8RQN2DkcmByzoNQhAieD9VEA+AAXMmrURWmvggulIUiXbn8bCvOLn2rwGQoRI1EpsFanc9IUaDlGZ/qIHiA88DGeFDuchbcgClpXAX4lBzqhLKv/CCIhhGxmyWc1RPlZUef7uTO1Zh7q2HxnO5iBKobvPfMq5VfvzE4rXeSQIHtW+yJFLU2sNjaYry2JTLEqGIZhWMROmzlkW8x/cj0GvjLbY4ltkIpLNSb+WRbY3zZNhQ81kosBZtiYRj3vgr/FzvSwKBgQDQ/dxghryGDqIIOfyZ59+2tpPrrLX4vy7tmAQg6zJIiQVHMZ/Av60QvY7lA0dvDfQmfJuNzTOXtwhgnYkHbg6jlFqpT7P1CZQOzbF0UuFCNvwzE/jgCEKUExckDa+Du7g/VOglB9B4XKqJnQLpZkzz4Hwmuvz4oUtZJCl796HTMwKBgQC+gyPKrNp6AthxZ1gAu+aIjf18vgGZjANQx3cRFQl5BqSyTwZpxIfuiKrT+sU65N2VCWpGH6oTzGruaXBXzG0rlNCMAL5XaJqJehvLAnejUqmXhyi8ouH174Q4azDngPOCFaSjuYJwpX4nUXwMExqs9hwkH9pV3tn6qzEpKJA44wKBgBa+SalHgGSmWwPCK7ckALiFjftrxDHZvGWm0WwzrDkwkB57TANR2KeluVM61B9FyXgWv2bjGeY8yPCatDrWw0kR7/vU7KSR8kbaI6GhE1p9WVzFo6PdwkETAsWtp5gSaa6asti0zP8MNpLbP2He/CjeGSSt3LHB+80rnkgbXsT5AoGAVaTytCxc0zjwQJrqzvmpu33JlQh+A58T5n6lIkLIzWWVKKuqAGZfjXZXBZDYlY9JpbRj2D5n4Rqmf6Na47183ni6ABZde0IFJwSD9hGX4w5IO1yBjbvTxRH16L8XbxtjXPmJ2CiPYUCko9xX3b3A4jyJjhvbtXrHl47aPNyFaW0CgYEAxDOcW5r5pXXkCF/mIqSklGa1jDO9TeMavLo1AguHaUlJEQU8DgGM9Flx3senFASN2EjwwTB3aScVuxe3X29LeZnCNsuTIzCVGpSNyFLUjZVU0fRaf6R9JIcI7dVoflPAPCdrqBVkezK5Eyu8f3NCwnVD+zv+iWA+RE1zUyKy4sM=";
|
||||
|
||||
fn endpoint_and_client(
|
||||
max_datagram: usize,
|
||||
) -> (Endpoint, QuinnClientConfig, CertificateDer<'static>) {
|
||||
let cert_bytes = base64::engine::general_purpose::STANDARD
|
||||
.decode(CERT_DER_B64)
|
||||
.unwrap();
|
||||
let key_bytes = base64::engine::general_purpose::STANDARD
|
||||
.decode(KEY_DER_B64)
|
||||
.unwrap();
|
||||
let cert = CertificateDer::from(cert_bytes);
|
||||
let key = PrivatePkcs8KeyDer::from(key_bytes);
|
||||
let mut server_config =
|
||||
QuinnServerConfig::with_single_cert(vec![cert.clone()], key.into()).unwrap();
|
||||
let mut transport_config = quinn::TransportConfig::default();
|
||||
transport_config.keep_alive_interval(Some(Duration::from_secs(1)));
|
||||
transport_config.datagram_send_buffer_size(max_datagram * 8);
|
||||
server_config.transport_config(Arc::new(transport_config));
|
||||
let endpoint = Endpoint::server(server_config, "127.0.0.1:0".parse().unwrap()).unwrap();
|
||||
let mut roots = rustls::RootCertStore::empty();
|
||||
roots.add(cert.clone()).unwrap();
|
||||
let client = QuinnClientConfig::with_root_certificates(Arc::new(roots)).unwrap();
|
||||
(endpoint, client, cert)
|
||||
}
|
||||
|
||||
fn client_config(port: u16, secret: &str) -> config::ClientConfig {
|
||||
config::Config::from_str(&format!(
|
||||
"[client]\nserver = \"localhost\"\nid = \"test\"\nsecret = \"{secret}\"\n[transport.quic]\nport = {port}\n[transport.auto]\nprobe_timeout_ms = 2000\n[network]\nmax_datagram_size = 1200\n"
|
||||
))
|
||||
.unwrap()
|
||||
.into_client()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authenticated_datagram_round_trip_and_limit() {
|
||||
let (endpoint, client_tls, _cert) = endpoint_and_client(1200);
|
||||
let mut clients = HashMap::new();
|
||||
clients.insert(
|
||||
"test".to_owned(),
|
||||
base64::engine::general_purpose::STANDARD
|
||||
.decode(SECRET)
|
||||
.unwrap(),
|
||||
);
|
||||
let server = QuicServer::from_endpoint(endpoint, clients, 1200, Duration::from_secs(2));
|
||||
let port = server.local_addr().unwrap().port();
|
||||
let client = QuicClientConnector::with_client_config(client_config(port, SECRET), client_tls);
|
||||
let server_task = tokio::spawn(async move { server.accept().await });
|
||||
let client_session = client.connect_session().await.unwrap();
|
||||
let server_session = server_task.await.unwrap().unwrap();
|
||||
client_session
|
||||
.send_datagram(Bytes::from_static(b"opaque"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
timeout(Duration::from_secs(2), server_session.recv_datagram())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
Bytes::from_static(b"opaque")
|
||||
);
|
||||
assert!(matches!(
|
||||
client_session
|
||||
.send_datagram(Bytes::from(vec![0; 1201]))
|
||||
.await,
|
||||
Err(TransportError::Oversized { maximum: 1200 })
|
||||
));
|
||||
client_session.close().await.unwrap();
|
||||
server_session.close().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bad_secret_is_rejected_as_authentication_error() {
|
||||
let (endpoint, client_tls, _cert) = endpoint_and_client(1200);
|
||||
let mut clients = HashMap::new();
|
||||
clients.insert(
|
||||
"test".to_owned(),
|
||||
base64::engine::general_purpose::STANDARD
|
||||
.decode(SECRET)
|
||||
.unwrap(),
|
||||
);
|
||||
let server = QuicServer::from_endpoint(endpoint, clients, 1200, Duration::from_secs(2));
|
||||
let port = server.local_addr().unwrap().port();
|
||||
let wrong = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||
let client = QuicClientConnector::with_client_config(client_config(port, wrong), client_tls);
|
||||
let server_task = tokio::spawn(async move { server.accept().await });
|
||||
let result = client.connect_session().await;
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(TransportError::AuthenticationRejected(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
server_task.await.unwrap(),
|
||||
Err(TransportError::AuthenticationRejected(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn certificate_hostname_mismatch_is_not_accepted() {
|
||||
let (endpoint, client_tls, _cert) = endpoint_and_client(1200);
|
||||
let mut clients = HashMap::new();
|
||||
clients.insert(
|
||||
"test".to_owned(),
|
||||
base64::engine::general_purpose::STANDARD
|
||||
.decode(SECRET)
|
||||
.unwrap(),
|
||||
);
|
||||
let server = QuicServer::from_endpoint(endpoint, clients, 1200, Duration::from_secs(2));
|
||||
let port = server.local_addr().unwrap().port();
|
||||
let server_task = tokio::spawn(async move { server.accept().await });
|
||||
// The connector's configured hostname is localhost, which matches. This
|
||||
// test changes it to an unrelated name while retaining the localhost root.
|
||||
let mut mismatch = client_config(port, SECRET);
|
||||
mismatch.client.server = "not-localhost".to_owned();
|
||||
let mismatch = QuicClientConnector::with_client_config(mismatch, client_tls);
|
||||
let result = mismatch.connect_session().await;
|
||||
assert!(result.is_err());
|
||||
server_task.abort();
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
#[path = "../src/config.rs"]
|
||||
mod config;
|
||||
#[path = "../src/datagram.rs"]
|
||||
mod datagram;
|
||||
#[path = "../src/protocol/mod.rs"]
|
||||
mod protocol;
|
||||
#[path = "../src/stats.rs"]
|
||||
mod stats;
|
||||
#[path = "../src/client/supervisor.rs"]
|
||||
mod supervisor;
|
||||
#[path = "../src/transport/mod.rs"]
|
||||
mod transport;
|
||||
|
||||
use bytes::Bytes;
|
||||
use config::{Config, TransportMode};
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use supervisor::{ProbeFuture, TransportProbe, TransportSupervisor};
|
||||
use tokio::{sync::Mutex, time::timeout};
|
||||
use transport::{TransportError, TransportFuture, TransportSession};
|
||||
|
||||
struct Session;
|
||||
impl TransportSession for Session {
|
||||
fn send_datagram<'a>(&'a self, _: Bytes) -> TransportFuture<'a, ()> {
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
fn recv_datagram<'a>(&'a self) -> TransportFuture<'a, Bytes> {
|
||||
Box::pin(async { Err(TransportError::Closed) })
|
||||
}
|
||||
fn close<'a>(&'a self) -> TransportFuture<'a, ()> {
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
fn max_datagram_size(&self) -> usize {
|
||||
1200
|
||||
}
|
||||
}
|
||||
struct Probe {
|
||||
calls: Arc<Mutex<Vec<TransportMode>>>,
|
||||
auth: bool,
|
||||
}
|
||||
impl TransportProbe for Probe {
|
||||
fn connect(&self, mode: TransportMode) -> ProbeFuture {
|
||||
let calls = self.calls.clone();
|
||||
let auth = self.auth;
|
||||
Box::pin(async move {
|
||||
calls.lock().await.push(mode);
|
||||
if auth {
|
||||
Err(TransportError::AuthenticationRejected(
|
||||
protocol::RejectReason::AuthenticationFailed,
|
||||
))
|
||||
} else {
|
||||
Err(TransportError::HandshakeTimeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
fn config(mode: &str) -> config::ClientConfig {
|
||||
Config::from_str(&format!("[client]\nserver=\"127.0.0.1\"\nid=\"c\"\nsecret=\"{}\"\n[transport]\nmode=\"{}\"\n[transport.auto]\norder=[\"direct_udp\",\"quic\"]\nretries=2\nprobe_timeout_ms=20\n", "A".repeat(64), mode)).unwrap().into_client().unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn selection_order_retries_and_auth_stop() {
|
||||
let calls = Arc::new(Mutex::new(Vec::new()));
|
||||
let supervisor = TransportSupervisor::new(
|
||||
config("auto"),
|
||||
Probe {
|
||||
calls: calls.clone(),
|
||||
auth: false,
|
||||
},
|
||||
);
|
||||
assert!(matches!(
|
||||
supervisor.connect_once().await,
|
||||
Err(TransportError::HandshakeTimeout)
|
||||
));
|
||||
assert_eq!(
|
||||
*calls.lock().await,
|
||||
vec![
|
||||
TransportMode::DirectUdp,
|
||||
TransportMode::DirectUdp,
|
||||
TransportMode::Quic,
|
||||
TransportMode::Quic
|
||||
]
|
||||
);
|
||||
let calls = Arc::new(Mutex::new(Vec::new()));
|
||||
let supervisor = TransportSupervisor::new(
|
||||
config("auto"),
|
||||
Probe {
|
||||
calls: calls.clone(),
|
||||
auth: true,
|
||||
},
|
||||
);
|
||||
assert!(matches!(
|
||||
supervisor.connect_once().await,
|
||||
Err(TransportError::AuthenticationRejected(_))
|
||||
));
|
||||
assert_eq!(*calls.lock().await, vec![TransportMode::DirectUdp]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backoff_is_bounded_and_resettable() {
|
||||
let mut supervisor = TransportSupervisor::new(
|
||||
config("direct_udp"),
|
||||
Probe {
|
||||
calls: Arc::new(Mutex::new(Vec::new())),
|
||||
auth: false,
|
||||
},
|
||||
);
|
||||
for _ in 0..30 {
|
||||
let delay = supervisor.backoff_delay();
|
||||
assert!((250..=30_000).contains(&delay.as_millis()));
|
||||
}
|
||||
supervisor.reset_backoff();
|
||||
assert_eq!(supervisor.backoff_delay(), Duration::from_millis(250));
|
||||
}
|
||||
|
||||
struct LifecycleProbe {
|
||||
calls: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl TransportProbe for LifecycleProbe {
|
||||
fn connect(&self, _mode: TransportMode) -> ProbeFuture {
|
||||
let calls = self.calls.clone();
|
||||
Box::pin(async move {
|
||||
if calls.fetch_add(1, Ordering::SeqCst) == 0 {
|
||||
Ok(Box::new(Session) as Box<dyn TransportSession>)
|
||||
} else {
|
||||
Err(TransportError::HandshakeTimeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn wait_for_session_failure<'a>(
|
||||
&'a self,
|
||||
_session: &'a dyn TransportSession,
|
||||
) -> supervisor::SessionWatchFuture<'a> {
|
||||
Box::pin(async { Err(TransportError::Closed) })
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn established_failure_reprobes_without_reading_datagrams() {
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let supervisor = TransportSupervisor::new(
|
||||
config("direct_udp"),
|
||||
LifecycleProbe {
|
||||
calls: calls.clone(),
|
||||
},
|
||||
);
|
||||
let shutdown = supervisor.shutdown_token();
|
||||
let task = tokio::spawn(supervisor.run());
|
||||
// The failed established session uses the normal minimum reconnect
|
||||
// backoff before the second probe.
|
||||
tokio::time::sleep(Duration::from_millis(320)).await;
|
||||
shutdown.cancel();
|
||||
let result = timeout(Duration::from_secs(2), task)
|
||||
.await
|
||||
.expect("supervisor should stop")
|
||||
.unwrap();
|
||||
assert!(result.is_ok());
|
||||
assert!(calls.load(Ordering::SeqCst) >= 2);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
#[path = "../src/transport/tls_tcp.rs"]
|
||||
mod tls_tcp;
|
||||
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
use tls_tcp::{
|
||||
client_authenticate, server_authenticate, AuthChallenge, FrameCodec, FrameError, FramedReader,
|
||||
FramedWriter,
|
||||
};
|
||||
use tokio::io::{duplex, split};
|
||||
|
||||
#[test]
|
||||
fn codec_handles_split_and_coalesced_frames() {
|
||||
let codec = FrameCodec::new(32).expect("valid maximum");
|
||||
let mut wire = codec.encode(b"first").unwrap();
|
||||
wire.extend_from_slice(&codec.encode(b"second").unwrap());
|
||||
let mut decoder = FrameCodec::new(32).unwrap();
|
||||
assert!(decoder.feed(&wire[..1]).unwrap().is_empty());
|
||||
assert!(decoder.feed(&wire[1..4]).unwrap().is_empty());
|
||||
assert_eq!(
|
||||
decoder.feed(&wire[4..]).unwrap(),
|
||||
vec![b"first".to_vec(), b"second".to_vec()]
|
||||
);
|
||||
decoder.finish().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_rejects_oversized_before_payload() {
|
||||
let mut decoder = FrameCodec::new(4).unwrap();
|
||||
let error = decoder.feed(&[0, 5]).unwrap_err();
|
||||
assert_eq!(
|
||||
error,
|
||||
FrameError::Oversized {
|
||||
length: 5,
|
||||
maximum: 4
|
||||
}
|
||||
);
|
||||
assert!(decoder.finish().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_reports_truncated_header_or_payload() {
|
||||
let mut decoder = FrameCodec::new(32).unwrap();
|
||||
decoder.feed(&[0]).unwrap();
|
||||
assert_eq!(decoder.finish(), Err(FrameError::Truncated));
|
||||
let mut decoder = FrameCodec::new(32).unwrap();
|
||||
decoder.feed(&[0, 3, b'x']).unwrap();
|
||||
assert_eq!(decoder.finish(), Err(FrameError::Truncated));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_response_is_bound_to_challenge_and_secret() {
|
||||
let challenge = AuthChallenge::random();
|
||||
let nonce = [9u8; 16];
|
||||
let message = tls_tcp::encode_auth_response(&challenge, "laptop", &nonce, &[7u8; 32]).unwrap();
|
||||
let mut clients = HashMap::new();
|
||||
clients.insert("laptop".to_owned(), vec![7u8; 32]);
|
||||
// Exercise the full asynchronous exchange below; this assertion also
|
||||
// ensures the generated challenge is not the all-zero sentinel.
|
||||
assert_ne!(challenge.bytes, [0u8; 32]);
|
||||
assert_eq!(message[0], 0x11);
|
||||
assert_eq!(clients["laptop"].len(), 32);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tls_stream_authentication_exchange_uses_challenge_response() {
|
||||
let (left, right) = duplex(4096);
|
||||
let (mut server_reader, mut server_writer) = split(left);
|
||||
let (mut client_reader, mut client_writer) = split(right);
|
||||
let mut clients = HashMap::new();
|
||||
clients.insert("laptop".to_owned(), vec![7u8; 32]);
|
||||
let server_clients = clients.clone();
|
||||
|
||||
let server = tokio::spawn(async move {
|
||||
server_authenticate(
|
||||
&mut server_reader,
|
||||
&mut server_writer,
|
||||
&server_clients,
|
||||
1024,
|
||||
Duration::from_secs(1),
|
||||
)
|
||||
.await
|
||||
});
|
||||
let client = tokio::spawn(async move {
|
||||
client_authenticate(
|
||||
&mut client_reader,
|
||||
&mut client_writer,
|
||||
"laptop",
|
||||
&[7u8; 32],
|
||||
1024,
|
||||
Duration::from_secs(1),
|
||||
)
|
||||
.await
|
||||
});
|
||||
assert!(client.await.unwrap().is_ok());
|
||||
let authenticated = server.await.unwrap().unwrap();
|
||||
assert_eq!(authenticated.id, "laptop");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bad_secret_is_rejected_without_authenticating() {
|
||||
let (left, right) = duplex(4096);
|
||||
let (mut server_reader, mut server_writer) = split(left);
|
||||
let (mut client_reader, mut client_writer) = split(right);
|
||||
let mut clients = HashMap::new();
|
||||
clients.insert("laptop".to_owned(), vec![7u8; 32]);
|
||||
let server_clients = clients.clone();
|
||||
let server = tokio::spawn(async move {
|
||||
server_authenticate(
|
||||
&mut server_reader,
|
||||
&mut server_writer,
|
||||
&server_clients,
|
||||
1024,
|
||||
Duration::from_secs(1),
|
||||
)
|
||||
.await
|
||||
});
|
||||
let client = tokio::spawn(async move {
|
||||
client_authenticate(
|
||||
&mut client_reader,
|
||||
&mut client_writer,
|
||||
"laptop",
|
||||
&[8u8; 32],
|
||||
1024,
|
||||
Duration::from_secs(1),
|
||||
)
|
||||
.await
|
||||
});
|
||||
assert!(matches!(
|
||||
client.await.unwrap(),
|
||||
Err(tls_tcp::AuthError::Rejected)
|
||||
));
|
||||
assert!(matches!(
|
||||
server.await.unwrap(),
|
||||
Err(tls_tcp::AuthError::InvalidCredentials)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn framed_session_preserves_empty_datagrams_and_heartbeats() {
|
||||
let (left, right) = duplex(4096);
|
||||
let (mut reader, _unused_writer) = split(left);
|
||||
let (unused_reader, writer) = split(right);
|
||||
drop(unused_reader);
|
||||
let mut writer = FramedWriter::new(writer, 1200);
|
||||
let mut reader = FramedReader::new(&mut reader, 1200, Some(Duration::from_secs(1)));
|
||||
// Write frames using the writer half and read them from the other side.
|
||||
// The opposite halves are intentionally dropped only after construction;
|
||||
// duplex keeps the active directions independent.
|
||||
let send = async {
|
||||
writer.send_keepalive().await.unwrap();
|
||||
writer.send_datagram(b"").await.unwrap();
|
||||
writer.send_datagram(b"payload").await.unwrap();
|
||||
};
|
||||
tokio::pin!(send);
|
||||
send.await;
|
||||
assert_eq!(reader.recv_datagram().await.unwrap(), b"");
|
||||
assert_eq!(reader.recv_datagram().await.unwrap(), b"payload");
|
||||
}
|
||||
Reference in new issue
Block a user