Skip to content

feat(dgw): route KDC traffic through agent tunnel#1792

Merged
irvingouj@Devolutions (irvingoujAtDevolution) merged 2 commits into
masterfrom
feat/DGW-384-kdc-agent-tunnel-v2
May 22, 2026
Merged

feat(dgw): route KDC traffic through agent tunnel#1792
irvingouj@Devolutions (irvingoujAtDevolution) merged 2 commits into
masterfrom
feat/DGW-384-kdc-agent-tunnel-v2

Conversation

@irvingoujAtDevolution
Copy link
Copy Markdown
Contributor

@irvingoujAtDevolution irvingouj@Devolutions (irvingoujAtDevolution) commented May 21, 2026

Closes DGW-384. Replaces #1781 (reverted in #1791).

Routes KDC traffic through the agent tunnel for the two paths still missing after #1741:

  • HTTP /jet/KdcProxy
  • RDP CredSSP/NLA (rdp_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 to agent_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_*_generatorsend_network_request) now takes a single &KdcConnector instead of three primitives.

Session correlation

  • RDP CredSSP uses the parent association's claims.jet_aid.
  • HTTP /jet/KdcProxy uses the KDC token's own jti. KdcTokenClaims now exposes jti via its serde helper, matching every other *TokenClaims type. KdcToken stays a tuple struct.

Guards

  • explicit_agent_id (from jet_agent_id) is forwarded to try_route, so a token-pinned session can never silently fall back to a different agent or to direct connect.
  • UDP-via-agent rejection: when try_route selects an agent for a udp:// target, the connector errors instead of silently downgrading (the tunnel only carries TCP today).
  • 64 KiB cap on TCP-framed KDC reply length, with overflow-safe math.

Tests

kdc_connector::tests covers the two cases that don't need a live AgentTunnelHandle: 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 ConnectRequest wire protocol unchanged. DVLS-side: no action required.

Issue: DGW-384

@github-actions
Copy link
Copy Markdown

Let maintainers know that an action is required on their side

  • Add the label release-required Please cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module) when you request a maintainer to cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module)

  • Add the label release-blocker Follow-up is required before cutting a new release if a follow-up is required before cutting a new release

  • Add the label publish-required Please publish libraries (`Devolutions.Gateway.Utils`, OpenAPI clients, etc) when you request a maintainer to publish libraries (Devolutions.Gateway.Utils, OpenAPI clients, etc.)

  • Add the label publish-blocker Follow-up is required before publishing libraries if a follow-up is required before publishing libraries

@irvingoujAtDevolution irvingouj@Devolutions (irvingoujAtDevolution) force-pushed the feat/DGW-384-kdc-agent-tunnel-v2 branch 4 times, most recently from 73722a3 to 4d0550c Compare May 22, 2026 14:49
@irvingoujAtDevolution irvingouj@Devolutions (irvingoujAtDevolution) marked this pull request as ready for review May 22, 2026 14:49
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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 KdcConnector to centralize KDC routing inputs (session_id, explicit_agent_id, agent_tunnel_handle) and delegate routing decisions to try_route.
  • Wires CredSSP KDC network dispatch (RDP proxy and clean-path credential injection) through KdcConnector.
  • Exposes KDC token jti in KdcTokenClaims and uses it as the HTTP /jet/KdcProxy session 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 if message.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.

Comment thread devolutions-gateway/src/token.rs
Comment thread devolutions-gateway/src/kdc_connector.rs Outdated
Comment thread devolutions-gateway/src/kdc_connector.rs Outdated
Copy link
Copy Markdown
Member

@CBenoit Benoît Cortier (CBenoit) left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Address Copilot suggestions and feel free to merge!

@irvingoujAtDevolution irvingouj@Devolutions (irvingoujAtDevolution) merged commit 45de0e6 into master May 22, 2026
42 checks passed
@irvingoujAtDevolution irvingouj@Devolutions (irvingoujAtDevolution) deleted the feat/DGW-384-kdc-agent-tunnel-v2 branch May 22, 2026 19:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants