//go:build windows package rdp import ( "fmt" "sync" "syscall" "time" "unsafe" ) var ( libfreerdp = syscall.NewLazyDLL("libfreerdp3.dll") libfreerdpClient = syscall.NewLazyDLL("libfreerdp-client3.dll") // Instance lifecycle procFreerdpNew = libfreerdp.NewProc("freerdp_new") procFreerdpFree = libfreerdp.NewProc("freerdp_free") procFreerdpConnect = libfreerdp.NewProc("freerdp_connect") procFreerdpDisconnect = libfreerdp.NewProc("freerdp_disconnect") // Context procContextNew = libfreerdp.NewProc("freerdp_context_new") // Settings procSettingsSetString = libfreerdp.NewProc("freerdp_settings_set_string") procSettingsSetUint32 = libfreerdp.NewProc("freerdp_settings_set_uint32") procSettingsSetBool = libfreerdp.NewProc("freerdp_settings_set_bool") // Input procInputSendMouse = libfreerdp.NewProc("freerdp_input_send_mouse_event") procInputSendKeyboard = libfreerdp.NewProc("freerdp_input_send_keyboard_event") // Event loop — NOTE: takes rdpContext*, not freerdp* procCheckEventHandles = libfreerdp.NewProc("freerdp_check_event_handles") // GDI subsystem (libfreerdp3.dll exports these) procGdiInit = libfreerdp.NewProc("gdi_init") procGdiFree = libfreerdp.NewProc("gdi_free") // Client helpers procClientNew = libfreerdpClient.NewProc("freerdp_client_context_new") procClientFree = libfreerdpClient.NewProc("freerdp_client_context_free") ) // ============================================================================ // FreeRDP3 settings IDs // // Source: FreeRDP 3.10.3 — include/freerdp/settings_types_private.h // Each ID is the ALIGN64 slot index from the rdp_settings struct. // ============================================================================ const ( FreeRDP_ServerHostname = 20 FreeRDP_ServerPort = 21 FreeRDP_Username = 22 FreeRDP_Password = 23 FreeRDP_Domain = 24 FreeRDP_DesktopWidth = 1025 FreeRDP_DesktopHeight = 1026 FreeRDP_ColorDepth = 1027 FreeRDP_FullscreenMode = 1028 FreeRDP_SoftwareGdi = 1601 // BOOL — enable software GDI rendering FreeRDP_IgnoreCertificate = 4556 FreeRDP_AuthenticationOnly = 4554 FreeRDP_NlaSecurity = 4560 FreeRDP_TlsSecurity = 4561 FreeRDP_RdpSecurity = 4562 ) // Keyboard event flags for FreeRDP input calls. const ( KBD_FLAGS_EXTENDED = 0x0100 KBD_FLAGS_DOWN = 0x4000 KBD_FLAGS_RELEASE = 0x8000 ) // ============================================================================ // FreeRDP3 struct offsets — x86_64 / Windows amd64 // // IMPORTANT: These offsets are specific to FreeRDP 3.10.3 compiled with // WITH_FREERDP_DEPRECATED=OFF (the default, and what our CI uses). // // Every ALIGN64 slot is 8 bytes. The slot number from the FreeRDP headers // gives the byte offset as: slot * 8. // // Source: FreeRDP 3.10.3 — include/freerdp/freerdp.h, include/freerdp/update.h // ============================================================================ const ( // rdp_freerdp struct (ALIGN64 slots, 8 bytes each) // slot 0: context (rdpContext*) // slot 48: PreConnect callback (pConnectCallback) // slot 49: PostConnect callback (pConnectCallback) // slot 55: PostDisconnect callback (pPostDisconnect) freerdpOffsetContext = 0 * 8 // rdpContext* — slot 0 freerdpOffsetPreConnect = 48 * 8 // pConnectCallback — slot 48 freerdpOffsetPostConnect = 49 * 8 // pConnectCallback — slot 49 freerdpOffsetPostDisconnect = 55 * 8 // pPostDisconnect — slot 55 // rdp_context struct (ALIGN64 slots, 8 bytes each) // slot 33: gdi (rdpGdi*) // slot 38: input (rdpInput*) // slot 39: update (rdpUpdate*) // slot 40: settings (rdpSettings*) contextOffsetGdi = 33 * 8 // rdpGdi* contextOffsetInput = 38 * 8 // rdpInput* contextOffsetUpdate = 39 * 8 // rdpUpdate* contextOffsetSettings = 40 * 8 // rdpSettings* // rdp_update struct (ALIGN64 slots, 8 bytes each) // slot 21: BitmapUpdate callback (pBitmapUpdate) updateOffsetBitmapUpdate = 21 * 8 // rdpGdi struct (NOT ALIGN64 — regular C struct with natural alignment) // On x86_64: // offset 0: rdpContext* context (8 bytes) // offset 8: INT32 width (4 bytes) // offset 12: INT32 height (4 bytes) // offset 16: UINT32 stride (4 bytes) // offset 20: UINT32 dstFormat (4 bytes) // offset 24: UINT32 cursor_x (4 bytes) // offset 28: UINT32 cursor_y (4 bytes) // offset 32: HGDI_DC hdc (8 bytes, pointer) // offset 40: gdiBitmap* primary (8 bytes) // offset 48: gdiBitmap* drawing (8 bytes) // offset 56: UINT32 bitmap_size (4 bytes) // offset 60: UINT32 bitmap_stride (4 bytes) // offset 64: BYTE* primary_buffer (8 bytes) gdiOffsetWidth = 8 gdiOffsetHeight = 12 gdiOffsetStride = 16 gdiOffsetPrimaryBuffer = 64 ) // Pixel format constants from FreeRDP3 codec/color.h. // Formula: (bpp << 24) | (type << 16) | (a << 12) | (r << 8) | (g << 4) | b // BGRA type = 4, RGBA type = 3 const ( PIXEL_FORMAT_BGRA32 = 0x20040888 ) // ============================================================================ // Instance-to-backend registry // // syscall.NewCallback produces a bare C function pointer — it cannot capture // Go closures. We use a global map keyed by the freerdp instance pointer to // recover the FreeRDPBackend from inside callbacks. // ============================================================================ var ( instanceRegistry = make(map[uintptr]*FreeRDPBackend) instanceRegistryMu sync.RWMutex ) func registerInstance(instance uintptr, backend *FreeRDPBackend) { instanceRegistryMu.Lock() instanceRegistry[instance] = backend instanceRegistryMu.Unlock() } func unregisterInstance(instance uintptr) { instanceRegistryMu.Lock() delete(instanceRegistry, instance) instanceRegistryMu.Unlock() } func lookupInstance(instance uintptr) *FreeRDPBackend { instanceRegistryMu.RLock() b := instanceRegistry[instance] instanceRegistryMu.RUnlock() return b } // ============================================================================ // Unsafe pointer helpers // ============================================================================ // readPtr reads a pointer-sized value at the given byte offset from base. func readPtr(base uintptr, offsetBytes uintptr) uintptr { return *(*uintptr)(unsafe.Pointer(base + offsetBytes)) } // writePtr writes a pointer-sized value at the given byte offset from base. func writePtr(base uintptr, offsetBytes uintptr, val uintptr) { *(*uintptr)(unsafe.Pointer(base + offsetBytes)) = val } // readU32 reads a uint32 at the given byte offset from base. func readU32(base uintptr, offsetBytes uintptr) uint32 { return *(*uint32)(unsafe.Pointer(base + offsetBytes)) } // ============================================================================ // Callbacks — registered via syscall.NewCallback (stdcall on Windows) // // FreeRDP callback signatures (from freerdp.h): // typedef BOOL (*pConnectCallback)(freerdp* instance); // typedef BOOL (*pBitmapUpdate)(rdpContext* context, const BITMAP_UPDATE* bitmap); // ============================================================================ // postConnectCallback is the global PostConnect handler. FreeRDP calls this // after the RDP connection is fully established. We use it to: // 1. Initialize the GDI subsystem (software rendering into a memory buffer) // 2. Extract the GDI primary_buffer pointer for frame capture // 3. Register the BitmapUpdate callback for partial screen updates var postConnectCallbackPtr = syscall.NewCallback(postConnectCallback) func postConnectCallback(instance uintptr) uintptr { backend := lookupInstance(instance) if backend == nil { return 0 // FALSE — unknown instance } // Read context from instance->context (slot 0). context := readPtr(instance, freerdpOffsetContext) if context == 0 { return 0 } // Initialize the GDI subsystem with BGRA32 pixel format. // gdi_init(freerdp* instance, UINT32 format) -> BOOL // This allocates the primary surface and registers internal paint callbacks. ret, _, _ := procGdiInit.Call(instance, uintptr(PIXEL_FORMAT_BGRA32)) if ret == 0 { return 0 // gdi_init failed } // Read the rdpGdi pointer from context->gdi (slot 33). gdi := readPtr(context, contextOffsetGdi) if gdi == 0 { return 0 } // Extract frame dimensions and primary buffer pointer from rdpGdi. gdiWidth := readU32(gdi, gdiOffsetWidth) gdiHeight := readU32(gdi, gdiOffsetHeight) gdiStride := readU32(gdi, gdiOffsetStride) primaryBuf := readPtr(gdi, gdiOffsetPrimaryBuffer) if primaryBuf == 0 { return 0 } // Store the GDI surface info on the backend for frame reads. backend.gdiPrimaryBuf = primaryBuf backend.gdiWidth = int(gdiWidth) backend.gdiHeight = int(gdiHeight) backend.gdiStride = int(gdiStride) // Re-create the pixel buffer if GDI negotiated a different resolution // (the server may reject our requested size). if int(gdiWidth) != backend.buffer.Width || int(gdiHeight) != backend.buffer.Height { backend.buffer = NewPixelBuffer(int(gdiWidth), int(gdiHeight)) } // Register the BitmapUpdate callback on update->BitmapUpdate (slot 21). // With GDI mode enabled, FreeRDP's internal GDI layer handles most // rendering via BeginPaint/EndPaint. The BitmapUpdate callback fires for // uncompressed bitmap transfers that bypass GDI. We register it as a // safety net to capture any frames that arrive through this path. update := readPtr(context, contextOffsetUpdate) if update != 0 { writePtr(update, updateOffsetBitmapUpdate, bitmapUpdateCallbackPtr) } backend.gdiReady = true return 1 // TRUE — success } // preConnectCallback is called before the connection is fully established. // We use it to enable SoftwareGdi mode so FreeRDP handles bitmap decoding. var preConnectCallbackPtr = syscall.NewCallback(preConnectCallback) func preConnectCallback(instance uintptr) uintptr { backend := lookupInstance(instance) if backend == nil { return 0 } // Enable software GDI — FreeRDP will decode bitmaps into a memory surface. backend.setBool(FreeRDP_SoftwareGdi, true) return 1 // TRUE } // bitmapUpdateCallback handles BITMAP_UPDATE messages. With GDI mode active, // most rendering goes through the GDI pipeline and lands in primary_buffer // automatically. This callback catches any bitmap updates that come through // the legacy path and blits them into our pixel buffer. // // BITMAP_UPDATE layout (FreeRDP 3.10.3): // UINT32 number — offset 0, number of rectangles // BITMAP_DATA* rectangles — offset 8 (pointer, 8-byte aligned) // // BITMAP_DATA layout (packed, not ALIGN64): // UINT32 destLeft — offset 0 // UINT32 destTop — offset 4 // UINT32 destRight — offset 8 // UINT32 destBottom — offset 12 // UINT32 width — offset 16 // UINT32 height — offset 20 // UINT32 bitsPerPixel — offset 24 // UINT32 flags — offset 28 // UINT32 bitmapLength — offset 32 // UINT32 cbCompFirstRowSize — offset 36 // UINT32 cbCompMainBodySize — offset 40 // UINT32 cbScanWidth — offset 44 // UINT32 cbUncompressedSize — offset 48 // BYTE* bitmapDataStream — offset 56 (pointer, 8-byte aligned after padding) // BOOL compressed — offset 64 var bitmapUpdateCallbackPtr = syscall.NewCallback(bitmapUpdateCallback) func bitmapUpdateCallback(context uintptr, bitmapUpdate uintptr) uintptr { if context == 0 || bitmapUpdate == 0 { return 1 // TRUE — don't break the pipeline } // Recover the instance from context->instance (slot 0 of rdpContext). instance := readPtr(context, 0) backend := lookupInstance(instance) if backend == nil || backend.buffer == nil { return 1 } // With GDI mode enabled, the primary_buffer is already updated by FreeRDP's // internal GDI renderer before this callback fires. We just need to mark // the pixel buffer as dirty so the next GetFrame picks up changes. // // For the bitmap update path specifically, we parse the rectangles and // copy each one from the GDI primary buffer into our PixelBuffer. if backend.gdiReady && backend.gdiPrimaryBuf != 0 { numRects := readU32(bitmapUpdate, 0) if numRects > 0 { backend.copyGdiToBuffer() } } return 1 // TRUE } // ============================================================================ // FreeRDPBackend // ============================================================================ // FreeRDPBackend implements RDPBackend using the FreeRDP3 library loaded at // runtime via syscall.NewLazyDLL. This avoids any CGO dependency and allows // cross-compilation from Linux while the DLLs are resolved at runtime on // Windows. type FreeRDPBackend struct { instance uintptr // freerdp* context uintptr // rdpContext* (extracted from instance->context) settings uintptr // rdpSettings* (extracted from context->settings) input uintptr // rdpInput* (extracted from context->input) buffer *PixelBuffer connected bool config RDPConfig mu sync.Mutex stopCh chan struct{} // GDI surface state — populated by PostConnect callback. gdiPrimaryBuf uintptr // BYTE* — FreeRDP's GDI framebuffer gdiWidth int gdiHeight int gdiStride int // bytes per row (may include padding) gdiReady bool } // NewFreeRDPBackend creates a new FreeRDP-backed RDP backend. The underlying // DLLs are not loaded until Connect is called. func NewFreeRDPBackend() *FreeRDPBackend { return &FreeRDPBackend{ stopCh: make(chan struct{}), } } // Connect establishes an RDP session using FreeRDP3. It creates a new FreeRDP // instance, configures connection settings, and starts the event loop. // // Initialization order (required by FreeRDP3): // 1. freerdp_new() — allocate instance // 2. Register callbacks — PreConnect, PostConnect on the instance struct // 3. freerdp_context_new() — allocate context (also allocates settings, input, update) // 4. Configure settings — via freerdp_settings_set_* on context->settings // 5. freerdp_connect() — triggers PreConnect -> TCP -> PostConnect // 6. Event loop — freerdp_check_event_handles in a goroutine func (f *FreeRDPBackend) Connect(config RDPConfig) error { f.mu.Lock() defer f.mu.Unlock() if f.connected { return fmt.Errorf("already connected") } f.config = config // ── Step 1: Create a bare FreeRDP instance ── ret, _, err := procFreerdpNew.Call() if ret == 0 { return fmt.Errorf("freerdp_new failed: %v", err) } f.instance = ret // Register this instance in the global map so callbacks can find us. registerInstance(f.instance, f) // ── Step 2: Register callbacks on the instance struct ── // PreConnect (slot 48): called before RDP negotiation, used to enable GDI. writePtr(f.instance, freerdpOffsetPreConnect, preConnectCallbackPtr) // PostConnect (slot 49): called after connection, sets up GDI rendering. writePtr(f.instance, freerdpOffsetPostConnect, postConnectCallbackPtr) // ── Step 3: Create the context ── // freerdp_context_new(freerdp* instance) -> BOOL // This allocates rdpContext and populates instance->context, including // the settings, input, and update sub-objects. ret, _, err = procContextNew.Call(f.instance) if ret == 0 { unregisterInstance(f.instance) procFreerdpFree.Call(f.instance) f.instance = 0 return fmt.Errorf("freerdp_context_new failed: %v", err) } // ── Extract context, settings, and input pointers ── f.context = readPtr(f.instance, freerdpOffsetContext) if f.context == 0 { unregisterInstance(f.instance) procFreerdpFree.Call(f.instance) f.instance = 0 return fmt.Errorf("freerdp context is null after context_new") } f.settings = readPtr(f.context, contextOffsetSettings) if f.settings == 0 { unregisterInstance(f.instance) procFreerdpFree.Call(f.instance) f.instance = 0 return fmt.Errorf("freerdp settings is null after context_new") } f.input = readPtr(f.context, contextOffsetInput) if f.input == 0 { unregisterInstance(f.instance) procFreerdpFree.Call(f.instance) f.instance = 0 return fmt.Errorf("freerdp input is null after context_new") } // ── Step 4: Configure connection settings ── f.setString(FreeRDP_ServerHostname, config.Hostname) f.setUint32(FreeRDP_ServerPort, uint32(config.Port)) f.setString(FreeRDP_Username, config.Username) f.setString(FreeRDP_Password, config.Password) if config.Domain != "" { f.setString(FreeRDP_Domain, config.Domain) } // Display settings with sensible defaults. width := config.Width if width == 0 { width = 1920 } height := config.Height if height == 0 { height = 1080 } f.setUint32(FreeRDP_DesktopWidth, uint32(width)) f.setUint32(FreeRDP_DesktopHeight, uint32(height)) colorDepth := config.ColorDepth if colorDepth == 0 { colorDepth = 32 } f.setUint32(FreeRDP_ColorDepth, uint32(colorDepth)) // Security mode selection. switch config.Security { case "nla": f.setBool(FreeRDP_NlaSecurity, true) case "tls": f.setBool(FreeRDP_TlsSecurity, true) case "rdp": f.setBool(FreeRDP_RdpSecurity, true) default: f.setBool(FreeRDP_NlaSecurity, true) } // Accept all server certificates. Per-host pinning is a future enhancement. f.setBool(FreeRDP_IgnoreCertificate, true) // Enable software GDI rendering. This tells FreeRDP to decode all bitmap // data into a memory framebuffer that we can read directly. The PreConnect // callback also sets this, but we set it here as well for clarity — // FreeRDP reads it during connection negotiation. f.setBool(FreeRDP_SoftwareGdi, true) // Allocate the pixel buffer for frame capture. f.buffer = NewPixelBuffer(width, height) // ── Step 5: Initiate the RDP connection ── // freerdp_connect calls PreConnect -> TCP/TLS/NLA -> PostConnect internally. ret, _, err = procFreerdpConnect.Call(f.instance) if ret == 0 { unregisterInstance(f.instance) procFreerdpFree.Call(f.instance) f.instance = 0 return fmt.Errorf("freerdp_connect failed: %v", err) } f.connected = true // ── Step 6: Start the event processing loop ── go f.eventLoop() return nil } // eventLoop polls FreeRDP for incoming events at roughly 60 fps. It runs // until the stop channel is closed or the connection is marked as inactive. func (f *FreeRDPBackend) eventLoop() { for { select { case <-f.stopCh: return default: f.mu.Lock() if !f.connected { f.mu.Unlock() return } // freerdp_check_event_handles takes rdpContext*, NOT freerdp*. procCheckEventHandles.Call(f.context) // After processing events, copy the GDI framebuffer into our // PixelBuffer so GetFrame returns current data. if f.gdiReady { f.copyGdiToBuffer() } f.mu.Unlock() time.Sleep(16 * time.Millisecond) // ~60 fps } } } // copyGdiToBuffer copies the FreeRDP GDI primary surface into the PixelBuffer. // The GDI buffer is BGRA32; our PixelBuffer expects RGBA. We do the channel // swap during the copy. // // Must be called with f.mu held. func (f *FreeRDPBackend) copyGdiToBuffer() { if f.gdiPrimaryBuf == 0 || f.buffer == nil { return } w := f.gdiWidth h := f.gdiHeight stride := f.gdiStride if stride == 0 { stride = w * 4 // fallback: assume tightly packed BGRA32 } // Total bytes in the GDI surface. totalBytes := stride * h if totalBytes <= 0 { return } // Read the raw GDI framebuffer. This is a direct memory read from the // FreeRDP-managed buffer. The GDI layer updates this buffer during // BeginPaint/EndPaint cycles triggered by freerdp_check_event_handles. src := unsafe.Slice((*byte)(unsafe.Pointer(f.gdiPrimaryBuf)), totalBytes) // Build the RGBA frame, swapping B and R channels (BGRA -> RGBA). buf := f.buffer buf.mu.Lock() defer buf.mu.Unlock() dstLen := w * h * 4 if len(buf.Data) != dstLen { buf.Data = make([]byte, dstLen) buf.Width = w buf.Height = h } for y := 0; y < h; y++ { srcRow := y * stride dstRow := y * w * 4 for x := 0; x < w; x++ { si := srcRow + x*4 di := dstRow + x*4 if si+3 >= totalBytes || di+3 >= dstLen { break } // BGRA -> RGBA buf.Data[di+0] = src[si+2] // R <- B buf.Data[di+1] = src[si+1] // G <- G buf.Data[di+2] = src[si+0] // B <- R buf.Data[di+3] = src[si+3] // A <- A } } buf.dirty = true } // Disconnect tears down the RDP session and frees the FreeRDP instance. func (f *FreeRDPBackend) Disconnect() error { f.mu.Lock() defer f.mu.Unlock() if !f.connected { return nil } close(f.stopCh) f.connected = false f.gdiReady = false // Free GDI resources before disconnecting. if f.gdiPrimaryBuf != 0 { procGdiFree.Call(f.instance) f.gdiPrimaryBuf = 0 } procFreerdpDisconnect.Call(f.instance) unregisterInstance(f.instance) procFreerdpFree.Call(f.instance) f.instance = 0 f.context = 0 f.settings = 0 f.input = 0 return nil } // SendMouseEvent forwards a mouse event to the remote RDP session. func (f *FreeRDPBackend) SendMouseEvent(x, y int, flags uint32) error { f.mu.Lock() defer f.mu.Unlock() if !f.connected || f.input == 0 { return fmt.Errorf("not connected") } procInputSendMouse.Call(f.input, uintptr(flags), uintptr(x), uintptr(y)) return nil } // SendKeyEvent forwards a keyboard event to the remote RDP session. The // scancode uses the same format as the ScancodeMap in input.go — extended // keys have the 0xE0 prefix in the high byte. func (f *FreeRDPBackend) SendKeyEvent(scancode uint32, pressed bool) error { f.mu.Lock() defer f.mu.Unlock() if !f.connected || f.input == 0 { return fmt.Errorf("not connected") } var flags uint32 if pressed { flags = KBD_FLAGS_DOWN } else { flags = KBD_FLAGS_RELEASE } if scancode > 0xFF { flags |= KBD_FLAGS_EXTENDED } procInputSendKeyboard.Call(f.input, uintptr(flags), uintptr(scancode&0xFF)) return nil } // SendClipboard sends clipboard text to the remote session. // TODO: Implement via the FreeRDP cliprdr channel. func (f *FreeRDPBackend) SendClipboard(data string) error { return nil } // GetFrame returns the current full-frame RGBA pixel buffer. The frame is // populated from the FreeRDP GDI primary surface during the event loop and // via the PostConnect/BitmapUpdate callbacks. func (f *FreeRDPBackend) GetFrame() ([]byte, error) { if f.buffer == nil { return nil, fmt.Errorf("no frame buffer") } return f.buffer.GetFrame(), nil } // IsConnected reports whether the backend has an active RDP connection. func (f *FreeRDPBackend) IsConnected() bool { f.mu.Lock() defer f.mu.Unlock() return f.connected } // setString sets a string setting on the FreeRDP settings object. func (f *FreeRDPBackend) setString(id int, value string) { b, _ := syscall.BytePtrFromString(value) procSettingsSetString.Call(f.settings, uintptr(id), uintptr(unsafe.Pointer(b))) } // setUint32 sets a uint32 setting on the FreeRDP settings object. func (f *FreeRDPBackend) setUint32(id int, value uint32) { procSettingsSetUint32.Call(f.settings, uintptr(id), uintptr(value)) } // setBool sets a boolean setting on the FreeRDP settings object. func (f *FreeRDPBackend) setBool(id int, value bool) { v := uintptr(0) if value { v = 1 } procSettingsSetBool.Call(f.settings, uintptr(id), v) } // Ensure FreeRDPBackend satisfies the RDPBackend interface at compile time. var _ RDPBackend = (*FreeRDPBackend)(nil)