//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") // 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 procCheckEventHandles = libfreerdp.NewProc("freerdp_check_event_handles") // Client helpers procClientNew = libfreerdpClient.NewProc("freerdp_client_context_new") procClientFree = libfreerdpClient.NewProc("freerdp_client_context_free") ) // FreeRDP settings IDs (from FreeRDP3 freerdp/settings.h) 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_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 ) // 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* settings uintptr // rdpSettings* input uintptr // rdpInput* buffer *PixelBuffer connected bool config RDPConfig mu sync.Mutex stopCh chan struct{} } // 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. 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 // Create a bare FreeRDP instance. ret, _, err := procFreerdpNew.Call() if ret == 0 { return fmt.Errorf("freerdp_new failed: %v", err) } f.instance = ret // Configure connection settings via the settings accessor functions. 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) // Allocate the pixel buffer for frame capture. f.buffer = NewPixelBuffer(width, height) // TODO: Register PostConnect callback to set up bitmap update handler. // TODO: Register BitmapUpdate callback to write frames into f.buffer. // Initiate the RDP connection. ret, _, err = procFreerdpConnect.Call(f.instance) if ret == 0 { procFreerdpFree.Call(f.instance) f.instance = 0 return fmt.Errorf("freerdp_connect failed: %v", err) } f.connected = true // Start the event processing loop in a background goroutine. 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 } procCheckEventHandles.Call(f.instance) f.mu.Unlock() time.Sleep(16 * time.Millisecond) // ~60 fps } } } // 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 procFreerdpDisconnect.Call(f.instance) procFreerdpFree.Call(f.instance) f.instance = 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 by bitmap update callbacks registered during PostConnect. 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 instance. 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 instance. func (f *FreeRDPBackend) setUint32(id int, value uint32) { procSettingsSetUint32.Call(f.settings, uintptr(id), uintptr(value)) } // setBool sets a boolean setting on the FreeRDP instance. 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)