A native Android RDP server experiment written in Kotlin and Go.
go-rdp-android is exploring how far a normal installed Android app can go toward exposing the Android screen over RDP without using ADB as the runtime architecture, building on rcarmo/go-rdp as the original core protocol implementation and reference. The app uses Android MediaProjection for screen capture, an AccessibilityService landing path for input, and a Go RDP server core bridged into Android with gomobile.
The current implementation is a CI-first prototype: it can build Go-backed APK/AAB artifacts, launch the APK in an Android emulator, grant MediaProjection, connect to the embedded Go RDP server over forwarded TCP, render RDP screenshots, exercise keyboard/mouse/touch input scripts, and generate a Gherkin/Playwright UX PDF report.
Implemented or validated today:
- Native Android Kotlin shell with
MainActivity,RdpForegroundService, MediaProjection consent/denial flow, serialized foreground-service mode switching, notification/UI stop actions, credential-refusal cleanup, network-address notification refresh, non-sticky service restart policy, non-secret settings persistence, Android controls for security mode and failed-auth policy, compact UI health/diagnostic sharing, and AccessibilityService declaration. - Go RDP server core with TPKT, X.224, MCS, GCC server core/security/network data, TLS-only Client Info authentication, Hybrid/NLA CredSSP/NTLMv2 authentication via
rcarmo/go-rdp, TLS certificate persistence/rotation/fingerprint support, access-policy controls, failed-auth backoff/lockout, Demand Active/Confirm Active finalization, FontMap, slow-path bitmap updates, and slow-path/Fast-Path input decoding. gomobile bindintegration viamobile.aar, with Kotlin reflection backend and logging fallback when the AAR is absent.- Android
MediaProjectioncapture pipeline usingVirtualDisplay+ImageReaderRGBA frames. - Synthetic test-pattern frame source for emulator/CI validation without capture permission.
- RDP 24-bit BGR bitmap update tiling sized for safe TPKT/PER envelopes.
- Dirty-tile suppression for post-initial streamed frames.
- Layered capture pacing/backpressure: Android adaptive capture interval, bounded Go queue drops, and server-side queued-frame coalescing.
- Optional MediaProjection downscale mode (
capture_scale/emulator_capture_scale). - Keyboard, mouse, pointer, wheel decode/degrade, and RDPEI touch validation in CI using scripted emulator input and synthetic dynamic-channel touch packets.
- Gherkin-style UX stories under
features/ux/and a Playwright-based PDF report generator. - GitHub Actions coverage for Go tests, race tests, fuzz smoke, classic and NLA authentication smokes, Android APK builds, gomobile AAR/API checks, FreeRDP compatibility probes, emulator capture tests, and UX PDF artifacts.
- Tag-driven CI/CD policy for build, UX, and release tag classes, including signed APK/AAB staging, SBOM, checksum, and release-note artifacts for
v*tags. - Security documentation in
docs/THREAT_MODEL.mdplus user-facing privacy/security copy indocs/PRIVACY.mdcovering capture, listening state, credentials, remote input, diagnostics, and recommended defaults.
Partially implemented / experimental:
- Real-client RDP compatibility. The mock server/probe path is stable, and the FreeRDP CI gate now requires
/sec:rdp,/sec:tls, and/sec:nlabitmap fallback plus/sec:nla /gfxRDPGFX to reach active state, handle Fast-Path input, capture a screenshot, and stay connected until CI terminates the client; Microsoft-client compatibility is still pending. - Accessibility input injection. Pointer taps/drags and frame-aware RDPEI touch contacts now reach bounded Accessibility gesture paths with continuation/multi-stroke fallback; richer keyboard/text, secondary-button behavior, gesture failure handling, and physical-device validation still need hardening.
- Performance. RDPGFX over
drdynvcis implemented as the default compressed graphics path using Planar no-alpha RLE, while slow-path 24-bit bitmap transport remains the measured fallback. Experimental AndroidMediaCodecH.264 capture and RDPGFX AVC420 emission scaffolding exists, with CI artifacts proving forced server-side emission but not true client compatibility yet; Android health now exposesh264Statusso testers can distinguish disabled, unsupported, forced, and ready paths. Physical-device performance comparison is still pending.
- Physical Android-device validation is still pending; current automated evidence comes from CI, emulator, Go tests, and FreeRDP probes.
- Microsoft Remote Desktop active-streaming validation is still pending, especially NLA behavior and certificate-warning UX.
- Accessibility input depends on Android service availability and user consent; richer keyboard/text, secondary-button, and gesture-failure handling remain limited.
- Graphics default to RDPGFX Planar when the client supports it, with raw 24-bit bitmap updates retained as compatibility fallback. Bitmap RLE is available only as an experimental opt-in fallback (
GO_RDP_ANDROID_ENABLE_BITMAP_RLE=1) with diagnostics/saved-byte evidence, not as a negotiated/default release path. NSCodec/JPEG/PNG/RemoteFX SurfaceBits emitters are also experimental/opt-in and report write/raw/saved/percentage diagnostics when exercised; RemoteFX has a production single-tile encoder but still requires client advertisement and remains non-default. RDPGFX ClearCodec, Progressive/ProgressiveV2, and AVC444/AVC444v2 have partial/minimal opt-in production encoders plus fixture hooks, but none are release defaults. H.264/AVC over RDPGFX AVC420 is experimental and not a release compatibility claim until a client advertises support without force-mode probes; useh264Statusin diagnostics to classify fallback reasons, and keep physical-device/Microsoft-client performance and compatibility evidence pending. - Public release defaults should prefer
nla-required;tls-onlyis for non-NLA clients, and plain RDP is only for isolated compatibility testing.
- SemVer:
0.1.2 - Android namespace/application ID:
io.carmo.go.rdp.android - Android
versionCode:3 - Go module:
github.com/rcarmo/go-rdp-android
Android package IDs cannot contain hyphens. The project name go-rdp-android is represented as the Android package io.carmo.go.rdp.android.
android/app/ Native Android Kotlin app
cmd/mock-server/ Desktop mock RDP server for protocol experiments
cmd/probe/ Scriptable RDP probe/client used by CI
features/ux/ Gherkin-style UX user stories
internal/frame/ Frame source abstractions and test patterns
internal/input/ Input sink abstractions
internal/rdpserver/ Go RDP server core
mobile/ gomobile-facing Go bridge API
scripts/ CI helpers, artifact checks, UX report generator
docs/ Architecture, testing, performance and release docs
- Documentation index
- Current project status
- Architecture
- Android integration — includes Android startup and RDP client connection instructions.
- Testing and CI
- Debugging
- Threat model
- Privacy/security notes
- Performance
- Release/tag policy
- Specification and feasibility notes
- Milestones
Go checks:
make test
make build-go
make coverageRun the desktop mock server and probe:
# terminal 1
make run-mock-pattern
# terminal 2
make probe
# or run both as a smoke test
make smokeBuild Android locally if the Android SDK and Gradle are available:
make android-buildBuild the Go-backed Android APK:
make gomobile-init # first time only
make android-build-goGenerate a UX report from an existing emulator-artifacts/ directory:
npm ci
npx playwright install --with-deps chromium
make ux-reportDefault push/PR CI runs:
- Go vet/build/test/coverage.
- Race tests and parser fuzz smoke.
- Mock server + probe artifact generation.
- Android debug APK build and inspection.
- gomobile AAR build, API verification, and Go-backed APK/AAB builds with standalone artifact uploads.
- Blocking FreeRDP compatibility probe requiring bitmap/update streaming evidence.
Current blocking FreeRDP compatibility signals are tracked in docs/STATUS.md:
| Mode | Active | Bitmap/update | Fast-Path input | Screenshot | Current expected exit |
|---|---|---|---|---|---|
/sec:rdp |
✅ | ✅ | ✅ | ✅ | 131 non-timeout shutdown after capture |
/sec:tls |
✅ | ✅ | ✅ | ✅ | 131 non-timeout shutdown after capture |
/sec:nla |
✅ | ✅ | ✅ | ✅ | 131 non-timeout shutdown after capture |
Run the local graphics encoding matrix when changing transport code or collecting release-candidate evidence:
make encoding-matrixThat matrix covers slow-path bitmap fallback, opt-in bitmap RLE fallback with saved-byte evidence, opt-in NSCodec/JPEG/PNG/RemoteFX SurfaceBits probes with selected/write/raw/saved/percentage fields, RDPGFX Planar, RDPGFX uncompressed diagnostics, production-vs-fixture probes for ClearCodec/Progressive/ProgressiveV2/AVC444/AVC444v2, forced /gfx:AVC420, and forced /gfx H.264 smoke cases. It explicitly separates release defaults from experimental emitters, fixture hooks, partial production encoders, and missing client proof. See docs/GRAPHICS_CODECS.md for the codec coverage and decision matrix.
Manual emulator UX run:
gh workflow run CI \
--ref main \
-f emulator_api_level=35 \
-f emulator_go_backed=true \
-f emulator_capture=true \
-f emulator_capture_scale=2Tag behavior:
| Tag pattern | Behavior |
|---|---|
*-ux |
Full emulator UX validation and Playwright PDF report. |
*-build |
Build/test/artifact production. |
vX.X.X |
Release tag: Go-backed release APK/AAB artifacts plus UX PDF report staged for signed release files. |
-
RDP compatibility hardening
- Improve real-client compatibility beyond the current probe/mock path.
- Expand GCC/security/licensing/capability handling.
- Keep expanding the now-blocking FreeRDP compatibility gate beyond bitmap/update streaming toward full clean-session behavior.
-
Input injection completion
- Map RDP pointer, keyboard, Unicode, and touch events into robust Accessibility gestures and text input.
- Add coordinate transforms for downscaled capture and rotation.
- Extend CI scripts and future device tests for more input workflows.
-
Performance workstreams
- Continue dirty-tile suppression improvements.
- Keep single RDP sessions open for UX navigation and incremental metrics.
- Capture pacing/backpressure now has the first production-oriented layers in place: Android adaptive capture interval, bounded queue drops, and server-side queued-frame coalescing; remaining validation is on real devices and constrained networks.
- Expand downscale/quality modes.
- Investigate compressed bitmap/RDPGFX updates.
- Continue H.264/AVC with Android hardware encoding: encoder/queue/RDPGFX AVC420 scaffolding exists, but client proof and physical performance validation remain pending.
-
Security and release readiness
- Security mode, failed-auth backoff controls, and copyable TLS fingerprint are now surfaced in Android UI; release guidance recommends
nla-requiredfirst and reservesrdp-onlyfor isolated compatibility testing. CIDR/user allowlists remain server-core/mock-server-only for the first polished APK; continue with TLS rotation controls. - Continue hardening TLS Client Info and Hybrid/NLA CredSSP authentication paths against real clients.
- Validate signed release APK/AAB staging with production secrets.
- Version/tag consistency is enforced by release preflight; validate the controlled
vX.X.Xtag path after production signing secrets are confirmed.
- Security mode, failed-auth backoff controls, and copyable TLS fingerprint are now surfaced in Android UI; release guidance recommends
-
Physical-device validation
- Validate MediaProjection, AccessibilityService behavior, network reachability, rotation, latency, and sustained capture on real Android devices.
- The app is not production-ready and should not be exposed to untrusted networks.
- The RDP server profile is intentionally minimal and not yet compatible with every client.
- Hybrid/NLA CredSSP passes current FreeRDP CI gates, but Microsoft Remote Desktop compatibility is still not guaranteed.
- Audio, clipboard, drive redirection, and full multi-monitor semantics are out of scope for the current prototype. Dynamic virtual channels are implemented only for the bounded
drdynvc/RDPEI touch-input subset. - MediaProjection cannot capture protected content.
- Accessibility input injection is more restricted than shell/ADB input injection.