diff --git a/Cargo.lock b/Cargo.lock index ae97329..ecc86e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,6 +94,45 @@ dependencies = [ "password-hash", ] +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -146,6 +185,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -173,6 +218,15 @@ dependencies = [ "syn", ] +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.11.0" @@ -502,6 +556,20 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.5.8" @@ -1004,7 +1072,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -1285,10 +1353,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ "aws-lc-rs", - "base64", + "base64 0.22.1", "getrandom 0.2.17", "js-sys", - "pem", + "pem 3.0.6", "serde", "serde_json", "signature", @@ -1407,6 +1475,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mio" version = "1.2.0" @@ -1424,7 +1498,7 @@ version = "0.8.29" dependencies = [ "arc-swap", "argon2", - "base64", + "base64 0.22.1", "comfy-table", "fjall", "flume", @@ -1457,7 +1531,7 @@ dependencies = [ name = "mqdb-cli" version = "0.8.39" dependencies = [ - "base64", + "base64 0.22.1", "bebytes", "clap", "comfy-table", @@ -1497,6 +1571,7 @@ dependencies = [ "mqdb-vault", "mqtt5", "quinn", + "rcgen", "regex", "ring", "rustls", @@ -1539,7 +1614,7 @@ dependencies = [ name = "mqdb-vault" version = "0.1.5" dependencies = [ - "base64", + "base64 0.22.1", "mqdb-agent", "mqdb-core", "mqtt5", @@ -1560,7 +1635,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d9de73200ed2112ed25ebcdd5e74b22aa5c89065fd3554e111e3353e9d5f0" dependencies = [ "argon2", - "base64", + "base64 0.22.1", "bebytes", "bytes", "flume", @@ -1618,6 +1693,16 @@ dependencies = [ "web-time", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1661,6 +1746,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1796,7 +1890,17 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64", + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120" +dependencies = [ + "base64 0.23.1", "serde_core", ] @@ -2065,6 +2169,20 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "rcgen" +version = "0.14.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8" +dependencies = [ + "pem 4.0.0", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -2109,7 +2227,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-channel", @@ -2175,6 +2293,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "1.1.4" @@ -2858,7 +2985,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" dependencies = [ "async-trait", - "base64", + "base64 0.22.1", "bytes", "http", "http-body", @@ -3577,12 +3704,40 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + [[package]] name = "xxhash-rust" version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec", + "time", +] + [[package]] name = "yoke" version = "0.8.2" diff --git a/crates/mqdb-cluster/Cargo.toml b/crates/mqdb-cluster/Cargo.toml index fb3a9a3..7fd1a6e 100644 --- a/crates/mqdb-cluster/Cargo.toml +++ b/crates/mqdb-cluster/Cargo.toml @@ -46,5 +46,6 @@ dev-insecure = ["mqdb-agent/dev-insecure"] http-api = ["mqdb-agent/http-api"] [dev-dependencies] +rcgen = "0.14.10" tempfile.workspace = true tracing-subscriber.workspace = true diff --git a/crates/mqdb-cluster/src/cluster/quic_transport.rs b/crates/mqdb-cluster/src/cluster/quic_transport.rs index 713cd1a..b65e018 100644 --- a/crates/mqdb-cluster/src/cluster/quic_transport.rs +++ b/crates/mqdb-cluster/src/cluster/quic_transport.rs @@ -889,3 +889,111 @@ mod tests { assert_eq!(MAX_MESSAGE_SIZE, 10 * 1024 * 1024); } } + +#[cfg(test)] +mod framing_tests { + use super::*; + use std::path::PathBuf; + + fn self_signed_certs() -> (tempfile::TempDir, PathBuf, PathBuf) { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]) + .expect("generate self-signed cert"); + let dir = tempfile::tempdir().expect("tempdir"); + let cert_path = dir.path().join("cert.pem"); + let key_path = dir.path().join("key.pem"); + std::fs::write(&cert_path, cert.cert.pem()).expect("write cert"); + std::fs::write(&key_path, cert.signing_key.serialize_pem()).expect("write key"); + (dir, cert_path, key_path) + } + + struct Pair { + _certs: tempfile::TempDir, + _client_endpoint: Endpoint, + _server_endpoint: Endpoint, + client: quinn::Connection, + server: quinn::Connection, + } + + async fn connected_pair() -> Pair { + rustls::crypto::ring::default_provider() + .install_default() + .ok(); + let (dir, cert_path, key_path) = self_signed_certs(); + + let server_config = + build_server_config(&cert_path, &key_path, None).expect("server config"); + let addr: SocketAddr = "127.0.0.1:0".parse().unwrap(); + let server = Endpoint::server(server_config, addr).expect("server endpoint"); + let server_addr = server.local_addr().expect("local addr"); + + let accept = tokio::spawn(async move { + let incoming = server.accept().await.expect("incoming"); + let conn = incoming.await.expect("accepted"); + (server, conn) + }); + + let client_config = + build_client_config_secure(Some(&cert_path), None, None).expect("client config"); + let mut client = Endpoint::client("127.0.0.1:0".parse().unwrap()).expect("client endpoint"); + client.set_default_client_config(client_config); + let client_conn = client + .connect(server_addr, "localhost") + .expect("connect") + .await + .expect("handshake"); + + let (server_endpoint, server_conn) = accept.await.expect("accept task"); + Pair { + _certs: dir, + _client_endpoint: client, + _server_endpoint: server_endpoint, + client: client_conn, + server: server_conn, + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn cancelled_write_all_desynchronizes_the_framed_stream() { + let pair = connected_pair().await; + let (client_conn, server_conn) = (pair.client.clone(), pair.server.clone()); + let (mut send, _client_recv) = client_conn.open_bi().await.expect("open_bi"); + + let reader = tokio::spawn(async move { + let (_srv_send, mut recv) = server_conn.accept_bi().await.expect("accept_bi"); + let mut len_buf = [0u8; 4]; + recv.read_exact(&mut len_buf).await.expect("read length"); + let announced = u32::from_be_bytes(len_buf) as usize; + let mut body = vec![0u8; announced]; + let framed = + tokio::time::timeout(Duration::from_secs(3), recv.read_exact(&mut body)).await; + (announced, framed.is_ok()) + }); + + let big_payload = vec![0xABu8; 8 * 1024 * 1024]; + #[allow(clippy::cast_possible_truncation)] + let big_len = (big_payload.len() as u32).to_be_bytes(); + send.write_all(&big_len).await.expect("length prefix"); + + let cancelled = + tokio::time::timeout(Duration::from_millis(2), send.write_all(&big_payload)).await; + assert!( + cancelled.is_err(), + "the timeout must fire while write_all is still mid-payload" + ); + + let second = b"SECOND"; + #[allow(clippy::cast_possible_truncation)] + let second_len = (second.len() as u32).to_be_bytes(); + send.write_all(&second_len).await.expect("second length"); + send.write_all(second).await.expect("second payload"); + + let (announced, completed) = reader.await.expect("reader task"); + assert_eq!(announced, big_payload.len()); + assert!( + !completed, + "a cancelled write_all leaves a partial frame: the reader is still waiting for the \ + abandoned frame's remaining bytes and has swallowed the next frame as its payload, \ + so the stream never resynchronizes" + ); + } +}