feat(agent): transparent routing through agent tunnel#1741
Conversation
8638365 to
ad3d3a0
Compare
76acc25 to
b70e278
Compare
There was a problem hiding this comment.
Pull request overview
Adds transparent target-based routing through the QUIC agent tunnel so the gateway can automatically forward connections via an enrolled agent when the destination matches advertised subnets/domains.
Changes:
- Introduces a shared agent-tunnel routing pipeline (
resolve_route/try_route) and wires it into forwarding (WS TCP/TLS), RDP clean path, and KDC proxy. - Extends route advertisements to support IPv4+IPv6 subnets and normalized domain suffix matching (longest domain suffix wins).
- Updates RDP clean-path server connection logic to support both TCP and QUIC transports via a concrete
ServerTransportenum (to preserveSend).
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| devolutions-gateway/src/rdp_proxy.rs | Updates Kerberos send function signature usage for CredSSP network requests. |
| devolutions-gateway/src/rd_clean_path.rs | Splits clean-path into authorization vs connect; adds TCP/QUIC ServerTransport for server side. |
| devolutions-gateway/src/proxy.rs | Tightens transport bounds to require Send for both sides. |
| devolutions-gateway/src/generic_client.rs | Integrates agent-tunnel routing into generic TCP forwarding path. |
| devolutions-gateway/src/api/rdp.rs | Plumbs agent_tunnel_handle into the RDP handler path. |
| devolutions-gateway/src/api/kdc_proxy.rs | Adds optional agent-tunnel routing to KDC proxy send path and generalizes reply reading. |
| devolutions-gateway/src/api/fwd.rs | Plumbs agent_tunnel_handle into WS forwarder and routes via tunnel when matched. |
| devolutions-gateway/src/agent_tunnel/routing.rs | New shared routing pipeline + unit tests. |
| devolutions-gateway/src/agent_tunnel/registry.rs | Adds target matching helpers and agent lookup by subnet/domain specificity; moves to IpNetwork. |
| devolutions-gateway/src/agent_tunnel/mod.rs | Exposes new routing module. |
| devolutions-agent/src/tunnel_helpers.rs | Extends tunnel target parsing/resolution to support IPv6 and IpNetwork. |
| devolutions-agent/src/tunnel.rs | Switches advertised subnets to IpNetwork and domains to normalized DomainName. |
| crates/agent-tunnel-proto/src/stream.rs | Refactors framing helpers placement and control stream split types. |
| crates/agent-tunnel-proto/src/lib.rs | Re-exports DomainName. |
| crates/agent-tunnel-proto/src/control.rs | Introduces DomainName and changes subnet advertisement type to IpNetwork (IPv4+IPv6). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
b70e278 to
80aed20
Compare
f323f30 to
3c49f7f
Compare
- routing.rs: when `explicit_agent_id` is set but the gateway has no
tunnel handle, return `Err` instead of silently falling back to a
direct connect. A token that names a specific `jet_agent_id` is
declaring a required network boundary; silent fallback would bypass
it.
- api/fwd.rs, generic_client.rs, rd_clean_path.rs, api/kdc_proxy.rs:
use `TargetAddr::as_addr()` (which brackets IPv6) instead of
`format!("{host}:{port}")` or `to_string()` (which includes scheme).
Fixes two real bugs: IPv6 targets were malformed (`::1:443` vs
`[::1]:443`), and kdc_proxy was passing `tcp://host:88` to the
tunnel target parser — which only accepts bare `host:port`.
- rdp_proxy.rs: add a `TODO(agent-tunnel)` documenting that CredSSP
Kerberos network requests cannot currently traverse the agent
tunnel because `send_network_request` hardcodes `None` for the
handle. Edge case (KDC behind a NAT'd site only reachable via an
enrolled agent); plumbing the handle through `RdpProxy` is a
follow-up.
- tests/agent_tunnel_routing.rs: replace a flaky `thread::sleep(10ms)`
(Windows timer resolution is ~16 ms) with an explicit
`set_received_at_for_test` helper. Adds two new tests for the new
explicit-agent-without-handle error path.
- registry.rs: expose `set_received_at_for_test` for the above.
- agent-tunnel-proto/control.rs: fix a stale doc comment that claimed
`subnets` is IPv4+IPv6 (it is IPv4-only; `Vec<Ipv4Network>`).
- routing.rs: when `explicit_agent_id` is set but the gateway has no
tunnel handle, return `Err` instead of silently falling back to a
direct connect. A token that names a specific `jet_agent_id` is
declaring a required network boundary; silent fallback would bypass
it.
- api/fwd.rs, generic_client.rs, rd_clean_path.rs, api/kdc_proxy.rs:
use `TargetAddr::as_addr()` (which brackets IPv6) instead of
`format!("{host}:{port}")` or `to_string()` (which includes scheme).
Fixes two real bugs: IPv6 targets were malformed (`::1:443` vs
`[::1]:443`), and kdc_proxy was passing `tcp://host:88` to the
tunnel target parser — which only accepts bare `host:port`.
- rdp_proxy.rs: add a `TODO(agent-tunnel)` documenting that CredSSP
Kerberos network requests cannot currently traverse the agent
tunnel because `send_network_request` hardcodes `None` for the
handle. Edge case (KDC behind a NAT'd site only reachable via an
enrolled agent); plumbing the handle through `RdpProxy` is a
follow-up.
- tests/agent_tunnel_routing.rs: replace a flaky `thread::sleep(10ms)`
(Windows timer resolution is ~16 ms) with an explicit
`set_received_at_for_test` helper. Adds two new tests for the new
explicit-agent-without-handle error path.
- registry.rs: expose `set_received_at_for_test` for the above.
- agent-tunnel-proto/control.rs: fix a stale doc comment that claimed
`subnets` is IPv4+IPv6 (it is IPv4-only; `Vec<Ipv4Network>`).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 32 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- routing.rs: when `explicit_agent_id` is set but the gateway has no
tunnel handle, return `Err` instead of silently falling back to a
direct connect. A token that names a specific `jet_agent_id` is
declaring a required network boundary; silent fallback would bypass
it.
- api/fwd.rs, generic_client.rs, rd_clean_path.rs, api/kdc_proxy.rs:
use `TargetAddr::as_addr()` (which brackets IPv6) instead of
`format!("{host}:{port}")` or `to_string()` (which includes scheme).
Fixes two real bugs: IPv6 targets were malformed (`::1:443` vs
`[::1]:443`), and kdc_proxy was passing `tcp://host:88` to the
tunnel target parser — which only accepts bare `host:port`.
- rdp_proxy.rs: add a `TODO(agent-tunnel)` documenting that CredSSP
Kerberos network requests cannot currently traverse the agent
tunnel because `send_network_request` hardcodes `None` for the
handle. Edge case (KDC behind a NAT'd site only reachable via an
enrolled agent); plumbing the handle through `RdpProxy` is a
follow-up.
- tests/agent_tunnel_routing.rs: replace a flaky `thread::sleep(10ms)`
(Windows timer resolution is ~16 ms) with an explicit
`set_received_at_for_test` helper. Adds two new tests for the new
explicit-agent-without-handle error path.
- registry.rs: expose `set_received_at_for_test` for the above.
- agent-tunnel-proto/control.rs: fix a stale doc comment that claimed
`subnets` is IPv4+IPv6 (it is IPv4-only; `Vec<Ipv4Network>`).
137a51f to
9f3321b
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 32 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 34 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
mistake, will fix
| let ca_manager = agent_tunnel::cert::CaManager::load_or_generate(&data_dir) | ||
| .context("failed to initialize agent tunnel CA")?; | ||
|
|
||
| // Bind to the IPv6 unspecified address so the listener is dual-stack and |
There was a problem hiding this comment.
mistake, will fix
| let target_addr = TargetAddr::parse(request.url.as_str(), Some(88))?; | ||
|
|
||
| send_krb_message(&target_addr, &request.data) | ||
| // TODO(agent-tunnel): plumb `agent_tunnel_handle` through `RdpProxy` and pass it here |
There was a problem hiding this comment.
Double check later, address in follow up
PR #1741 was reviewed as too large. Reduce its scope to A+B (refactor + transparent routing) by backing out the cert-renewal additions (C) and the JWT-based enrollment pivot (D). Both will be opened as their own PRs against master. Cert renewal (C) removed: - Agent-side: drop the pre-loop expiry check, periodic cert_expiry_tick in the main select! loop, ConnectionOutcome enum, and the `is_cert_expiring` / `read_agent_name_from_cert` / `generate_csr_from_existing_key` helpers from enrollment.rs. - Gateway-side: drop the agent's ability to drive renewal; the CertRenewal proto messages stay (they exist on master from #1738) and the listener keeps the stub debug-and-drop arm. AGENT_CERT_VALIDITY_DAYS reverts to 365. JWT enrollment refactor (D) removed: - Gateway: revert token.rs (TunnelEnroll only, no AgentEnroll/AgentRead), extract.rs (no AgentManagement scope unions), and api/tunnel.rs to master (EnrollmentTokenStore-backed enroll handler with quic_endpoint in the response). - Agent-tunnel crate: restore enrollment_store module + handle getter + registration in bind(). - Agent CLI: revert main.rs and cli_tests.rs to before --advertise-domains (config-side advertise_domains support stays, only the CLI flag goes). Test JWTs go back to gateway.tunnel.enroll scope. - NuGet: delete EnrollmentClaims.cs, drop GatewayAgentEnroll/Read from AccessScope.cs, revert csproj version, drop the new JsonSerializationTests cases.
Trim missed agent-side D content — the JWT enrollment refactor lives in its own follow-up PR, so PR2 should not carry any of it: - enrollment.rs: restore EnrollResponse::quic_endpoint and the original enroll_agent / persist_enrollment_response signatures (no extra quic_endpoint or advertise_domains parameters). Drop the EnrollmentJwtClaims::jet_quic_endpoint claim — the enrollment JWT carries gw_url / agent_name only. - main.rs: drop --quic-endpoint CLI flag, drop UpCommand::quic_endpoint, drop the JWT jet_quic_endpoint extraction, restore the two-arg-shorter service-mode signature, restore the inline cli tests module. - cli_tests.rs: removed (the tests are back inline in main.rs at the master state).
… PRs
Tests (testsuite/tests/agent_tunnel/{integration,registry,routing}.rs) and
the read_cert_chain rewrite are not part of the routing/upstream feature
itself — they ship as their own PRs so this one stays focused on the
feature code:
- Cert PEM parsing fix → #1771
- Agent-tunnel test suite → follow-up PR (stacked on this one)
After this trim, PR2's diff is purely:
- Routing: agent-tunnel/{routing,registry,listener}.rs
- Upstream refactor: devolutions-gateway/upstream.rs and the proxy paths
(fwd, kdc_proxy, rdp, rdp_proxy, rd_clean_path, generic_client)
- Agent client: devolutions-agent/tunnel_helpers.rs (TargetAddr widening
to handle IPv6 alongside IPv4)
Revert kdc_proxy.rs to master. KDC tunnel routing covers two callers (the /jet/KdcProxy HTTP handler and the CredSSP/NLA path in rdp_proxy.rs::send_network_request), and both need to agree on how send_krb_message takes a session_id. Doing only the HTTP handler here forced a Uuid::new_v4() at the routing site for a "session" the KDC token has no notion of -- meaningless on the wire and on the agent log. Move the whole KDC-via-tunnel story (HTTP path + CredSSP path, plus the read_kdc_reply_message DoS cap) into DGW-384 where the API can be designed once with both call sites in view. Also drop the kdc_proxy.rs reference from routing.rs's module doc since this crate's caller is now only the upstream module family.
b4ec360 to
a2b2ff5
Compare
Address CBenoit's review on #1741: in addition to the `#[doc(hidden)]` marker and the `_for_test` naming, gate `AgentPeer::set_last_seen_for_test` and `set_received_at_for_test` behind `#[cfg(any(test, feature = "test-utils"))]` so production builds of `devolutions-gateway` and `devolutions-agent` don't compile these methods at all. Cross-crate test consumers (the workspace `testsuite` crate carrying the agent-tunnel integration tests, in #1772) opt in via `features = ["test-utils"]` on their `agent-tunnel` dev-dep.
Benoît Cortier (CBenoit)
left a comment
There was a problem hiding this comment.
LGTM
Let’s iterate on that in follow up PRs as we discussed on Slack
When an agent advertises the KDC's subnet or DNS domain, route Kerberos traffic through the QUIC tunnel just like every other proxy path. This closes the last gap left after the transparent routing PR (#1741): - `/jet/KdcProxy` HTTP endpoint — `send_krb_message` now consults the routing pipeline before falling back to direct TCP. The HTTP handler has no parent association, so it mints a fresh session_id purely for agent-side log correlation. - RDP CredSSP/NLA — `rdp_proxy.rs::send_network_request` previously hard-coded `None` for the agent handle. Plumb `agent_tunnel_handle` and `session_id` from `RdpProxy` down through `perform_credssp_with_*` → `resolve_*_generator` → `send_network_request`. The same change reaches the credential-injection clean path (`rd_clean_path.rs`). `session_id` here is `session_info.id` / `claims.jet_aid` so the agent log ties KDC sub-traffic to its parent RDP session. Stack: based on #1741. Picks up `agent_tunnel::routing::try_route`. `send_krb_message` signature gains `(agent_tunnel_handle, session_id)` in that order — required `Uuid`, no `Option<>` — so the call site is honest about which UUID it's logging. The UDP scheme guard (KDC over UDP keeps going direct because the agent protocol only carries TCP) and the 64 KiB `MAX_KDC_REPLY_MESSAGE_LEN` DoS cap (and the matching generic `read_kdc_reply_message`) come along since they live in the same file and serve the same end.
When an agent advertises the KDC's subnet or DNS domain, route Kerberos traffic through the QUIC tunnel just like every other proxy path. This closes the last gap left after the transparent routing PR (#1741): - `/jet/KdcProxy` HTTP endpoint — `send_krb_message` now consults the routing pipeline before falling back to direct TCP. The HTTP handler has no parent association, so it mints a fresh session_id purely for agent-side log correlation. - RDP CredSSP/NLA — `rdp_proxy.rs::send_network_request` previously hard-coded `None` for the agent handle. Plumb `agent_tunnel_handle` and `session_id` from `RdpProxy` down through `perform_credssp_with_*` → `resolve_*_generator` → `send_network_request`. The same change reaches the credential-injection clean path (`rd_clean_path.rs`). `session_id` here is `session_info.id` / `claims.jet_aid` so the agent log ties KDC sub-traffic to its parent RDP session. Stack: based on #1741. Picks up `agent_tunnel::routing::try_route`. `send_krb_message` signature gains `(agent_tunnel_handle, session_id)` in that order — required `Uuid`, no `Option<>` — so the call site is honest about which UUID it's logging. The UDP scheme guard (KDC over UDP keeps going direct because the agent protocol only carries TCP) and the 64 KiB `MAX_KDC_REPLY_MESSAGE_LEN` DoS cap (and the matching generic `read_kdc_reply_message`) come along since they live in the same file and serve the same end.
When an agent advertises the KDC's subnet or DNS domain, route Kerberos traffic through the QUIC tunnel just like every other proxy path. This closes the last gap left after the transparent routing PR (#1741): - `/jet/KdcProxy` HTTP endpoint — `send_krb_message` now consults the routing pipeline before falling back to direct TCP. The HTTP handler has no parent association, so it mints a fresh session_id purely for agent-side log correlation. - RDP CredSSP/NLA — `rdp_proxy.rs::send_network_request` previously hard-coded `None` for the agent handle. Plumb `agent_tunnel_handle` and `session_id` from `RdpProxy` down through `perform_credssp_with_*` → `resolve_*_generator` → `send_network_request`. The same change reaches the credential-injection clean path (`rd_clean_path.rs`). `session_id` here is `session_info.id` / `claims.jet_aid` so the agent log ties KDC sub-traffic to its parent RDP session. Stack: based on #1741. Picks up `agent_tunnel::routing::try_route`. `send_krb_message` signature gains `(agent_tunnel_handle, session_id)` in that order — required `Uuid`, no `Option<>` — so the call site is honest about which UUID it's logging. The UDP scheme guard (KDC over UDP keeps going direct because the agent protocol only carries TCP) and the 64 KiB `MAX_KDC_REPLY_MESSAGE_LEN` DoS cap (and the matching generic `read_kdc_reply_message`) come along since they live in the same file and serve the same end.
When an agent advertises the KDC's subnet or DNS domain, route Kerberos traffic through the QUIC tunnel just like every other proxy path. This closes the last gap left after the transparent routing PR (#1741). Two paths now use the same routing pipeline as connection forwarding: - `/jet/KdcProxy` HTTP endpoint — `send_krb_message` consults the routing pipeline before falling back to direct TCP. - RDP CredSSP/NLA — `rdp_proxy.rs::send_network_request` previously hard-coded `None` for the agent handle. Plumb `agent_tunnel_handle`, `session_id`, and `explicit_agent_id` from `RdpProxy` down through `perform_credssp_as_*` -> `resolve_*_generator` -> `send_network_request`. The same change reaches the credential-injection clean path (`rd_clean_path.rs`). Session correlation: - RDP CredSSP callers pass the parent association's `jet_aid` so KDC sub-traffic ties back to its parent RDP session in agent-side logs. - The HTTP `/jet/KdcProxy` handler passes the KDC token's own `jti` (the most persistent identifier available without a parent association). `KdcToken` now carries `jti` alongside the claims for this purpose. Explicit-agent routing (matches every other proxy path): - `send_krb_message` takes `explicit_agent_id: Option<Uuid>` and forwards it to `agent_tunnel::routing::try_route`. When the parent association pins `jet_agent_id`, the KDC sub-traffic is routed via that agent or fails -- never silently falls back to a different agent or to direct connect. The HTTP handler passes `None`. Hardening (came along since they live in the same file): - 64 KiB `MAX_KDC_REPLY_MESSAGE_LEN` DoS cap on the announced TCP-framed KDC reply length, with overflow-safe length math. - UDP scheme guard: KDC over UDP keeps going direct because the agent tunnel only carries TCP today. Drive-by: `crates/agent-tunnel/src/listener.rs` move-after-move on `ca_manager` introduced by #1775 -- fixed with `Arc::clone` to keep master building on `--no-default-features` configurations. Stack: based on #1741. Picks up `agent_tunnel::routing::try_route`. Issue: DGW-384
When an agent advertises the KDC's subnet or DNS domain, route Kerberos traffic through the QUIC tunnel just like every other proxy path. This closes the last gap left after the transparent routing PR (#1741). Two paths now use the same routing pipeline as connection forwarding: - `/jet/KdcProxy` HTTP endpoint — `send_krb_message` consults the routing pipeline before falling back to direct TCP. - RDP CredSSP/NLA — `rdp_proxy.rs::send_network_request` previously hard-coded `None` for the agent handle. Plumb `agent_tunnel_handle`, `session_id`, and `explicit_agent_id` from `RdpProxy` down through `perform_credssp_as_*` -> `resolve_*_generator` -> `send_network_request`. The same change reaches the credential-injection clean path (`rd_clean_path.rs`). Session correlation: - RDP CredSSP callers pass the parent association's `jet_aid` so KDC sub-traffic ties back to its parent RDP session in agent-side logs. - The HTTP `/jet/KdcProxy` handler passes the KDC token's own `jti` (the most persistent identifier available without a parent association). `KdcToken` now carries `jti` alongside the claims for this purpose. Explicit-agent routing (matches every other proxy path): - `send_krb_message` takes `explicit_agent_id: Option<Uuid>` and forwards it to `agent_tunnel::routing::try_route`. When the parent association pins `jet_agent_id`, the KDC sub-traffic is routed via that agent or fails -- never silently falls back to a different agent or to direct connect. The HTTP handler passes `None`. Hardening (came along since they live in the same file): - 64 KiB `MAX_KDC_REPLY_MESSAGE_LEN` DoS cap on the announced TCP-framed KDC reply length, with overflow-safe length math. - UDP scheme guard: KDC over UDP keeps going direct because the agent tunnel only carries TCP today. Drive-by: `crates/agent-tunnel/src/listener.rs` move-after-move on `ca_manager` introduced by #1775 -- fixed with `Arc::clone` to keep master building on `--no-default-features` configurations. Stack: based on #1741. Picks up `agent_tunnel::routing::try_route`. Issue: DGW-384
When an agent advertises the KDC's subnet or DNS domain, route Kerberos traffic through the QUIC tunnel just like every other proxy path. This closes the last gap left after the transparent routing PR (#1741). Two paths now use the same routing pipeline as connection forwarding: - `/jet/KdcProxy` HTTP endpoint — `send_krb_message` consults the routing pipeline before falling back to direct TCP. - RDP CredSSP/NLA — `rdp_proxy.rs::send_network_request` previously hard-coded `None` for the agent handle. Plumb `agent_tunnel_handle`, `session_id`, and `explicit_agent_id` from `RdpProxy` down through `perform_credssp_as_*` -> `resolve_*_generator` -> `send_network_request`. The same change reaches the credential-injection clean path (`rd_clean_path.rs`). Session correlation: - RDP CredSSP callers pass the parent association's `jet_aid` so KDC sub-traffic ties back to its parent RDP session in agent-side logs. - The HTTP `/jet/KdcProxy` handler passes the KDC token's own `jti` (the most persistent identifier available without a parent association). `KdcToken` now carries `jti` alongside the claims for this purpose. Explicit-agent routing (matches every other proxy path): - `send_krb_message` takes `explicit_agent_id: Option<Uuid>` and forwards it to `agent_tunnel::routing::try_route`. When the parent association pins `jet_agent_id`, the KDC sub-traffic is routed via that agent or fails -- never silently falls back to a different agent or to direct connect. The HTTP handler passes `None`. Hardening (came along since they live in the same file): - 64 KiB `MAX_KDC_REPLY_MESSAGE_LEN` DoS cap on the announced TCP-framed KDC reply length, with overflow-safe length math. - UDP scheme guard: KDC over UDP keeps going direct because the agent tunnel only carries TCP today. Drive-by: `crates/agent-tunnel/src/listener.rs` move-after-move on `ca_manager` introduced by #1775 -- fixed with `Arc::clone` to keep master building on `--no-default-features` configurations. Stack: based on #1741. Picks up `agent_tunnel::routing::try_route`. Issue: DGW-384
When an agent advertises the KDC's subnet or DNS domain, route Kerberos traffic through the QUIC tunnel just like every other proxy path. This closes the last gap left after the transparent routing PR (#1741). Two paths now use the same routing pipeline as connection forwarding: - `/jet/KdcProxy` HTTP endpoint — `send_krb_message` consults the routing pipeline before falling back to direct TCP. - RDP CredSSP/NLA — `rdp_proxy.rs::send_network_request` previously hard-coded `None` for the agent handle. Plumb `agent_tunnel_handle`, `session_id`, and `explicit_agent_id` from `RdpProxy` down through `perform_credssp_as_*` -> `resolve_*_generator` -> `send_network_request`. The same change reaches the credential-injection clean path (`rd_clean_path.rs`). Session correlation: - RDP CredSSP callers pass the parent association's `jet_aid` so KDC sub-traffic ties back to its parent RDP session in agent-side logs. - The HTTP `/jet/KdcProxy` handler passes the KDC token's own `jti` (the most persistent identifier available without a parent association). `KdcToken` now carries `jti` alongside the claims for this purpose. Explicit-agent routing (matches every other proxy path): - `send_krb_message` takes `explicit_agent_id: Option<Uuid>` and forwards it to `agent_tunnel::routing::try_route`. When the parent association pins `jet_agent_id`, the KDC sub-traffic is routed via that agent or fails -- never silently falls back to a different agent or to direct connect. The HTTP handler passes `None`. Hardening (came along since they live in the same file): - 64 KiB `MAX_KDC_REPLY_MESSAGE_LEN` DoS cap on the announced TCP-framed KDC reply length, with overflow-safe length math. - UDP scheme guard: KDC over UDP keeps going direct because the agent tunnel only carries TCP today. Drive-by: `crates/agent-tunnel/src/listener.rs` move-after-move on `ca_manager` introduced by #1775 -- fixed with `Arc::clone` to keep master building on `--no-default-features` configurations. Stack: based on #1741. Picks up `agent_tunnel::routing::try_route`. Issue: DGW-384
When an agent advertises the KDC's subnet or DNS domain, route Kerberos traffic through the QUIC tunnel just like every other proxy path. Closes the last gap left after the transparent routing PR (#1741). Two paths now use the same routing pipeline as connection forwarding: - `/jet/KdcProxy` HTTP endpoint -- the handler builds a `KdcConnector` and forwards through it. When an agent advertises the KDC subnet, the request goes through the agent tunnel; otherwise it falls back to a direct TCP/UDP connection. - RDP CredSSP/NLA -- `rdp_proxy.rs::send_network_request` previously hard-coded `None` for the agent handle. `RdpProxy` now carries a `KdcConnector` field that the CredSSP machinery (`perform_credssp_as_*` -> `resolve_*_generator` -> `send_network_request`) uses for every Kerberos sub-request. The same change reaches the credential-injection clean path (`rd_clean_path.rs`). `KdcConnector` (new `src/kdc_connector.rs`) encapsulates the routing decision behind a single value so callers no longer thread `agent_tunnel_handle`, `session_id`, and `explicit_agent_id` through every layer. CredSSP code only sees `&KdcConnector`. Session correlation: - RDP CredSSP callers build `KdcConnector::agent_tunnel(claims.jet_aid, ...)` so KDC sub-traffic ties back to its parent RDP session in agent-side logs. - The HTTP `/jet/KdcProxy` handler builds `KdcConnector::agent_tunnel(claims.jti, ...)` so all sub-requests of the same KDC token share a correlation ID. `KdcTokenClaims` now exposes `jti` through its serde helper (matching how every other `*TokenClaims` type surfaces `jti`). Explicit-agent routing (matches every other proxy path): - The `AgentTunnel` variant of `KdcConnector` carries an `explicit_agent_id: Option<Uuid>`. When the parent association pins `jet_agent_id`, KDC traffic must route via that agent or fail -- never silently fall back to a different agent or to direct connect. The HTTP handler passes `None` (it has no parent association). Hardening (came along since they share the file): - 64 KiB `MAX_KDC_REPLY_MESSAGE_LEN` DoS cap on the announced TCP-framed KDC reply length, with overflow-safe length math. - UDP scheme guard: KDC over UDP keeps going direct because the agent tunnel only carries TCP today. Issue: DGW-384
When an agent advertises the KDC's subnet or DNS domain, route Kerberos traffic through the QUIC tunnel just like every other proxy path. Closes the last gap left after the transparent routing PR (#1741). Two paths now use the same routing pipeline as connection forwarding: - `/jet/KdcProxy` HTTP endpoint -- the handler builds a `KdcConnector` and forwards through it. When an agent advertises the KDC subnet, the request goes through the agent tunnel; otherwise it falls back to a direct TCP/UDP connection. - RDP CredSSP/NLA -- `rdp_proxy.rs::send_network_request` previously hard-coded `None` for the agent handle. `RdpProxy` now carries a `KdcConnector` field that the CredSSP machinery (`perform_credssp_as_*` -> `resolve_*_generator` -> `send_network_request`) uses for every Kerberos sub-request. The same change reaches the credential-injection clean path (`rd_clean_path.rs`). `KdcConnector` (new `src/kdc_connector.rs`) bundles the three inputs the routing pipeline needs (`session_id`, `explicit_agent_id`, `agent_tunnel_handle`) into a single value and always defers the routing decision to `agent_tunnel::routing::try_route`. Callers never pre-decide "direct" vs "via tunnel": the routing pipeline does, and its existing `explicit_agent_id` enforcement (pin without tunnel handle must error, never silently fall back to direct) is preserved end-to-end. Session correlation: - RDP CredSSP callers pass the parent association's `claims.jet_aid` as `session_id`, so KDC sub-traffic ties back to its parent RDP session in agent-side logs. - The HTTP `/jet/KdcProxy` handler passes the KDC token's own `jti`, the most persistent identifier available without a parent association. `KdcTokenClaims` now exposes `jti` through its serde helper, matching how every other `*TokenClaims` type surfaces `jti`. Explicit-agent routing (matches every other proxy path): - The parent association's `jet_agent_id`, when set, is forwarded to `try_route`. KDC traffic must route via that agent or fail -- never silently fall back to a different agent or to direct connect. The HTTP handler passes `None` (no parent association). - A new UDP-via-agent guard rejects `udp://` KDC targets whenever the routing pipeline selects an agent. Without it, an explicit `jet_agent_id` pin could be downgraded to direct UDP, since the agent tunnel currently carries only TCP. Hardening (came along since they share the file): - 64 KiB `MAX_KDC_REPLY_MESSAGE_LEN` DoS cap on the announced TCP-framed KDC reply length, with overflow-safe length math. - UDP scheme guard at the direct-connect branch (preserved). Tests: - `kdc_connector` unit tests pin the routing-decision contract: pin-without-tunnel must error, no-pin-no-tunnel directs, pin-with- missing-agent errors, no-match falls back to direct. The success path and the UDP-via-agent guard both require a live `connect_via_agent` fixture and are tracked as follow-up TODOs in the same file. - A `pub(hidden) AgentTunnelHandle::for_testing(registry, ca_manager)` constructor was added to the `agent-tunnel` crate so the gateway crate's tests can build a handle without standing up a real QUIC endpoint. Issue: DGW-384
When an agent advertises the KDC's subnet or DNS domain, route Kerberos traffic through the QUIC tunnel just like every other proxy path. Closes the last gap left after the transparent routing PR (#1741). Two paths now use the same routing pipeline as connection forwarding: - `/jet/KdcProxy` HTTP endpoint -- the handler builds a `KdcConnector` and forwards through it. When an agent advertises the KDC subnet, the request goes through the agent tunnel; otherwise it falls back to a direct TCP/UDP connection. - RDP CredSSP/NLA -- `rdp_proxy.rs::send_network_request` previously hard-coded `None` for the agent handle. `RdpProxy` now carries a `KdcConnector` field that the CredSSP machinery (`perform_credssp_as_*` -> `resolve_*_generator` -> `send_network_request`) uses for every Kerberos sub-request. The same change reaches the credential-injection clean path (`rd_clean_path.rs`). `KdcConnector` (new `src/kdc_connector.rs`) bundles the three inputs the routing pipeline needs (`session_id`, `explicit_agent_id`, `agent_tunnel_handle`) into a single value and always defers the routing decision to `agent_tunnel::routing::try_route`. Callers never pre-decide "direct" vs "via tunnel": the routing pipeline does, and its existing `explicit_agent_id` enforcement (pin without tunnel handle must error, never silently fall back to direct) is preserved end-to-end. Session correlation: - RDP CredSSP callers pass the parent association's `claims.jet_aid` as `session_id`, so KDC sub-traffic ties back to its parent RDP session in agent-side logs. - The HTTP `/jet/KdcProxy` handler passes the KDC token's own `jti`, the most persistent identifier available without a parent association. `KdcTokenClaims` now exposes `jti` through its serde helper, matching how every other `*TokenClaims` type surfaces `jti`. Explicit-agent routing (matches every other proxy path): - The parent association's `jet_agent_id`, when set, is forwarded to `try_route`. KDC traffic must route via that agent or fail -- never silently fall back to a different agent or to direct connect. The HTTP handler passes `None` (no parent association). - A new UDP-via-agent guard rejects `udp://` KDC targets whenever the routing pipeline selects an agent. Without it, an explicit `jet_agent_id` pin could be downgraded to direct UDP, since the agent tunnel currently carries only TCP. Hardening (came along since they share the file): - 64 KiB `MAX_KDC_REPLY_MESSAGE_LEN` DoS cap on the announced TCP-framed KDC reply length, with overflow-safe length math. - UDP scheme guard at the direct-connect branch (preserved). Tests: - `kdc_connector` unit tests pin the routing-decision contract: pin-without-tunnel must error, no-pin-no-tunnel directs, pin-with- missing-agent errors, no-match falls back to direct. The success path and the UDP-via-agent guard both require a live `connect_via_agent` fixture and are tracked as follow-up TODOs in the same file. - A `pub(hidden) AgentTunnelHandle::for_testing(registry, ca_manager)` constructor was added to the `agent-tunnel` crate so the gateway crate's tests can build a handle without standing up a real QUIC endpoint. Issue: DGW-384
When an agent advertises the KDC's subnet or DNS domain, route Kerberos traffic through the QUIC tunnel just like every other proxy path. Closes the last gap left after the transparent routing PR (#1741). Two paths now use the same routing pipeline as connection forwarding: - `/jet/KdcProxy` HTTP endpoint -- the handler builds a `KdcConnector` and forwards through it. When an agent advertises the KDC subnet, the request goes through the agent tunnel; otherwise it falls back to a direct TCP/UDP connection. - RDP CredSSP/NLA -- `rdp_proxy.rs::send_network_request` previously hard-coded `None` for the agent handle. `RdpProxy` now carries a `KdcConnector` field that the CredSSP machinery (`perform_credssp_as_*` -> `resolve_*_generator` -> `send_network_request`) uses for every Kerberos sub-request. The same change reaches the credential-injection clean path (`rd_clean_path.rs`). `KdcConnector` (new `src/kdc_connector.rs`) bundles the three inputs the routing pipeline needs (`session_id`, `explicit_agent_id`, `agent_tunnel_handle`) into a single value and always defers the routing decision to `agent_tunnel::routing::try_route`. Callers never pre-decide "direct" vs "via tunnel": the routing pipeline does, and its existing `explicit_agent_id` enforcement (pin without tunnel handle must error, never silently fall back to direct) is preserved end-to-end. Session correlation: - RDP CredSSP callers pass the parent association's `claims.jet_aid` as `session_id`, so KDC sub-traffic ties back to its parent RDP session in agent-side logs. - The HTTP `/jet/KdcProxy` handler passes the KDC token's own `jti`, the most persistent identifier available without a parent association. `KdcTokenClaims` now exposes `jti` through its serde helper, matching how every other `*TokenClaims` type surfaces `jti`. Explicit-agent routing (matches every other proxy path): - The parent association's `jet_agent_id`, when set, is forwarded to `try_route`. KDC traffic must route via that agent or fail -- never silently fall back to a different agent or to direct connect. The HTTP handler passes `None` (no parent association). - A new UDP-via-agent guard rejects `udp://` KDC targets whenever the routing pipeline selects an agent. Without it, an explicit `jet_agent_id` pin could be downgraded to direct UDP, since the agent tunnel currently carries only TCP. Hardening (came along since they share the file): - 64 KiB `MAX_KDC_REPLY_MESSAGE_LEN` DoS cap on the announced TCP-framed KDC reply length, with overflow-safe length math. - UDP scheme guard at the direct-connect branch (preserved). Tests: - `kdc_connector` unit tests pin the routing-decision contract: pin-without-tunnel must error, no-pin-no-tunnel directs, pin-with- missing-agent errors, no-match falls back to direct. The success path and the UDP-via-agent guard both require a live `connect_via_agent` fixture and are tracked as follow-up TODOs in the same file. - A `pub(hidden) AgentTunnelHandle::for_testing(registry, ca_manager)` constructor was added to the `agent-tunnel` crate so the gateway crate's tests can build a handle without standing up a real QUIC endpoint. Issue: DGW-384
When an agent advertises the KDC's subnet or DNS domain, route Kerberos traffic through the QUIC tunnel just like every other proxy path. Closes the last gap left after the transparent routing PR (#1741). Two paths now use the same routing pipeline as connection forwarding: - `/jet/KdcProxy` HTTP endpoint -- the handler builds a `KdcConnector` and forwards through it. When an agent advertises the KDC subnet, the request goes through the agent tunnel; otherwise it falls back to a direct TCP/UDP connection. - RDP CredSSP/NLA -- `rdp_proxy.rs::send_network_request` previously hard-coded `None` for the agent handle. `RdpProxy` now carries a `KdcConnector` field that the CredSSP machinery (`perform_credssp_as_*` -> `resolve_*_generator` -> `send_network_request`) uses for every Kerberos sub-request. The same change reaches the credential-injection clean path (`rd_clean_path.rs`). `KdcConnector` (new `src/kdc_connector.rs`) bundles the three inputs the routing pipeline needs (`session_id`, `explicit_agent_id`, `agent_tunnel_handle`) into a single value and always defers the routing decision to `agent_tunnel::routing::try_route`. Callers never pre-decide "direct" vs "via tunnel": the routing pipeline does, and its existing `explicit_agent_id` enforcement (pin without tunnel handle must error, never silently fall back to direct) is preserved end-to-end. Session correlation: - RDP CredSSP callers pass the parent association's `claims.jet_aid` as `session_id`, so KDC sub-traffic ties back to its parent RDP session in agent-side logs. - The HTTP `/jet/KdcProxy` handler passes the KDC token's own `jti`, the most persistent identifier available without a parent association. `KdcTokenClaims` now exposes `jti` through its serde helper, matching how every other `*TokenClaims` type surfaces `jti`. Explicit-agent routing (matches every other proxy path): - The parent association's `jet_agent_id`, when set, is forwarded to `try_route`. KDC traffic must route via that agent or fail -- never silently fall back to a different agent or to direct connect. The HTTP handler passes `None` (no parent association). - A new UDP-via-agent guard rejects `udp://` KDC targets whenever the routing pipeline selects an agent. Without it, an explicit `jet_agent_id` pin could be downgraded to direct UDP, since the agent tunnel currently carries only TCP. Hardening (came along since they share the file): - 64 KiB `MAX_KDC_REPLY_MESSAGE_LEN` DoS cap on the announced TCP-framed KDC reply length, with overflow-safe length math. - UDP scheme guard at the direct-connect branch (preserved). Tests: - `kdc_connector` unit tests cover the two cases that don't need a live `AgentTunnelHandle`: pin-without-tunnel must error, no-pin-no-tunnel falls through to direct. The remaining cases (pin-with-missing-agent, no-match-falls-back, tunnel success, UDP-via-agent guard) need an integration-style listener fixture and are left as a follow-up. Issue: DGW-384
When an agent advertises the KDC's subnet or DNS domain, route Kerberos traffic through the QUIC tunnel just like every other proxy path. Closes the last gap left after the transparent routing PR (#1741). Two paths now use the same routing pipeline as connection forwarding: - `/jet/KdcProxy` HTTP endpoint -- the handler builds a `KdcConnector` and forwards through it. When an agent advertises the KDC subnet, the request goes through the agent tunnel; otherwise it falls back to a direct TCP/UDP connection. - RDP CredSSP/NLA -- `rdp_proxy.rs::send_network_request` previously hard-coded `None` for the agent handle. `RdpProxy` now carries a `KdcConnector` field that the CredSSP machinery (`perform_credssp_as_*` -> `resolve_*_generator` -> `send_network_request`) uses for every Kerberos sub-request. The same change reaches the credential-injection clean path (`rd_clean_path.rs`). `KdcConnector` (new `src/kdc_connector.rs`) bundles the three inputs the routing pipeline needs (`session_id`, `explicit_agent_id`, `agent_tunnel_handle`) into a single value and always defers the routing decision to `agent_tunnel::routing::try_route`. Callers never pre-decide "direct" vs "via tunnel": the routing pipeline does, and its existing `explicit_agent_id` enforcement (pin without tunnel handle must error, never silently fall back to direct) is preserved end-to-end. Session correlation: - RDP CredSSP callers pass the parent association's `claims.jet_aid` as `session_id`, so KDC sub-traffic ties back to its parent RDP session in agent-side logs. - The HTTP `/jet/KdcProxy` handler passes the KDC token's own `jti`, the most persistent identifier available without a parent association. `KdcTokenClaims` now exposes `jti` through its serde helper, matching how every other `*TokenClaims` type surfaces `jti`. Explicit-agent routing (matches every other proxy path): - The parent association's `jet_agent_id`, when set, is forwarded to `try_route`. KDC traffic must route via that agent or fail -- never silently fall back to a different agent or to direct connect. The HTTP handler passes `None` (no parent association). - A new UDP-via-agent guard rejects `udp://` KDC targets whenever the routing pipeline selects an agent. Without it, an explicit `jet_agent_id` pin could be downgraded to direct UDP, since the agent tunnel currently carries only TCP. Hardening (came along since they share the file): - 64 KiB `MAX_KDC_REPLY_MESSAGE_LEN` DoS cap on the announced TCP-framed KDC reply length, with overflow-safe length math. - UDP scheme guard at the direct-connect branch (preserved). Tests: - `kdc_connector` unit tests cover the two cases that don't need a live `AgentTunnelHandle`: pin-without-tunnel must error, no-pin-no-tunnel falls through to direct. The remaining cases (pin-with-missing-agent, no-match-falls-back, tunnel success, UDP-via-agent guard) need an integration-style listener fixture and are left as a follow-up. Issue: DGW-384
When an agent advertises the KDC's subnet or DNS domain, route Kerberos traffic through the QUIC tunnel just like every other proxy path. Closes the last gap left after the transparent routing PR (#1741). Two paths now use the same routing pipeline as connection forwarding: - `/jet/KdcProxy` HTTP endpoint -- the handler builds a `KdcConnector` and forwards through it. When an agent advertises the KDC subnet, the request goes through the agent tunnel; otherwise it falls back to a direct TCP/UDP connection. - RDP CredSSP/NLA -- `rdp_proxy.rs::send_network_request` previously hard-coded `None` for the agent handle. `RdpProxy` now carries a `KdcConnector` field that the CredSSP machinery (`perform_credssp_as_*` -> `resolve_*_generator` -> `send_network_request`) uses for every Kerberos sub-request. The same change reaches the credential-injection clean path (`rd_clean_path.rs`). `KdcConnector` (new `src/kdc_connector.rs`) bundles the three inputs the routing pipeline needs (`session_id`, `explicit_agent_id`, `agent_tunnel_handle`) into a single value and always defers the routing decision to `agent_tunnel::routing::try_route`. Callers never pre-decide "direct" vs "via tunnel": the routing pipeline does, and its existing `explicit_agent_id` enforcement (pin without tunnel handle must error, never silently fall back to direct) is preserved end-to-end. Session correlation: - RDP CredSSP callers pass the parent association's `claims.jet_aid` as `session_id`, so KDC sub-traffic ties back to its parent RDP session in agent-side logs. - The HTTP `/jet/KdcProxy` handler passes the KDC token's own `jti`, the most persistent identifier available without a parent association. `KdcTokenClaims` now exposes `jti` through its serde helper, matching how every other `*TokenClaims` type surfaces `jti`. Explicit-agent routing (matches every other proxy path): - The parent association's `jet_agent_id`, when set, is forwarded to `try_route`. KDC traffic must route via that agent or fail -- never silently fall back to a different agent or to direct connect. The HTTP handler passes `None` (no parent association). - A new UDP-via-agent guard rejects `udp://` KDC targets whenever the routing pipeline selects an agent. Without it, an explicit `jet_agent_id` pin could be downgraded to direct UDP, since the agent tunnel currently carries only TCP. Hardening (came along since they share the file): - 64 KiB `MAX_KDC_REPLY_MESSAGE_LEN` DoS cap on the announced TCP-framed KDC reply length, with overflow-safe length math. - UDP scheme guard at the direct-connect branch (preserved). Tests: - `kdc_connector` unit tests cover the two cases that don't need a live `AgentTunnelHandle`: pin-without-tunnel must error, no-pin-no-tunnel falls through to direct. The remaining cases (pin-with-missing-agent, no-match-falls-back, tunnel success, UDP-via-agent guard) need an integration-style listener fixture and are left as a follow-up. Issue: DGW-384
Summary
Transparent routing through QUIC agent tunnel (PR 2 of 4, stacked on #1738).
When a connection target matches an agent's advertised subnets or domains, the gateway automatically routes through the QUIC tunnel instead of connecting directly.
Depends on: #1738 (must merge first)
Changes
ServerTransportenum (Tcp/Quic) inrd_clean_path.rsfor RDP tunnel supportPR stack
🤖 Generated with Claude Code