Skip to content

feat(pam-rdp): record RDP sessions#218

Open
bernie-g wants to merge 9 commits intofix/cli-rdp-musl-staticfrom
feat/pam-rdp-recording
Open

feat(pam-rdp): record RDP sessions#218
bernie-g wants to merge 9 commits intofix/cli-rdp-musl-staticfrom
feat/pam-rdp-recording

Conversation

@bernie-g
Copy link
Copy Markdown
Contributor

@bernie-g bernie-g commented May 6, 2026

Description 📣

Implements end-to-end RDP session recording: a Rust IronRDP-based MITM bridge taps each PDU on the gateway, the Go side streams events into the chunked-recording uploader, and byte-level capability filters force the server into a codec set the WASM replay decoder can decompress. Event timestamps are anchored to the PAM session start so reconnects within a single session play back as one continuous timeline.

Type ✨

  • Bug fix
  • New feature
  • Improvement
  • Breaking change
  • Documentation

Tests 🛠️

```sh
cd packages/pam/handlers/rdp/native && cargo test --lib
go vet -tags rdp ./packages/pam/...
```

Manually verified: Windows Server 2022 RDP playback (single connection, reconnect within session, multi-reconnect), playback total matches last frame, non-RDP session types (SSH/DB/K8s) unaffected.


bernie-g added 3 commits May 5, 2026 10:04
Tap each PDU in the post-CredSSP byte bridge and stream structured events
(target_frame / keyboard / unicode / mouse) through the existing session
logger so they land in the encrypted chunk pipeline.

Capture switches the post-CredSSP path from copy_bidirectional to a
PDU-framed bridge: read_pdu yields TPKT/FastPath frames pure-framing, no
RDP state machine, the bytes are forwarded unchanged, and the tap emits
SessionEvent variants on an mpsc channel. This preserves the
no-MCS/capability/share-state-drift property of the byte-pump it
replaces.

The FFI gains rdp_bridge_poll_event for Go to drain those events with a
timeout. TargetFrame payloads are handed across as libc::malloc'd
buffers; the Go side defers C.free after copying.

Go-side, RDPProxy.HandleConnection spawns a drain goroutine that JSON-
encodes each event and calls SessionLogger.LogTerminalEvent with
ChannelType=rdp. The chunk uploader is protocol-agnostic, so RDP
sessions now flow into pam_session_event_chunks like SSH/HTTP do.

session.LogTerminalEvent skips masking for the rdp channel because the
data field carries a base64-JSON envelope; SSH-shaped masking regexes
would corrupt valid recordings.
Three fixes that together make RDP recording playback render correctly:

- Filter Order, BitmapCodecs, and INFO_COMPRESSION on the wire so the
  server only emits Bitmap update PDUs IronRDP-session can decompress.
  Implemented as byte surgery on Confirm Active and Client Info PDUs;
  IronRDP's typed decode->encode loses unrelated fields. New cap_filter
  module + walk_caps + 14 unit tests pin the byte-preservation contract.
- Override ev.ElapsedNs with time.Since(SessionStartedAt) in the Go drain
  so reconnects within the same PAM session don't restart the bridge's
  local clock from zero. SessionUploader exposes GetSessionStartedAt
  (reconstructed from the persisted lastEndElapsedMs).
- Stamp chunk endElapsedMs from the last entry's elapsedTime instead of
  time.Since(state.startedAt) at flush moment, so the playback total
  doesn't reach past the last actual frame. readFromOffset returns the
  trailing entry's elapsed time; falls back to wallclock for non-terminal
  sessions whose entries lack the field.

Comment cleanup pass across the touched RDP files.
@infisical-review-police
Copy link
Copy Markdown

💬 Discussion in Slack: #pr-review-cli-218-feat-pam-rdp-record-rdp-sessions

Posted by Review Police — reviews, comments, new commits, and CI failures will stream into this channel.

bernie-g added 3 commits May 6, 2026 13:54
…isement

Adds two MITM bridge fixes so both Windows App/mstsc and FreeRDP work
through the gateway:

- Connector now advertises HYBRID_EX|HYBRID|SSL (matching native
  clients) instead of IronRDP's hardcoded HYBRID|HYBRID_EX. Native
  clients validate the MCS Connect Response echo of clientRequestedProtocols
  against what they sent on their own X.224 step and disconnect on
  mismatch. Done via a small connector_x224_with_protocol helper that
  replaces ironrdp_tokio::connect_begin (which exposes no knob for the
  protocol set).
- filter_client_mcs_connect_initial now mutates CS_CORE.serverSelectedProtocol
  to HYBRID_EX before forwarding (FreeRDP echoes the wrong value, which
  target Windows servers reject) in addition to clearing CS_NET channels
  to stop the target from opening virtual channels the bridge can't
  service.

