Features / status

August 6, 2026 · View on GitHub

Functional v0. RDP clients (mstsc, Microsoft Remote Desktop, FreeRDP) can:

  • Connect over TLS to the Mac on port 3390 with a local Mac username/password.

  • See the primary display at native resolution with incremental damage-region updates.

  • Get the session served at the resolution the client asks for, by default (client-resolution auto-adopt; --no-client-resolution opts out). The vendored acceptor reads the client's requested desktop size from its GCC Client Core Data and negotiates the session at that size from the very first Demand Active — e.g. mstsc full-screen on a 1920×1080 monitor gets a 1920×1080 session instead of the Mac's 1512×982, so the client presents 1:1 with no client-side rescale (mstsc's rescale costs typing latency and, with --enable-h264, audio drift). Applies on the mirror-primary path with no explicit --width/--height/--hidpi, and on --virtual-display — there the virtual display is re-moded to the client's requested size at connect (--width/--height are its initial size, not a pin), so a phone and a laptop each get a crisp 1:1 desktop whose mouse coordinates (sent in the client's own display-space) map correctly; --no-client-resolution restores the old fixed-size behavior. Caveat for the headless modes: adopting a client narrower than the physical panel under --detach-primary/--capture-primary can make the Dock invisible over RDP — see the Dock/largest-online-display quirk note. An operator can cap the adoptable size with --max-client-size WxH (requests above it are clamped per-dimension — a defense-in-depth resource bound; without it a client can request up to the protocol max 8192×8192 ≈ 256 MB of framebuffer per frame). The usual non-native-capture trade-offs apply (full-frame legacy updates; and on an aspect mismatch the picture is letterboxed/pillarboxed to preserve the Mac's aspect ratio by default--stretch opts back into fill-and-distort, with input mapped into the centered picture either way — see the scaling quirk note). Verified live on FreeRDP (legacy + H.264, incl. reconnect at a different size); see the quirk note below for why this must live in the acceptor. The session also resizes live if the client resizes its window mid-connection (Windows App / mstsc send an MS-RDPEDISP monitor-layout PDU on resize): a debounced deactivation-reactivation renegotiates the session at the new size, with the H.264 pipeline rebuilding fresh at the new dimensions on the EGFX path. Gated by the same condition as the initial auto-adopt — it only works where auto-adopt itself is active (mirror-primary, no --width/--height/--hidpi), plus on --virtual-display (which re-modes the live CGVirtualDisplay to match, the same mechanism as its connect-time adopt above). --hidpi disables live resize the same way it disables the initial auto-adopt — a HiDPI mirror-primary session with no virtual display stays pinned at its connect-time size; a resize attempt is silently ignored (logged at debug). Live-verified on Windows App for macOS on all three supported paths (mirror-primary legacy bitmap, mirror-primary EGFX, and --virtual-display + EGFX — HIDPI is inert on the --virtual-display path regardless, per the HiDPI/virtual-display quirk note below, so it doesn't factor into that verification).

  • Optionally capture the primary display at its backing (Retina) pixel resolution (--hidpi, e.g. 3024×1964 instead of 1512×982 logical points) so clients render crisp native pixels instead of upscaling a point-density frame. Opt-in (it's ~4× the pixels); the win is biggest with --enable-h264. Verified crisp; input/cursor are resolution-correct. Caveat: mstsc decodes 4× the pixels per frame and feels laggy at HiDPI — Thincast/FreeRDP stay snappy. Also disables live client-driven resize (see above) — a --hidpi mirror-primary session with no --virtual-display pins the desktop size for the life of the connection, same as it pins the initial auto-adopt. See the HiDPI quirk note below.

  • Optionally stream the display as H.264 over EGFX (--enable-h264, AVC420, Annex-B framing, VideoToolbox-encoded) — far less bandwidth than legacy bitmaps. Verified rendering on mstsc, on FreeRDP built with H.264 decode, and on the macOS Windows App / Microsoft Remote Desktop client (it decodes AVC420 over EGFX — only its legacy bitmap-codec list is NSCodec-only). Clients that genuinely don't advertise AVC420 decode (e.g. a decoder-less FreeRDP build) fall back to legacy BitmapUpdate automatically. Caveat: reconnecting mstsc to a still-running macrdp can show a blank screen (mstsc-specific EGFX surface-handling quirk — confirmed not a server bug, since FreeRDP reconnects cleanly); reliable workaround is to fully close and reopen the mstsc window (clears its surface cache). See the H.264 quirk note below.

  • Drive keyboard and mouse, including modifier keys (per-side L/R tracking with NX_DEVICE bits, Caps Lock as a toggle, MS-RDPBCGR Synchronize lock-state reconciliation), mouse buttons, and wheel. Keyboard input is positional scancode→macOS keycode by default — but non-US layouts are served by translating each typing key against the client's layout via Carbon UCKeyTranslate and posting the resulting character as a Unicode string, without changing the Mac's own input source. The layout is auto-detected from the client's announced KLID by default (US/unknown keep the plain keycode path, so the US majority is unaffected); --keyboard-layout <name|KLID|input-source-id> (e.g. french, de, 0x040C) forces a specific layout, --keyboard-layout none disables translation. Cmd/Ctrl combos stay on the keycode path so shortcuts work; dead keys (´+e→é) compose via UCKeyTranslate's persistent state. See src/keyboard_layout.rs and the keyboard-layout quirk note.

  • Optionally remap Windows editing shortcuts from Ctrl to Cmd (--map-ctrl-to-cmd, opt-in) so Windows muscle memory drives macOS copy/paste: a curated key set (C V X A Z S F N T W O P R G, plus Shift variants like Ctrl+Shift+Z→redo) fires as the Cmd equivalent. Off by default (Q excluded so Ctrl+QCmd+Q-quit; nav keys untouched). Auto-suppressed when a terminal is frontmost so Ctrl+C stays SIGINT — built-in standalone-terminal list plus --no-remap-apps <bundle,…> for editors with an embedded terminal (e.g. com.microsoft.VSCode), where Ctrl then stays Ctrl everywhere (editor copy uses Cmd+C, the app's native macOS copy). Detecting the frontmost app in a headless session needed three last-wins signals (NSWorkspace activation observer + mouse-down AX hit-test + AX poll) because Electron apps take key focus without activating; see src/input.rs and the Ctrl→Cmd quirk note. From Devolutions' IronRDP CTO feedback (pairs with the legacy-codec startup nudge toward --enable-h264).

  • Forward macOS symbolic hotkeys that WindowServer's dispatcher refuses to fire for user-space CGEventPost: Cmd+Tab / Cmd+Shift+Tab cycle apps via Accessibility API (per-bundle dedup with MRU, dead-pid filtering via kill(pid, 0); --alt-tab-switch additionally accepts Option+Tab / Option+Shift+Tab as the same trigger — committing on Option release — for clients that forward Alt+Tab but gate Win+Tab). The app you release on always surfaces — a window is raised+AXMain'd, a minimized app is un-minimized (with a deferred re-raise to beat the genie animation), and a running-but-windowless app (Notes/Calendar/Mail with their window closed) is reopened via open -b — all gated to the landing app so apps merely cycled through don't pop/flicker. Optionally --app-switcher-hud spawns a separate Swift helper (macrdphud, in gui/) that draws a real non-activating overlay panel (icon row, like native Cmd+Tab) which ScreenCaptureKit captures so the remote client sees the switcher; macrdp pushes SHOW/ADVANCE/HIDE to it over loopback (src/switcher_hud.rs), best-effort. Cmd+` / Cmd+Shift+` cycle windows of the current app (AXRaise + window AXMain + app AXMainWindow for Electron compatibility; --alt-backtick-switch additionally accepts Option+` / Option+Shift+` as the same trigger, mirroring --alt-tab-switch — no release-commit needed since the window cycle acts immediately per press), Cmd+Space invokes Spotlight via AppleScript, Cmd+Shift+3/4/5 shell out to /usr/sbin/screencapture or open Screenshot.app.

  • See the real macOS cursor shape (I-beam, hand, etc.) overlaid by the client.

  • Copy/paste UTF-8 text and images (CF_DIB ↔ PNG) between Mac and remote.

  • Mac→Windows file copy, including whole folders: copying a file or directory in Finder and pasting on Windows produces a real file/tree in Explorer. The pasteboard walk recurses into directories (skipping symlinks, capped at 10 000 descriptors per copy) and emits one FILEGROUPDESCRIPTORW entry per leaf with relative_path set so upstream's wire encoder reconstructs the right MyFolder\sub\file.txt cFileName. Bytes stream via MS-RDPECLIP FileContentsRequest SIZE + RANGE chunks (4 MiB per chunk). Reaches upstream Cliprdr::initiate_file_copy via the vendored ServerEvent::ClipboardFileCopy(Vec<FileDescriptor>) variant — that's the only API that populates local_file_list, without which upstream short-circuits every byte fetch with CB_RESPONSE_FAIL. Finder hands out file-reference URLs (/.file/id=...); we resolve them through NSURL::URLByResolvingSymlinksInPath because std::fs::metadata can't stat them directly.

  • Windows→Mac file copy (single files OR folder trees via Ctrl-A+Ctrl-C inside a folder; raw Ctrl-C on a folder doesn't work — see caveat below). Two paths, switched by --no-lazy-paste (lazy is the default):

    • Lazy (default, src/file_promise_lazy.rs): create a pre-sized empty temp file per leaf, register one NSFilePresenter per file via NSFileCoordinator.addFilePresenter:, publish only the top-level NSURLs to NSPasteboard. Bytes stream only when Finder's Cmd-V triggers the coordinator's relinquishPresentedItemToReader: callback, during which we synchronously fetch chunks (1 MiB × 2 in flight, LAZY_PARALLEL_CHUNKS — lower than eager because the user is actively interacting at paste time and a higher count visibly stutters RDP input) into the pre-allocated file, then invoke reader(nil). macOS shows its native "Preparing to paste" progress dialog during the wait. No Glass chime / auto-Cmd-V needed (the user pasted; Finder handles it). On fetch failure we delete the temp file so Finder errors loudly rather than silently copying a zero-padded ghost.
    • Eager (--no-lazy-paste, src/file_promise.rs): when Windows announces a FileGroupDescriptorW we download every entry to /tmp/macrdp-paste-<pid>-<nanos>/ via parallel FileContentsRequest chunks (1 MiB × 8 in flight, EAGER_PARALLEL_CHUNKS), recreating any directory structure encoded in each descriptor's relative_path, then publish the top-level entries to NSPasteboard as real NSURLs. On completion we play /System/Library/Sounds/Glass.aiff (afplay bypasses notification permissions; osascript display notification is silently suppressed because macOS attributes the banner to the unsigned macrdp binary) and, only if Finder is the frontmost app, fire Cmd-V via System Events so the paste the user attempted finishes automatically. Kept as a fallback for users who prefer the up-front download + auto-paste UX, and for any file whose descriptor lacks a size (lazy falls back to eager automatically in that case). Both paths share paste_temp_dir + self_change_count on the backend and clean up on disconnect (Drop on MacCliprdrBackend) and signal exit (shutdown_cleanup() via process-global handle, since std::process::exit bypasses Drop).
    • Both paths use the same resolve_dest for relative_path sanitization (rejects ., .., embedded /) so a malicious remote can't escape the temp sandbox; both share the same fetch_one_file chunk fan-out (pwrite via FileExt::write_at over an Arc<File>, no per-chunk open+seek+close); both rely on CAN_LOCK_CLIPDATA being negotiated (see clipboard.rs client_capabilities) so cliprdr auto-issues Lock/Unlock around the descriptor — without that cap, Windows treated the descriptor as ephemeral and would silently drop rapid follow-up Ctrl-C and release file data mid-stream on large downloads (CB_RESPONSE_FAIL). A SelfChangeCount atomic stops our own NSPasteboard write from being rebroadcast to Windows by the change-count poller.

    Ctrl-C on a folder in Windows Explorer is a known no-op — not our bug, and not fixable from the server side. Explorer puts CFSTR_SHELLIDLIST (Shell IDList Array) on the clipboard as the primary format and delay-renders FileGroupDescriptorW only when a shell-aware receiver asks. mstsc doesn't request the delayed format, so it never forwards anything via CLIPRDR — cliprdr=debug shows zero PDUs for the folder copy attempt. Workaround for the user: enter the folder in Explorer, Ctrl-A then Ctrl-C to copy the contents (with directory descriptors for any subfolders) — that path uses FileGroupDescriptorW directly and forwards correctly. True drag-from-Windows folder copy would need drive redirection (a different RDP feature, not clipboard).

  • Forward macOS system audio to the remote (RDPSND, 44.1 kHz stereo 16-bit PCM; SCK captures at 48 kHz and the capture loop resamples via rubato). Optionally compress it as AAC-LC (--enable-aac, WAVE_FORMAT_AAC_MS over RDPSND, ~128 kbps vs PCM's ~1.4 Mbit/s) — AudioToolbox-encoded (src/aac.rs), raw access units, advertised ahead of PCM so clients that decode AAC negotiate it while everyone else falls back to PCM automatically. Opt-in because AAC adds ~40–50 ms of encoder priming latency, so PCM stays the zero-latency LAN default. See the AAC quirk note below.

  • NLA / CredSSP authentication — no more "type username before Connect" mstsc workaround.

  • Auth hardening in front of the NLA/CredSSP gate (on by default): per-source-IP connection rate-limiting (default 10 attempts / 60 s), escalating, auto-expiring failed-attempt lockout (after 5 consecutive failures: 30 s, doubling per further failure to a 15 min cap; any clean session resets it), and a greppable auth audit log (macrdp::audit lines — accept/reject/disconnect, source IP, reason, outcome — in ~/Library/Logs/macrdp.log). Loopback is exempt so you can't lock yourself out locally; the lockout is heuristic (errored/very-short ⇒ failure, clean long session ⇒ reset), so a benign disconnect like mstsc's first-connect cert-prompt "Broken pipe" never trips it. All thresholds are tunable / disable-able via MACRDP_CONN_GUARD / MACRDP_GUARD_* / MACRDP_AUDIT_LOG (or the matching config.env keys). Lives in src/auth_guard.rs, wired through the existing ConnectionHandler seam. See @docs/cli.md.

  • Optionally attach a headless virtual display (--virtual-display --width W --height H) and serve that to the client instead of mirroring the primary panel — behaves like plugging in an external monitor, so the local Mac screen stays available while the remote session has its own desktop at any requested resolution. Backed by undocumented CGVirtualDisplay* private API; see the maintenance note below.

  • Optionally go fully headless while a client is connected via one of two mechanisms (mutually exclusive):

    • --virtual-display ... --detach-primary: disables every active physical display at the WindowServer level once the first RDP client actually connects (private CGSConfigureDisplayEnabled). Backlight off, no menu bar, cursor can't cross over. Cleaning a stale detach if macrdp dies hard happens automatically — the detach uses CGConfigureForAppOnly so SIGKILL / panic / power loss trigger an OS-level revert with no logout required. Caveat: on some macOS versions / displays the disable transaction succeeds but the panel keeps showing the desktop; if that's the case, use --capture-primary instead.
    • --virtual-display ... --capture-primary: takes exclusive CGDisplayCapture of every physical display once a client connects AND forces each panel's gamma LUT to map every input to black via CGSetDisplayTransferByFormula(_, 0,0,1, 0,0,1, 0,0,1). Capture alone doesn't visually blank modern macOS panels (the "fill with black on capture" semantic disappeared around 10.10) — the gamma trick is what actually makes the panel render solid black while the WindowServer keeps compositing the desktop to it. Backlight stays on, cursor sunk by the capture. Both gamma changes and capture tokens are process-scoped, so SIGKILL / panic auto-restores. Uses only public CG symbols — no private SkyLight surface, no CGError 1001 window. Security caveat: while this mode is engaged the Mac CANNOT BE LOCKED — Lock Screen silently no-ops, because loginwindow cannot draw onto a captured display. Treat such a Mac as physically unsecured, or use --shield-primary below.
    • --virtual-display ... --shield-primary: covers every physical panel with an opaque black window (drawn by the bundled macrdpshield helper process) instead of capturing + gamma-blacking it. Same visible result, two concrete wins: the Mac can still be locked (no capture is taken), and there is no ~250 ms desktop flash on a live client resize (a window survives a display reconfiguration; a gamma LUT is reset by one and can't be re-written until it commits). The trade-off is that the local pointer is no longer confined, so a person at the machine can move it and disturb the remote cursor — their clicks are swallowed by the shield, and their keystrokes behave exactly as under capture, which never blocked the keyboard. A missing helper binary aborts startup; an unreachable one at connect time only warns, so check the log. The Mac locks and the lock screen is visible (live-verified): by default this mode keeps the physical panel as system main, so loginwindow draws the lock screen on the physical panel over the black shield. The cost (confirmed on a real second-machine client) is that the remote desktop has no menu bar or Dock — they stay on the shielded physical panel — though it stays usable via Ctrl+Alt+G to gather app windows onto the visible display. While the Mac is locked the remote session can still see the desktop but cannot control it (macOS blocks synthetic input to a locked session). MACRDP_SHIELD_KEEP_PHYSICAL_MAIN=0 reverts to vd-as-main (Dock on the vd, but the lock screen invisible locally). See the quirk note. Config: PRIMARY_MODE=shield. Either way, the original layout is restored the moment the last client disconnects; local Mac usage is normal whenever no one is connected. A window opened on the physical panel before you connected (or one an app pops onto the primary mid-session) is stranded off the virtual display and invisible over RDP — press Ctrl+Alt+G (Ctrl+Option+G) any time to sweep such windows onto the display the client sees (see the stranded-windows quirk note).

Not yet implemented: multi-monitor (client-side multi-display), printer redirection. (Non-US keyboard layouts work, auto-detected from the client by default--keyboard-layout overrides, none disables.) Drive redirection (RDPDR) behind --enable-drive-redirection (opt-in, read-write): the connecting client redirects its local drive(s) and the Mac mounts each as its own real volume (one NFS mount per redirected filesystem device, keyed by device id). Phase 1a — MS-RDPEFS handshake + drive discovery (verified FreeRDP and mstsc); 1b — device I/O via RdpdrHandle (read_file, list_dir, matched by a completion-id router); Phase 2 — the macOS surface (src/rdpdr/surface.rs) is a real NFS mount: RdpdrFs implements nfsserve's NFSFileSystem over the RdpdrHandle (a path↔fileid cache), an in-process NFSv3 server is mounted via the built-in mount_nfs (no root, no kext, no FUSE), and the OS's VFS drives lazy lookups as the user browses — so full subdirectory navigation works (the Phase-1c temp-folder/NSFilePresenter mirror was top-level-only and is replaced). Writes map NFS ops to RDPDR: write_file (DeviceWrite), create/mkdir (DeviceCreate), and truncate/delete/rename (SetInformation FileEndOfFile/Disposition/Rename) — so editing, copying-in, mkdir, mv, and rm all work (verified byte-exact on FreeRDP and on mstsc, writing to a folder the redirected user owns — a STATUS_ACCESS_DENIED from a non-writable target like the C:\ root is Windows' own ACL, surfaced as NFS3ERR_ACCES). setattr honors size only (mode/times are no-ops). Unmounted on disconnect (Surface::Drop). See the vendored ironrdp-rdpdr / ironrdp-server divergence logs and the RDPDR quirk notes.

Smart-card redirection (RDPDR / MS-RDPESC) behind --enable-smartcard-redirection (opt-in): the connecting client redirects its smart-card reader and macOS PC/SC apps use the card through it — the standard client→server direction, so the card stays on the client while the Mac in the session reads it. The card is announced as an RDPDR Smartcard device and PC/SC calls ride that channel as SCARD_IOCTL_* device-control IOCTLs (MS-RDPESC), bodies NDR/RPCE-marshaled. Server-direction MS-RDPESC lives in the vendored ironrdp-rdpdr (pdu/esc, divergence (2)): HeaderlessEncode for the *Call set the server sends + HeaderlessDecode for the *Return set + the ScardControlRequest DR_CONTROL_REQ envelope; the async RdpdrHandle::scard_* methods (establish/list/get-status/connect/status/transmit/disconnect) + the completion-id router are in the vendored ironrdp-server (src/rdpdr.rs, divergence (11)). The macOS side is macrdp's own PC/SC IFD handler (ifd-handler/ — a from-scratch MIT/Apache cdylib, so no GPL vpcd) loaded by com.apple.ifdreader; src/rdpdr/smartcard.rs is a loopback-TCP bridge (port 40242, MACRDP_SCARD_PORT) that translates the handler's POWER_ON/POWER_OFF/TRANSMIT/PRESENCE protocol into scard_* calls. Full flow: macOS app → PC/SC → our IFD handler → bridge → MS-RDPESC over RDPDR → client → physical card → back. Verified end-to-end on mstsc with a Windows TPM virtual smart card (card-free test path): establish-context / list-readers / get-status-change (ATR) / connect / a full APDU transceive (GIDS SELECT → FCI + 90 00). Six real-Windows NDR conformance edges the offline round-trips couldn't catch were fixed during verification (8-byte pointer-sized SCARDCONTEXT/SCARDHANDLE; NULL-referent value sections on mszReaderNames / pbExtraBytes / pbRecvBuffer / the empty embedded Context of a returned handle — that last one made the connect handle decode cbHandle=0 and Windows tore down the session); plus the bridge fills the handle's context with the established context for requests, reads the ATR from GetStatusChange (real Windows rejects our Status_Call params), caches presence 300 ms (macOS CryptoTokenKit polls IFDHICCPresence tens of times/sec → would flood RDP), and caps cbRecvLength at 8 KiB (Windows rejects 0x10000). Deployment: the IFD handler ships embedded in macrdp.app/Contents/Resources/ifd-macrdp.bundle and installs (once, privileged — one GUI admin prompt) into /usr/local/libexec/SmartCardServices/drivers via packaging/install-ifd-handler.sh; macOS loads a third-party IFD driver only on a USB hotplug matching its Info.plist VID/PID, so a headless server needs a USB device permanently attached (bind it with IFD_VID/IFD_PID). macOS-only. See the vendored ironrdp-rdpdr (divergence (2)) / ironrdp-server (divergence (11)) logs.

Generic USB redirection (MS-RDPEUSB / URBDRC) behind --enable-usb-redirection (EXPERIMENTAL, opt-in, default OFF, macOS-only, needs the entitled build): the connecting client redirects a physical USB device and macrdp presents it as a real local device on the Mac — e.g. a redirected flash drive mounts in Finder and is read/write. This is the standard client→server direction (the device stays on the client; the Mac in the session drives it). Unlike smart-card redirection (which is device-class / PC-SC only), this is generic USB: macrdp synthesizes a local USB device via a user-space virtual USB host controller — the public IOUSBHost UserHCI API (IOUSBHostControllerInterface), which needs the managed entitlement com.apple.developer.usb.host-controller-interface (granted to the project; a plain non-entitled build logs "controller unavailable" and no-ops, so it ships inert). As far as is known, macrdp is the first open-source RDP server to present a client-redirected USB device (evidence + caveats: oss-rdp-server-comparison.md) (and the only working one) — FreeRDP's urbdrc is client-direction only (no server/, issue #7558), and xrdp/ogon/gnome-remote-desktop have none; on real Windows RDS the presentation is done by closed kernel drivers (usbdr.sys). Architecture: the URBDRC server-direction DVC processor + the async UsbHandle transfer path (get_descriptor / select_configuration / bulk_transfer_in/out / control_transfer_out, all over MS-RDPEUSB TS_URB PDUs, routed by a completion-id router) live in the vendored ironrdp-server (src/rdpeusb.rs, divergence (16)) against the ironrdp-rdpeusb PDU crate (the upstream a5d1c682 git dep — its lenient USB-3 caps decode landed upstream, so the fork was de-vendored at the v0.9.5 pin bump); the macrdp side (src/usb_redirect/) is a cross-platform MacUsb factory + drive_device (dedups one controller per physical device on VID:PID:bcdDevice — the client can double-announce one drive) driving the quarantined Obj-C UserHCI state machine (usb_spike.m: the command/doorbell loop + async out-of-band transfer completion hopping onto the interface's serial queue). The macOS kernel's EP0 GET_DESCRIPTOR / control-OUT (mass-storage Bulk-Only Reset / Clear-Feature) and the bulk-endpoint SCSI (CBW/data/CSW) are all answered by driving the client's real device over URBDRC. VERIFIED end-to-end on a real Linux FreeRDP client (UTM-QEMU Ubuntu + a USB-2.0 hub for a claimable mass-storage interface): the ESD310C flash drive mounts on the Mac and stays mounted, 1300+ steady bulk transfers, file copy + remove/reattach clean. Connect-while-mounted, Get Max LUN (generic control-IN), and hot-unplug/reset recovery are all live-verified. Caveats / scope: mass storage and HID-input (gamepad) are verified — a client-redirected Xbox controller (045e:0b12) is a live, button-responsive gamepad on the Mac (macOS binds its own XboxSeriesXGamepad driver; 2600+ interrupt-IN input reports delivered, cold-start included — verified on Linux FreeRDP and mstsc 2026-07-08, gamepad-tester confirmed), the first HID-class device through the redirect. On FreeRDP it needed no server code change (the interrupt-transfer path already carried it); on mstsc one fix was needed — pressing the controller's Guide button issues a SET_FEATURE(DEVICE_REMOTE_WAKEUP) control-OUT that the URBDRC codec only accepts in a TRANSFER_IN_REQUEST, so it's now routed there (was mis-routed as TRANSFER_OUT → an encode error that used to disconnect the whole session; the URBDRC send path is also encode-tolerant now so no URB can tear the session down). The one setup requirement is standard for USB redirection — release the client's own driver (e.g. modprobe -r xpad on Linux) so the redirect can claim the interface. A bulk UVC webcam also streams live video over FreeRDP (verified 2026-07-08: an A4Tech FHD 1080P PC Camera 09da:2692, whose VideoStreaming endpoint is bulk not isochronous, shows smooth moving video in Photo Booth on the Mac — as far as is known a first for any OSS RDP server). This needed a bulk-IN read-ahead engine in usb_spike.m: macOS double/triple-buffers a streaming bulk-IN endpoint (it queues several concurrent reads so the device's pipe never runs dry), but the UserHCI ring exposes only one transfer at a time (currentTransferMessage advances only on completion — no peek-ahead), so serving it one-read-at-a-time starves the device (it returns moved=0 and macOS tears the stream down). macrdp instead keeps MACRDP_USB_PREFETCH_DEPTH (default 4) concurrent bulk_transfer_in reads in flight to the client, buffered in sequence order and delivered one per ring TRB — restoring URB depth with no data loss. Gated on the transfer's read length (normalReadLen >= 512), so a webcam's tens-of-KB streaming reads engage read-ahead while an HID interrupt poll (≤ wMaxPacketSize, e.g. 64 B) stays on the serial path. That gate is load-bearing and its history is instructive: from v0.8.30 to v0.8.33 the test was address-only (non-EP0 IN + NormalTransfer), which an interrupt-IN pipe also satisfies — so a redirected gamepad enumerated but its buttons did nothing (input reports at ~20 s instead of ~8 ms). v0.8.34 first fixed that by gating on the endpoint's declared transfer type (is_bulk from SelectConfiguration), but a UVC video endpoint is frequently reported over the wire with is_bulk=false (measured 69/81), so the declared-type gate wrongly excluded the webcam → no image. v0.8.35 switched to the read-length gate above, which reliably separates the two where neither the endpoint address nor the wire-declared type does. See the USB quirk note. (Whether mstsc then delivers a webcam's frames is client-side — it prefers its own camera-redirection channel and can refuse the raw-USB transfers with 0x8007001f; the FreeRDP bulk-webcam path is the verified one.) Other classes (audio, etc.) remain untested; isochronous webcams are not yet supported (this cam is bulk); an explicit RETRACT_DEVICE PDU (the client channel-close path already covers detach/reset) and true multi-device (needs iSerialNumber to distinguish identical models) are not yet done; and it's in main and shipped in the tagged v0.8.27 release — but as inert-unless-entitled code: the standard release build/DMG contains it but no-ops ("controller unavailable"), so it only functions in a build signed with the USB host-controller entitlement + provisioning profile. The client must opt in too: FreeRDP /usb:... (no client-side policy needed); mstsc gates RemoteFX USB behind Group Policy — enable "Allow RDP redirection of other supported RemoteFX USB devices from this computer" (Computer Config → Admin Templates → Windows Components → Remote Desktop Services → Remote Desktop Connection Client → RemoteFX USB Device Redirection) and reboot, after which the device appears under Local Resources → More → USB (the checkbox isn't shown until the policy is on). mstsc status (2026-07-07): a redirected device ENUMERATES, CONFIGURES, and negotiates its format end-to-end; the only remaining gap is the client not delivering the actual video/data frames. Verified live with a camera + a USB-audio/HID device: macrdp reads their real descriptors, SelectConfiguration succeeds (endpoint pipe handles established), 135+ control transfers succeed, and — with a camera opened in Photo Booth on the Mac — the UVC VS_PROBE/COMMIT format negotiation completes and macOS issues continuous bulk reads on the video endpoint. But mstsc never returns frame data (of ~10 concurrent bulk reads it completes one with 0x8007001f and leaves the rest pending). That's a client/mstsc-side limitation, not a server bug — for a webcam, Windows routes real video over the dedicated camera-redirection channel ("Video capture devices" in mstsc) — a different protocol, which macrdp now implements (see Camera redirection below, shipped in v0.9.0). For a webcam, use camera redirection, not USB redirection — that's the path mstsc actually feeds. Getting this far took several mstsc-strictness interop fixes on top of enumeration, all FreeRDP-safe and regression-checked against FreeRDP mass storage: the per-device channel needs the full capability-exchange → CHANNEL_CREATED → RIMCALL_RELEASE handshake; UsbDevice=0 is accepted and interface-0 URB completions route by function id; SelectConfiguration emits one interface-info per interface number (not per alternate setting) and carries the full configuration descriptor (real Windows rejects a header-only one with 0x80070057); control transfers are sent as the typed URB functions real Windows uses (CLASS_INTERFACE, GET_DESCRIPTOR_FROM_INTERFACE, …) instead of the generic CONTROL_TRANSFER_EX mstsc rejects; and bulk/interrupt IN sets USBD_SHORT_TRANSFER_OK. Note mstsc's RemoteFX USB list excludes mass storage (a redirected drive rides Drives / RDPDR instead), so the verified-working bulk/mount path can't be exercised from mstsc; mass storage is verified on FreeRDP. See docs/usb-redirection-feasibility.md and the vendored ironrdp-server divergence (16) log.

Camera redirection (MS-RDPECAM) behind --enable-camera-redirection (opt-in, default OFF, macOS-only): the connecting client redirects its webcam and macrdp presents it as a real macOS camera — "macrdp Camera" appears in Photo Booth / Zoom / FaceTime / Teams showing the client's live video. This is the standard client→server direction (the camera stays on the client; the Mac in the session uses it). As far as is known, macrdp is the first open-source RDP server to present a client-redirected webcam as a native OS camera. Be precise about what that means: FreeRDP does ship server-direction MS-RDPECAM code (channels/rdpecam/server/), so macrdp is not the first to speak the protocol server-side — but that code is a channel endpoint, not a pipeline: on a Sample Response it extracts only the payload size + pointer and hands the raw bytes to an application callback, with no video decoding and no OS device registration (verified by direct source read, 2026-07-20). The first-ness is the end-to-end path — decode the samples and register a real camera device with the host OS — and it is the path that actually works for mstsc, which routes webcams over MS-RDPECAM and refuses the raw-USB reads (0x8007001f) the USB-redirection path would need. The default runtime path is byte-identical when the flag is off.

The pipeline, end to end: H.264 samples arrive over the MS-RDPECAM RDCamera DVC (plain TCP — UDP is not a prerequisite) → VideoToolbox decodes them to 420v (NV12) CVPixelBuffers → macrdp, acting as a CoreMediaIO client, enqueues those onto the sink stream of a CoreMediaIO Camera system extension (the buffers are IOSurface-backed, so the handoff is zero-copy) → the extension's consume loop forwards each onto its source stream, which is what capturing apps see. LIVE-VERIFIED on real mstsc at 1080p/~30 fps with zero dropped frames.

Architecture: the MS-RDPECAM server state machine (enumerator handshake → the client-named per-device channel → ActivateDeviceStreamList → media-type negotiation picking H.264 → StartStreams → the SampleRequestSampleResponse pull loop) lives in the vendored ironrdp-server (src/rdcamera.rs, divergence (19)), mirroring FreeRDP's rdpecam server and the URBDRC per-device model. The macrdp side is src/camera/ — a cross-platform MacCamera factory + policy (mod.rs), the VideoToolbox decoder (decode.rs), and the CoreMediaIO sink producer (feed.rs). The virtual camera itself is gui/Sources/macrdpcamera — a Swift CMIOExtension built from a plain SwiftPM target and hand-assembled into a .systemextension bundle with no Xcode (packaging/make-camera-extension.sh).

Setup / caveats: presenting the camera requires the system extension to be installed and activated once — it ships inside macrdpController.app and is activated from its menu ("Enable macrdp Camera…"), which needs the signed + notarized build (the com.apple.developer.system-extension.install entitlement is self-serviceable — no Apple approval, unlike the USB host-controller entitlement). Without it macrdp still negotiates and decodes; it simply has no camera to feed. The client opts in too (mstsc: Local Resources → More → Video capturing devices, enabled before connecting). Decode diagnostics (raw H.264 + PNG frame dumps to $TMPDIR) are opt-in behind MACRDP_CAMERA_DUMP=1. Migrating the camera channel onto the UDP tunnel is scoped but deferred — TCP carries it fine. Read docs/camera-extension-setup.md before touching any of this: it documents the one-time Apple-portal setup and, more importantly, four CoreMediaIO failure modes that all fail silently (the .systemextension filename must equal its bundle id; CMIOExtensionClient.signingID is literally the string "unknown", so sink-producer authentication is impossible and a rejecting hook surfaces as a bogus -4; kCMIOStreamPropertyDirection is inverted from its documented meaning, and starting the wrong stream returns success while nothing ever drains; and macOS never replaces a same-CFBundleVersion system extension). See the vendored ironrdp-server divergence (19) log.

RDP UDP multitransport (MS-RDPEMT / MS-RDPEUDP / MS-RDPEDYC Soft-Sync) behind --enable-udp-multitransport (opt-in, gated by the multitransport cargo feature; default OFF). Serves the EGFX (H.264) channel over a reliable UDP tunnel instead of TCP. Scope (confirmed by a lossy-link soak 2026-06-26): this is a clean-link / low-loss feature, NOT a lossy-link win. A reliable ordered RDPEUDP stream head-of-line-blocks on its own loss exactly like TCP, so under real loss EGFX-over-UDP freezes — and so does EGFX-over-TCP under the same shaping (the freeze is H.264-under-loss on an ordered stream, not a transport bug). The genuine loss-resilience win needs Phase 2 (lossy UdpFecL + FEC), deferred. What Phase 1 does deliver: a working server-side UDP data path + channel isolation on a clean link. As far as is known this is the first open-source RDP server with a working UDP multitransport data path (FreeRDP, the most complete OSS stack, has no working UDP data path on either side — its server is a TCP-side bootstrap stub and its client declines UDP with E_ABORT; the RDPEUDP/RDPEUDP2 work is out-of-tree prototype only, never merged. Re-verified against FreeRDP git history 2026-06-26. xrdp/ogon/gnome-remote-desktop/Weston are TCP-only). The whole transport (RDPEUDP v1 reliability state machine, RDPEUDP2 codecs) lives in a new sans-I/O crate vendor/ironrdp-rdpeudp/; the listener + TLS + tunnel + Soft-Sync live in the vendored ironrdp-server (src/multitransport/, divergence (12)) and vendored ironrdp-dvc (Soft-Sync codec). Flow: the acceptor offers multitransport after licensing → the client opens a UDP flow → RDPEUDP v2 reliable handshake (mstsc uses v2-carrying-TLS, not EUDP2) → rustls TLS over the reliable stream (same cert as TCP) → MS-RDPEMT tunnel with strict cookie binding (CSPRNG cookie, one-time-use registry, bound to the TCP session) → a DYNVC Soft-Sync request migrates the EGFX DVC onto the tunnel → EGFX H.264 PDUs ride the tunnel as RDP_TUNNEL_DATA (bare DRDYNVC PDUs), both directions. EGFX migration itself is additionally gated by the experimental --udp-migrate-egfx flag (default off → EGFX stays on TCP, the proven empty-Soft-Sync safe spike; promoted 2026-06-28 from the MACRDP_UDP_MIGRATE_EGFX=1 env var, which still works as a fallback so the ..._LOSSY isolation test keeps working). It's clean-link optimal — under loss the reliable ordered tunnel head-of-line-blocks like TCP — but no longer freezes-until-reconnect: an auto-recovery watchdog detects the wedge (~3 s of silent EGFX acks while shipping) and de-migrates EGFX back to the TCP DRDYNVC channel (one-way per session), and an opt-in congestion-responsive adaptive bitrate (--adaptive-bitrate, AIMD on the standing queue delay — ship→ack RTT minus the windowed-minimum link RTT, EWMA-smoothed — plus retransmits, with hysteresis + a 3-zone hold, UDP-side IDR backoff, and a frame-rate floor — see the rate-control note below) backs the encoder off before the wedge — running on both the UDP tunnel and the TCP path. See the rate-control + watchdog notes in the feasibility doc. Verified rendering end-to-end on real mstsc (Win11/WiFi). EGFX video rides UDP; audio can also ride a lossy UDP/DTLS tunnel via --enable-lossy-audio (opt-in, implies --enable-udp-multitransport, needs --enable-aac + --enable-h264) — this is the Phase-2 loss-resilience win: AAC Wave2 data goes on a lossy UdpFecL RDPEUDP flow (deliver-on-arrival, no retransmit) with 1+1 redundancy (each datagram sent twice; client DTLS anti-replay dedups → p→p² effective loss), the MS-RDPEA format handshake staying on a reliable DVC over TCP. Soak-verified on real mstsc 2026-06-29: dup=0 glitches at 5% loss; 1+1 stays smooth at 5/10/15% (see the lossy-audio soak RESULT in the feasibility doc). The flag bridges the four expert env gates (MACRDP_UDP_{OFFER_FECL,LOSSY_DELIVERY,LOSSY_AUDIO,LOSSY_AUDIO_DUP}, which still work standalone). Input + clipboard still ride TCP by design. CAVEAT (2026-07-04): LAN/WiFi only — do NOT enable over VPN/ZeroTier-class overlay links: if the UDP tunnel wedges there, mstsc's ~60 s dead-tunnel timeout resets the whole session cyclically (up ~60 s → reset → auto-reconnect → reconnect-blank → blank-recovery drop → renders → repeat — live repro 2026-07-04 over ZeroTier; every recovery stage behaved correctly, the tunnel wedge was the driver). Tunnel-death detection (2026-07-04) bounds the damage: after ~30 s of inbound silence on a bound tunnel whose owning TCP session is still alive, the listener declares it dead (a tunnel abandoned by an ended session is retired quietly with no cooldown — #138) — audio falls back to TCP immediately and multitransport offers are suppressed for a cooldown (10 min default), so the client's reset reconnects as a stable plain-TCP session: at most one reset instead of an endless cycle (MACRDP_UDP_TUNNEL_DEAD_SECS / MACRDP_UDP_MT_COOLDOWN_SECS; keepalives were evaluated and rejected — a dead overlay path wouldn't deliver them either). LIVE-VERIFIED 2026-07-04 (real mstsc over ZeroTier, natural wedge — no artificial block needed): three tunnel-death declarations at idle_ms≈30s, audio kept playing through the fallback (user-confirmed), and every subsequent connection logged multitransport offer SUPPRESSED (tunnel-death cooldown) and ran plain TCP — cycle broken. Caveat: the cooldown is in-process state, so a server crash/restart wipes it (one fresh offer follows).. The offer itself is now RTT-GATED (2026-07-05): the server measures each connection's TCP RTT at accept (the divergence-15 cell) and simply does not offer multitransport to a connection at/above MACRDP_UDP_OFFER_MAX_RTT_MS (default 80 ms; 0 disables) — an overlay-class link runs plain TCP from the first byte with no tunnel to wedge, so the UDP switches are safe to leave enabled on a roaming client (LAN/WiFi sessions get UDP, distant sessions silently stay TCP; verified live: threshold-1 loopback trips the gate, default-80 loopback offers normally). A link that degrades after connect stays covered by the reactive tunnel-death detection above. Plain TCP (ENABLE_UDP_MULTITRANSPORT=0, ENABLE_LOSSY_AUDIO=0) remains the zero-risk config for such links. Remaining Phase-2 work is loss-resilient video over the tunnel (the reliable-tunnel HOL-block ceiling; lower value). FEC proper is a structural NO-GO (RDPUDP2 has no FEC; no reachable client decodes it). See docs/rdp-udp-multitransport-feasibility.md and the vendored ironrdp-server / ironrdp-rdpeudp / ironrdp-dvc divergence logs.

Congestion-responsive rate control (--adaptive-bitrate, opt-in). Under packet loss or a thin/high-latency link (VPN, ZeroTier, internet) an H.264 session degrades gracefully instead of freezing — on both the UDP tunnel and the TCP path. An AIMD controller reads the standing queue delay — each frame's ship→ack round trip minus the windowed-minimum RTT, EWMA-smoothed, with hysteresis + a 3-zone hold so single spikes don't pump the rate — plus reliable-tunnel retransmits, and live-adjusts the VideoToolbox bitrate within [floor, --bitrate ceiling] — so --bitrate becomes a ceiling, not a fixed target. The signal is RTT-aware (2026-07-04): a long-but-clean pipe reads as ~0 standing queue and keeps full quality at any RTT — the previous frame-count ack-lag signal conflated RTT with congestion (frames-in-flight = RTT × fps), so a clean 240 ms VPN link read as permanently congested and the controller crater-climb oscillated at the floor (the "frozen/partial video over VPN/ZeroTier" report; diagnosed + fixed under a shaped 240 ms/2 Mbit lab link). A no-ack distress fallback (time since the last real ack, while actively shipping into outstanding frames) keeps the signal alive when a fully choked client stops acking entirely. Layered levers: (1) lower bitrate; (2) IDR backoff — stretch/suppress the periodic keyframe under congestion (both transports — an IDR can be seconds of link time on a thin pipe) with one clean recovery IDR on restore; (3) P2b frame-rate floor — once bitrate is pinned at the floor and still congested (quality cuts exhausted), cap the effective fps (default 10, never to zero — the client needs trailing frames to present/ack) so video stays choppy-but-steady-and-in-sync rather than freezing. Recovers (fps + bitrate climb back) when the link clears. Lab-verified across three regimes (clean LAN unchanged; 240 ms/2 Mbit stays at full quality; saturated 500 Kbit detects in ~3 s, converges to alive-and-in-sync at the sustainable rate — queue drained 2.9 s → 45 ms, ack lag from thousands to 2–3). See docs/rdp-udp-multitransport-feasibility.md (rate-control SHIPPED notes) for the full design + the per-PR history.

Live-telemetry endpoint (--stats-endpoint, opt-in, default OFF) — a loopback-only, read-only TCP listener (src/stats.rs, 127.0.0.1:$MACRDP_STATS_PORT / 40245; config STATS_ENDPOINT=1) that writes one JSON snapshot of the live H.264 session on each connect and closes: current bitrate + the --bitrate ceiling, link RTT, standing queue delay, fps, frames sent, and the session dimensions. No disk writes — the snapshot is a handful of in-memory atomics updated at three low-frequency points in the encode path (connection setup, the adaptive-bitrate step, and the QoE callback), each guarded so that when the endpoint is off the default runtime path is a byte-identical no-op (stats::global() returns None). It exists for the menu-bar controller's Status pane (below), which polls it every ~2 s only while that pane is open and a client is connected; the response is served inline with a 2 s write-timeout so a stuck/hostile local process can't wedge or flood it. It carries only the local session's own A/V numbers (no client IP/hostname) and never reads request bytes — see the loopback-IPC trust-boundary note in @docs/macos-gotchas.md (channel 0). macOS-only concern, cross-platform code.

Menu-bar controller — tabbed Settings window. The bundled macrdpController.app (menu-bar front-end that drives the LaunchAgent + config.env; gui/Sources/macrdptray) presents a real tabbed Settings window ("Show macrdp…") with a System-Settings-style sidebar (Status · Connection · Video · Audio · Display · Input · Redirection · Advanced · Permissions — also a Section menu, ⌘1–⌘9) instead of the old flat menu submenus. Settings edit an in-memory draft and apply in one server restart via Apply/Revert (rather than one restart per toggle); the tray menu is trimmed to Show / Start / Stop / Restart / Quit. The Status tab shows live server RAM/CPU/uptime (ps) + the connected client (lsof + the log fingerprint), and — when --stats-endpoint is on — the live bitrate/RTT/fps/frames from the endpoint above. The Video tab exposes the H.264 bitrate ceiling (via the new BITRATE config key → --bitrate) + the adaptive-bitrate toggle. Plus an About macrdp panel (version + status). The controller needs no TCC grants of its own.