feat(dgw): route KDC traffic through agent tunnel#1792
Conversation
Let maintainers know that an action is required on their side
|
73722a3 to
4d0550c
Compare
4d0550c to
2289d96
Compare
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
2289d96 to
2dc8a62
Compare
There was a problem hiding this comment.
Pull request overview
Adds a unified KDC outbound dispatcher (KdcConnector) so Kerberos traffic from both /jet/KdcProxy and RDP CredSSP/NLA can be routed through the agent tunnel via the existing agent_tunnel::routing::try_route pipeline, preserving explicit agent pinning behavior.
Changes:
- Introduces
KdcConnectorto centralize KDC routing inputs (session_id,explicit_agent_id,agent_tunnel_handle) and delegate routing decisions totry_route. - Wires CredSSP KDC network dispatch (RDP proxy and clean-path credential injection) through
KdcConnector. - Exposes KDC token
jtiinKdcTokenClaimsand uses it as the HTTP/jet/KdcProxysession correlation ID.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| devolutions-gateway/src/upstream.rs | Exposes route_target_from_target_addr for reuse by KdcConnector. |
| devolutions-gateway/src/token.rs | Adds jti to KdcTokenClaims and plumbs it through custom serde helper. |
| devolutions-gateway/src/api/webapp.rs | Populates jti when minting KDC session tokens. |
| devolutions-gateway/src/api/kdc_proxy.rs | Routes real-KDC forwarding via KdcConnector and uses token jti as session id. |
| devolutions-gateway/src/kdc_connector.rs | New module implementing agent-tunnel-aware KDC send logic + reply-length cap. |
| devolutions-gateway/src/rdp_proxy.rs | Replaces direct KDC dispatch with KdcConnector for CredSSP network requests. |
| devolutions-gateway/src/rd_clean_path.rs | Constructs and passes KdcConnector through credential-injection CredSSP flows. |
| devolutions-gateway/src/generic_client.rs | Adds KdcConnector to RdpProxy construction for credential injection mode. |
| devolutions-gateway/src/lib.rs | Exposes the new kdc_connector module. |
Comments suppressed due to low confidence (1)
devolutions-gateway/src/kdc_connector.rs:149
send_to(&message[4..], ...)will panic ifmessage.len() < 4. Even if callers normally provide a TCP-framed Kerberos message, this is an unchecked assumption that can crash the process. Add a length check and return a structured error when the message is too short to contain the length prefix.
// First 4 bytes contains message length. We don't need it for UDP.
#[allow(clippy::redundant_closure)] // We get a better caller location for the error by using a closure.
udp_socket
.send_to(&message[4..], kdc_addr.as_addr())
.await
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Benoît Cortier (CBenoit)
left a comment
There was a problem hiding this comment.
LGTM! Address Copilot suggestions and feel free to merge!
45de0e6
into
master
Closes DGW-384. Replaces #1781 (reverted in #1791).
Routes KDC traffic through the agent tunnel for the two paths still missing after #1741:
/jet/KdcProxyrdp_proxy.rs::send_network_request)How
A new
KdcConnector(src/kdc_connector.rs) bundles the three routing inputs —session_id,explicit_agent_id,agent_tunnel_handle— into one value and delegates every send toagent_tunnel::routing::try_route. Callers no longer pre-decide direct vs. tunnel; the routing pipeline decides, and its existing pin-without-tunnel enforcement is preserved.CredSSP plumbing (
perform_credssp_as_*→resolve_*_generator→send_network_request) now takes a single&KdcConnectorinstead of three primitives.Session correlation
claims.jet_aid./jet/KdcProxyuses the KDC token's ownjti.KdcTokenClaimsnow exposesjtivia its serde helper, matching every other*TokenClaimstype.KdcTokenstays a tuple struct.Guards
explicit_agent_id(fromjet_agent_id) is forwarded totry_route, so a token-pinned session can never silently fall back to a different agent or to direct connect.try_routeselects an agent for audp://target, the connector errors instead of silently downgrading (the tunnel only carries TCP today).Tests
kdc_connector::testscovers the two cases that don't need a liveAgentTunnelHandle: pin-without-tunnel (must error) and 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 fixture that stands up a real listener; left as a follow-up.No-op for external consumers
JWT/wire format unchanged. Agent-tunnel
ConnectRequestwire protocol unchanged. DVLS-side: no action required.Issue: DGW-384