Bridge errors and panics also surface to the gateway stderr via
eprintln so silent Rust failures aren't lost.
Generated .rdp file now sets `authentication level:i:0`. mstsc validates
the server's TLS cert by default and rejects the bridge's self-signed
cert with "unexpected server authentication certificate", terminating
the connection before the X.224 handshake. FreeRDP and Windows App
don't enforce the same check, so this only manifests for mstsc users.

Verified through mstsc on a Windows EC2 connecting via gateway+relay.
@bernie-g bernie-g force-pushed the feat/pam-rdp-recording branch 2 times, most recently from 875ac29 to 3f00ca5 Compare May 7, 2026 20:39
PR #191's release pipeline flipped the linux builds from CGO_ENABLED=0
to CGO_ENABLED=1 to link the Rust IronRDP bridge. With CGO on, the Go
linker hands off to gcc, which dynamically links against the build
host's glibc. v0.43.80 ended up with a GLIBC_2.39 floor from the
ubuntu-24.04 GitHub runner, breaking ~80% of customer environments
(Ubuntu 22.04, RHEL 8/9, Amazon Linux, Alpine, distroless/static).

Switch the linux RDP builds to musl-static so the binary is fully
self-contained again, matching pre-PAM portability:

- build-rdp-bridge.yml: linux Rust matrix swapped from *-linux-gnu*
  to *-linux-musl* (windows-gnu kept).
- goreleaser.yaml: each linux-*-rdp build entry uses
  CC=<triple>-unknown-linux-musl-gcc, points CGO_LDFLAGS at the musl
  target dir, adds -extldflags '-static' to ldflags, and adds
  osusergo,netgo to build tags to keep Go's pure-Go user/DNS
  resolvers (matching pre-RDP behaviour and sidestepping musl's
  NSS-less getaddrinfo).
- release_build_infisical_cli.yml: install musl cross-toolchains
  from cross-tools/musl-cross GitHub releases (CDN-backed, replaces
  the unreliable musl.cc single-host mirror); pinned to release
  20260430. curl retries kept for any network blips.
- README.md (rust bridge): updated example triples.

Adds a release-time gate: every linux RDP binary in dist/ must be
'statically linked', and the amd64 binary must --version cleanly
across a matrix of older / minimal distros (Ubuntu 20.04+, RHEL 8+,
Amazon Linux 2+, Alpine, distroless/static). A regression of the
v0.43.80 shape now blocks publish.

The Alpine Docker images and the .apk package are fixed for free
since copying a musl-static binary into Alpine works cleanly. No Go
or Rust source code changed beyond restoring the RDP feature.
@bernie-g bernie-g changed the base branch from main to fix/cli-rdp-musl-static May 7, 2026 20:40
…at/pam-rdp-recording

# Conflicts:
#	packages/pam/handlers/rdp/bridge_cgo_unix.go
#	packages/pam/handlers/rdp/proxy.go
#	packages/pam/pam-proxy.go
@bernie-g bernie-g marked this pull request as ready for review May 7, 2026 20:55
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 795ae0530b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/pam/handlers/rdp/bridge_cgo_shared.go Outdated
Comment thread packages/pam/pam-proxy.go
Comment thread packages/pam/session/uploader.go
Comment thread packages/pam/handlers/rdp/native/src/events.rs
Comment thread packages/pam/session/uploader.go
Comment thread packages/pam/handlers/rdp/native/src/bridge.rs Outdated
@bernie-g bernie-g requested a review from x032205 May 7, 2026 21:18
- Bridge tap channel switched from unbounded to bounded(1024) with try_send;
  drops on full instead of risking gateway OOM under heavy graphics.
- bridge_pdus uses tokio::select! instead of try_join! so a normal client
  disconnect doesn't hang on the t2c branch waiting for a quiet target.
- HandleConnection no longer cancels the drain on normal session end; the
  drain runs to PollEnded so the recording tail is preserved. Cancellation
  paths still cancel explicitly.
- SessionUploader.RegisterSession preserves the existing in-memory anchor
  when called multiple times for the same session (RDP reconnects), so
  elapsedNs stays monotonic across reconnects within a single PAM session.
- uploadSessionFile bulk-upload fallback handles ResourceTypeWindows the
  same way as SSH (TerminalEvent records); previously fell through to the
  database-row decoder which silently zero-filled input/output.
@bernie-g bernie-g force-pushed the fix/cli-rdp-musl-static branch from 5b44f1a to 760fef6 Compare May 8, 2026 00:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant