Skip to content

feat(agent): transparent routing through agent tunnel#1741

Merged
Benoît Cortier (CBenoit) merged 29 commits into
masterfrom
feat/quic-tunnel-2-routing
May 13, 2026
Merged

feat(agent): transparent routing through agent tunnel#1741
Benoît Cortier (CBenoit) merged 29 commits into
masterfrom
feat/quic-tunnel-2-routing

Conversation

@irvingoujAtDevolution
Copy link
Copy Markdown
Contributor

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

  • Routing pipeline: explicit agent_id → subnet match → domain suffix (longest wins) → direct
  • Integrated into all proxy paths: RDP (clean path), SSH, VNC, ARD, KDC proxy
  • ServerTransport enum (Tcp/Quic) in rd_clean_path.rs for RDP tunnel support
  • 7 routing unit tests

PR stack

  1. Protocol + Tunnel Core (feat: initial implementation of QUIC agent tunnel #1738)
  2. Transparent Routing (this PR)
  3. Auth + Webapp
  4. Deployment + Installer

🤖 Generated with Claude Code

@irvingoujAtDevolution
Copy link
Copy Markdown
Contributor Author

⚠️ Not ready to merge — depends on #1738. Will rebase and mark ready once #1738 is merged.

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 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 ServerTransport enum (to preserve Send).

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.

Comment thread crates/agent-tunnel/src/routing.rs Outdated
Comment thread devolutions-gateway/src/generic_client.rs Outdated
Comment thread devolutions-gateway/src/api/fwd.rs Outdated
Comment thread devolutions-gateway/src/rdp_proxy.rs
Comment thread devolutions-gateway/src/rd_clean_path.rs Outdated
Comment thread devolutions-gateway/src/agent_tunnel/routing.rs Outdated
Comment thread devolutions-gateway/src/agent_tunnel/registry.rs
Comment thread crates/agent-tunnel-proto/src/control.rs
Comment thread devolutions-gateway/src/api/kdc_proxy.rs Outdated
Base automatically changed from feat/quic-tunnel-1-core to master April 21, 2026 16:44
@irvingoujAtDevolution irvingouj@Devolutions (irvingoujAtDevolution) force-pushed the feat/quic-tunnel-2-routing branch 2 times, most recently from f323f30 to 3c49f7f Compare April 21, 2026 18:38
irvingouj@Devolutions (irvingoujAtDevolution) added a commit that referenced this pull request Apr 22, 2026
- 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>`).
irvingouj@Devolutions (irvingoujAtDevolution) added a commit that referenced this pull request Apr 22, 2026
- 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>`).
@irvingoujAtDevolution irvingouj@Devolutions (irvingoujAtDevolution) marked this pull request as ready for review April 27, 2026 15:20
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

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.

Comment thread devolutions-gateway/src/api/tunnel.rs Outdated
Comment thread devolutions-gateway/src/api/tunnel.rs Outdated
Comment thread devolutions-agent/src/tunnel.rs Outdated
Comment thread devolutions-agent/src/tunnel.rs Outdated
Comment thread devolutions-agent/src/tunnel.rs Outdated
irvingouj@Devolutions (irvingoujAtDevolution) added a commit that referenced this pull request Apr 27, 2026
- 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>`).
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

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.

Comment thread devolutions-gateway/src/api/kdc_proxy.rs Outdated
Comment thread devolutions-gateway/src/api/tunnel.rs Outdated
Comment thread devolutions-gateway/tests/agent_tunnel_integration.rs Outdated
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

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.

Comment thread devolutions-gateway/src/rd_clean_path.rs
Comment thread devolutions-gateway/src/api/fwd.rs
Comment thread utils/dotnet/Devolutions.Gateway.Utils.Tests/JsonSerializationTests.cs Outdated
Comment thread crates/agent-tunnel/src/registry.rs
Comment thread crates/agent-tunnel/src/routing.rs Outdated
Comment thread enroll.nu Outdated
Comment thread utils/dotnet/Devolutions.Gateway.Utils/src/EnrollmentClaims.cs Outdated
Comment thread testsuite/tests/agent_tunnel/routing.rs Outdated
Comment thread crates/agent-tunnel/src/registry.rs
}
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

mistake, will fix

Comment thread devolutions-gateway/src/rdp_proxy.rs Outdated
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
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.
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.
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

Let’s iterate on that in follow up PRs as we discussed on Slack

@CBenoit Benoît Cortier (CBenoit) merged commit 9340a82 into master May 13, 2026
42 checks passed
@CBenoit Benoît Cortier (CBenoit) deleted the feat/quic-tunnel-2-routing branch May 13, 2026 15:19
irvingouj@Devolutions (irvingoujAtDevolution) added a commit that referenced this pull request May 13, 2026
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.
irvingouj@Devolutions (irvingoujAtDevolution) added a commit that referenced this pull request May 19, 2026
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.
irvingouj@Devolutions (irvingoujAtDevolution) added a commit that referenced this pull request May 19, 2026
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.
irvingouj@Devolutions (irvingoujAtDevolution) added a commit that referenced this pull request May 21, 2026
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
irvingouj@Devolutions (irvingoujAtDevolution) added a commit that referenced this pull request May 21, 2026
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
irvingouj@Devolutions (irvingoujAtDevolution) added a commit that referenced this pull request May 21, 2026
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
irvingouj@Devolutions (irvingoujAtDevolution) added a commit that referenced this pull request May 21, 2026
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
irvingouj@Devolutions (irvingoujAtDevolution) added a commit that referenced this pull request May 22, 2026
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
irvingouj@Devolutions (irvingoujAtDevolution) added a commit that referenced this pull request May 22, 2026
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
irvingouj@Devolutions (irvingoujAtDevolution) added a commit that referenced this pull request May 22, 2026
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
irvingouj@Devolutions (irvingoujAtDevolution) added a commit that referenced this pull request May 22, 2026
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
irvingouj@Devolutions (irvingoujAtDevolution) added a commit that referenced this pull request May 22, 2026
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
irvingouj@Devolutions (irvingoujAtDevolution) added a commit that referenced this pull request May 22, 2026
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
irvingouj@Devolutions (irvingoujAtDevolution) added a commit that referenced this pull request May 22, 2026
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
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