Skip to content

Commit 3c49f7f

Browse files
feat(dgw): agent tunnel transparent routing + cert renewal
Builds on #1738 (core infrastructure). Follow-up PRs will add the Windows/Linux installer integration, gateway webapp agent management UI, Docker deployment, and Playwright E2E harness. Transparent routing: - `crates/agent-tunnel/src/routing.rs`: `RoutingDecision` pipeline — explicit `jet_agent_id` from the JWT → subnet match → domain suffix match (longest wins) → direct connect. Single `try_route` entry point consumed by all gateway proxy paths. - `crates/agent-tunnel/src/registry.rs`: `find_agents_for(host)` + `RouteAdvertisementState::matches_target()` do the lookup in one spot; offline agents are skipped. - Gateway proxy integration: `api/fwd.rs`, `api/kdc_proxy.rs`, `api/rdp.rs`, `rd_clean_path.rs`, `generic_client.rs`, `rdp_proxy.rs` all call `try_route` before falling through to direct TCP. - Tests: `agent-tunnel/src/integration_test.rs` (2 full-stack QUIC E2E), `tests/agent_tunnel_registry.rs` (13), `tests/agent_tunnel_ routing.rs` (8). Agent-side certificate renewal: - `enrollment.rs`: `is_cert_expiring(cert_path, threshold_days)` and `generate_csr_from_existing_key(key_path, agent_name)` — the key never changes across renewals, the gateway just signs a new cert with the same public key. - `tunnel.rs`: on connect, if the cert is within 15 days of expiry, the agent sends a `CertRenewalRequest` control message with a new CSR, waits for `CertRenewalResponse::Success`, writes the renewed cert and CA, and reconnects. - `agent-tunnel/src/listener.rs`: gateway-side handler signs the CSR via `CaManager::sign_agent_csr` and returns the new cert chain. (Stub replaced: master's handler emitted a debug log and dropped the message.) QUIC endpoint override: - `enrollment.rs`: new `quic_endpoint_override: Option<String>` parameter on `enroll_agent` — if set, overrides the endpoint returned by the enroll API. Needed because the gateway's `quic_endpoint` is derived from `conf.hostname`, which in a containerized deployment is often the container ID (not routable from outside). - `main.rs`: new `--quic-endpoint` CLI flag and `jet_quic_endpoint` JWT claim; precedence is CLI flag > JWT claim > enroll API response. Agent-side routing primitives: - `tunnel_helpers.rs`: `Target::Ip` / `Target::Domain` enum parsed from the gateway's `ConnectRequest::target`, `resolve_target` (domain → DNS), `connect_to_target` (happy-eyeballs). Tests: 22 agent-tunnel lib + 3 proto version + 24 proto control + 11 proto session + 13 registry + 8 routing integration + 64 gateway lib, all green. Zero clippy warnings; nightly fmt clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d89dd0c commit 3c49f7f

21 files changed

Lines changed: 1486 additions & 151 deletions

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/agent-tunnel/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,6 @@ quinn = "0.11"
4343

4444
[dev-dependencies]
4545
base64 = "0.22"
46-
tokio = { version = "1.45", features = ["macros"] }
46+
tempfile = "3"
47+
rustls-pemfile = "2.2"
48+
tokio = { version = "1.45", features = ["macros", "net"] }

crates/agent-tunnel/src/cert.rs

Lines changed: 21 additions & 22 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() {
@@ -87,7 +86,7 @@ const SERVER_CERT_FILENAME: &str = "agent-tunnel-server-cert.pem";
8786
const SERVER_KEY_FILENAME: &str = "agent-tunnel-server-key.pem";
8887
const CA_VALIDITY_DAYS: u32 = 3650; // ~10 years
8988
const SERVER_CERT_VALIDITY_DAYS: u32 = 365; // 1 year
90-
const AGENT_CERT_VALIDITY_DAYS: u32 = 365; // 1 year
89+
const AGENT_CERT_VALIDITY_DAYS: u32 = 30; // 30 days (short-lived, auto-renewed)
9190

9291
const SECS_PER_DAY: u64 = 86_400;
9392
const CA_COMMON_NAME: &str = "Devolutions Gateway Agent Tunnel CA";

0 commit comments

Comments
 (0)