Skip to content

Commit 552f7da

Browse files
test(agent-tunnel): add e2e + registry + routing test suite
- testsuite/tests/agent_tunnel/integration.rs: full QUIC e2e (CA → enroll → mTLS → control stream → ConnectRequest → echo) and a domain-routing variant. - testsuite/tests/agent_tunnel/registry.rs: AgentRegistry unit tests covering online/offline timeouts, route epoch handling (replace vs refresh vs ignore stale), domain advertisement persistence, and the agent-info snapshot. - testsuite/tests/agent_tunnel/routing.rs: RoutePlan::resolve / try_route decision matrix — explicit jet_agent_id, subnet match, domain match, fallback to direct, offline filtering, and the no-handle paths. Also includes the read_cert_chain rewrite from #1771 (needed for the e2e tests to parse the rcgen-produced PEM); once #1771 lands this collapses to just the test files.
1 parent 6c27fb8 commit 552f7da

7 files changed

Lines changed: 778 additions & 22 deletions

File tree

crates/agent-tunnel/src/cert.rs

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::time::Duration;
88

99
use anyhow::{Context as _, bail};
1010
use camino::{Utf8Path, Utf8PathBuf};
11-
use picky::pem::parse_pem;
11+
use picky::pem::{PemError, parse_pem, read_pem};
1212
use picky::x509::Cert;
1313
use picky_asn1_x509::{ExtensionView, GeneralName};
1414
use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose, SanType};
@@ -32,29 +32,28 @@ fn cert_pem_to_der(pem_str: &str) -> anyhow::Result<Vec<u8>> {
3232
/// Parse one or more PEM-encoded certificates into `rustls` certificate types.
3333
///
3434
/// A PEM file can carry multiple concatenated CERTIFICATE blocks (chain). We
35-
/// iterate block-by-block with [`parse_pem`], check each label, and wrap the
36-
/// DER bytes in [`rustls_pki_types::CertificateDer`] — the only type the
37-
/// rustls/quinn TLS builders accept.
35+
/// use [`read_pem`] in a loop — each call consumes one block; `HeaderNotFound`
36+
/// signals "no more blocks left", which is the termination condition. Each
37+
/// block's label is verified, then the DER bytes are wrapped in
38+
/// [`rustls_pki_types::CertificateDer`] — the type the rustls/quinn TLS
39+
/// builders accept.
3840
fn read_cert_chain(pem_str: &str) -> anyhow::Result<Vec<rustls::pki_types::CertificateDer<'static>>> {
41+
use std::io::BufReader;
42+
43+
let mut reader = BufReader::new(pem_str.as_bytes());
3944
let mut chain = Vec::new();
40-
let mut remaining = pem_str;
41-
while let Some(start) = remaining.find("-----BEGIN ") {
42-
let block_end = remaining[start..]
43-
.find("-----END ")
44-
.and_then(|e| {
45-
remaining[start + e..]
46-
.find("-----\n")
47-
.map(|n| start + e + n + "-----\n".len())
48-
})
49-
.context("malformed PEM block (no END tag)")?;
50-
51-
let block = &remaining[start..block_end];
52-
let pem = parse_pem(block).context("parse PEM block")?;
53-
if pem.label() != PEM_LABEL_CERTIFICATE {
54-
bail!("expected {PEM_LABEL_CERTIFICATE} PEM, got {}", pem.label());
45+
46+
loop {
47+
match read_pem(&mut reader) {
48+
Ok(pem) => {
49+
if pem.label() != PEM_LABEL_CERTIFICATE {
50+
bail!("expected {PEM_LABEL_CERTIFICATE} PEM, got {}", pem.label());
51+
}
52+
chain.push(rustls::pki_types::CertificateDer::from(pem.data().to_vec()));
53+
}
54+
Err(PemError::HeaderNotFound) => break,
55+
Err(e) => return Err(anyhow::Error::new(e).context("parse PEM block")),
5556
}
56-
chain.push(rustls::pki_types::CertificateDer::from(pem.data().to_vec()));
57-
remaining = &remaining[block_end..];
5857
}
5958

6059
if chain.is_empty() {

testsuite/Cargo.toml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,27 @@ typed-builder = "0.21"
3131
tokio-tungstenite = { version = "0.26", features = ["rustls-tls-native-roots"] }
3232

3333
[dev-dependencies]
34+
agent-tunnel = { path = "../crates/agent-tunnel" }
35+
agent-tunnel-proto = { path = "../crates/agent-tunnel-proto", features = ["serde"] }
36+
devolutions-gateway-task = { path = "../crates/devolutions-gateway-task" }
3437
base64 = "0.22"
35-
proxy-socks = { path = "../crates/proxy-socks" }
38+
camino = "1"
39+
ipnetwork = "0.20"
3640
libsql = { version = "0.9", default-features = false, features = ["core"] }
3741
mcp-proxy.path = "../crates/mcp-proxy"
42+
proxy-socks = { path = "../crates/proxy-socks" }
43+
quinn = "0.11"
44+
rcgen = { version = "0.13", features = ["pem", "x509-parser"] }
3845
rstest = "0.25"
46+
rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] }
47+
rustls-pemfile = "2"
48+
rustls-pki-types = "1"
3949
serde_json = "1"
4050
sysevent.path = "../crates/sysevent"
4151
tempfile = "3"
4252
test-utils.path = "../crates/test-utils"
4353
tokio-rustls = { version = "0.26", features = ["ring"] }
54+
uuid = { version = "1", features = ["v4"] }
4455

4556
[target.'cfg(unix)'.dev-dependencies]
4657
sysevent-syslog.path = "../crates/sysevent-syslog"

0 commit comments

Comments
 (0)