Skip to content

feat: auth, enrollment API, and webapp for agent tunnel#1742

Draft
irvingouj@Devolutions (irvingoujAtDevolution) wants to merge 11 commits intofeat/quic-tunnel-2-routingfrom
feat/quic-tunnel-3-auth-webapp
Draft

feat: auth, enrollment API, and webapp for agent tunnel#1742
irvingouj@Devolutions (irvingoujAtDevolution) wants to merge 11 commits intofeat/quic-tunnel-2-routingfrom
feat/quic-tunnel-3-auth-webapp

Conversation

@irvingoujAtDevolution
Copy link
Copy Markdown
Contributor

Summary

Auth layer, enrollment string generation, and Angular webapp for agent tunnel management (PR 3 of 4, stacked on #1741).

Depends on: #1741 (must merge first)

Changes

Backend:

  • POST /enrollment-string endpoint — generates enrollment string with QUIC endpoint
  • Scope token exchange for agent management API calls
  • QUIC endpoint override from enrollment string (fixes Docker container ID issue)

Frontend:

  • Agents page with enrollment form and agent list
  • Agent selector control in RDP/SSH/VNC/ARD connection forms
  • Auth interceptor fix (preserves existing Authorization headers)

PR stack

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

🤖 Generated with Claude Code

@irvingoujAtDevolution
Copy link
Copy Markdown
Contributor Author

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

Add QUIC-based agent tunnel core infrastructure. Agents in private
networks connect outbound to Gateway via QUIC/mTLS, advertise reachable
subnets and domains, and proxy TCP connections on behalf of Gateway.

Protocol (agent-tunnel-proto crate):
- RouteAdvertise with subnets + domain advertisements
- ConnectMessage/ConnectResponse for session stream setup
- Heartbeat/HeartbeatAck for liveness detection
- Protocol version negotiation (v2)

Gateway (agent_tunnel module):
- QUIC listener with mTLS authentication
- Agent registry with subnet/domain tracking
- Certificate authority for agent enrollment
- Enrollment token store (one-time tokens)
- Bidirectional proxy stream multiplexing

Agent (devolutions-agent):
- QUIC client with auto-reconnect and exponential backoff
- Agent enrollment with config merge (preserves existing settings)
- Domain auto-detection (Windows: USERDNSDOMAIN, Linux: resolv.conf)
- Subnet validation on incoming connections
- Certificate file permissions (0o600 on Unix)

API endpoints:
- POST /jet/agent-tunnel/enroll — agent enrollment
- GET /jet/agent-tunnel/agents — list agents
- GET /jet/agent-tunnel/agents/{id} — get agent
- DELETE /jet/agent-tunnel/agents/{id} — delete agent
- POST /jet/agent-tunnel/agents/resolve-target — routing diagnostics

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ConnectMessage → ConnectRequest (precise naming)
- Move encode/decode into ControlStream/SessionStream wrappers
  (actor-on-object: ctrl.send(&msg) instead of msg.encode(&mut stream))
- ControlStream.into_split() → ControlSendStream + ControlRecvStream
  (compile-time separation, no phantom halves)
- From<(S, R)> for stream wrappers (connection.open_bi().await?.into())
- Rename spawned tasks: run_control_reader, run_session_proxy,
  run_agent_connection, run_control_loop
- Spawned tasks own args and handle errors internally
- Collect JoinHandles, abort all on shutdown
- Extract helpers to tunnel_helpers.rs
- Document backoff strategy with examples

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CaManager::load_or_generate returns Arc<Self> directly
- Rename enrollment token consume → redeem
- Remove unused resolve-target API endpoint + helpers + tests
- Remove routing methods from registry (PR2 scope)
- Remove Option from RouteAdvertisementState (empty = no routes)
- Target enum for typed IP vs domain parsing
- Prefix variables clearly (server_cert_*, ca_*)
- Add TODO for traffic audit and Windows DACL
- Backoff strategy documented with examples

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address review feedback from Benoit and Marc-André:

- Rename HTTP mountpoint /jet/agent-tunnel → /jet/tunnel
- Replace SkipHostnameVerification with SpkiPinnedVerifier that
  performs full chain + hostname + SPKI pin validation
- Enrollment response now includes server_spki_sha256 for pinning
- Agent sends machine hostname; gateway adds it as DNS SAN alongside
  the UUID SAN (dual names for future direct connectivity)
- Agent connects using real gateway hostname instead of dummy value
- Move sha2/hex to cross-platform deps, add x509-parser + hostname

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove agent_name from EnrollResponse (agent knows it already)
- Agent generates its own UUID and sends it in EnrollRequest
- Rename api/agent_enrollment.rs → api/tunnel.rs (match endpoint)
- Use backoff crate for reconnect loop (same pattern as subscriber.rs)
- ALPN: "devolutions-agent-tunnel" → "gw-agent-tunnel/1" (versioned)
- Protocol version: 2 → 1 (previous was experimental, start fresh)
- Move session tests to integration test file (public API only)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SanType::Rfc822Name → SanType::URI for urn:uuid: (correct X.509 type)
- GeneralName::RFC822Name → GeneralName::URI in extraction
- Reject duplicate agent UUID on enrollment (409 Conflict)
- tokio::join! instead of select! for session proxy (prevents data loss)
- JoinSet instead of Vec<JoinHandle> (prevents unbounded growth)
- Timeout (30s) on session handshake recv_request/recv_response
- Fix typos: "redeemd" → "redeemed"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move current_time_millis() to agent-tunnel-proto (R1: eliminate duplication)
- Delete DomainInfo, use DomainAdvertisement directly in AgentInfo (R2)
- Merge enroll_agent/bootstrap_and_persist into single function (I1)
- Agent task_handles: Vec<JoinHandle> → JoinSet with reaping (I4)
- Same-epoch route refresh: mutate updated_at in place, no clone (I5)
- Add #[must_use] on enrollment_store::redeem() (I6)
- connect_via_agent: cleaner error extraction with if-let (I3)
- Add TODO for active_stream_count tracking (I2)
- SECS_PER_DAY constant replaces magic 86400 (P4)
- Consistent .context() for ProtoError instead of map_err (P7)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Hoist protocol version validation before match in both gateway and
  agent control loops (single check, no per-variant boilerplate)
- Validate ConnectResponse protocol version in connect_via_agent
- ServerCertStatus enum for ensure_server_cert (expiry + hostname SAN)
- send.finish() after proxy copy (graceful QUIC EOF)
- Fix constant_time_eq doc (inaccurate timing claim)
- Extract ALPN to agent_tunnel_proto::ALPN_PROTOCOL constant
- Destruct EnrollResponse at parameter level for readability
- ValidatedTunnelConf: make wrong state unrepresentable at type level
  (dto::TunnelConf for JSON, TunnelConf for runtime with non-optional fields)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@irvingoujAtDevolution irvingouj@Devolutions (irvingoujAtDevolution) force-pushed the feat/quic-tunnel-2-routing branch 2 times, most recently from 76acc25 to b70e278 Compare April 14, 2026 16:48
When a connection target matches an agent's advertised subnets or
domains, the gateway automatically routes through the QUIC tunnel
instead of connecting directly. This enables access to private
network resources without VPN or inbound firewall rules.

- Add routing pipeline (subnet match → domain suffix → direct)
- Integrate tunnel routing into RDP, SSH, VNC, ARD, and KDC proxy paths
- Support ServerTransport enum (Tcp/Quic) in rd_clean_path
- Add 7 routing unit tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Scope token exchange, enrollment string generation, and the Angular
webapp for agent management and tunnel-aware session creation.

- Enrollment string endpoint (POST /enrollment-string)
- Scope token exchange for agent management API
- QUIC endpoint override from enrollment string
- Agent enrollment UI (Agents page, enrollment form)
- Agent selector control in connection forms
- Auth interceptor fix (skip requests with existing Authorization)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

1 participant