//go:build linux package server import ( "encoding/binary" "fmt" "slices" "sync" "time" "unicode" "unsafe" log "github.com/sirupsen/logrus" "golang.org/x/sys/unix" ) // /dev/uinput ioctl numbers. Computed from the kernel _IO/_IOW macros so // we don't depend on cgo. UINPUT_IOCTL_BASE = 'U' = 0x55. const ( uiDevCreate = 0x5501 uiDevDestroy = 0x5502 // _IOW('U', 3, struct uinput_setup); uinput_setup is 92 bytes on amd64. uiDevSetup = (1 << 30) | (92 << 16) | (0x55 << 8) | 3 uiSetEvBit = (1 << 30) | (4 << 16) | (0x55 << 8) | 100 uiSetKeyBit = (1 << 30) | (4 << 16) | (0x55 << 8) | 101 uiSetRelBit = (1 << 30) | (4 << 16) | (0x55 << 8) | 102 uiSetAbsBit = (1 << 30) | (4 << 16) | (0x55 << 8) | 103 uinputAbsSize = 64 // legacy struct uses absmin/absmax/absfuzz/absflat[64]. ) // Linux input event types and key codes (linux/input-event-codes.h). const ( evSyn = 0x00 evKey = 0x01 evRel = 0x02 evAbs = 0x03 evRep = 0x14 synReport = 0 // relWheel carries notched vertical scroll, one unit per detent, which is // what the RFB wheel-up/wheel-down button bits describe. relWheel = 0x08 // keyMaxCode is the kernel's KEY_MAX: the largest code UI_SET_KEYBIT // accepts. Anything above it would make the ioctl fail. keyMaxCode = 0x2ff absX = 0x00 absY = 0x01 btnLeft = 0x110 btnRight = 0x111 btnMiddle = 0x112 btnSide = 0x113 // mouse-back (X1) btnExtra = 0x114 // mouse-forward (X2) ) // inputEvent matches struct input_event. The leading timeval is two C longs, // so the struct is 24 bytes on a 64-bit kernel and 16 on a 32-bit one; using // unix.Timeval rather than a fixed pair of int64 keeps the layout right on // both, and a wrong-sized write is rejected outright by uinput. type inputEvent struct { Time unix.Timeval Type uint16 Code uint16 Value int32 } // UInputInjector synthesizes keyboard and mouse events via /dev/uinput. // Used as a fallback when X11 isn't running, e.g. at the kernel console // or pre-login screen on a server without X. Requires root or // CAP_SYS_ADMIN, which the netbird service has. type UInputInjector struct { mu sync.Mutex fd int closeOnce sync.Once keysymToKey map[uint32]uint16 // console is the active console keymap, rune to the key that types it, or // nil when it could not be read and the US table is all there is. console map[rune]consoleKey prevButtons uint16 screenW int screenH int } // NewUInputInjector opens /dev/uinput and registers a virtual keyboard + // absolute pointer device sized to (w, h). The dimensions are needed // because uinput's ABS axes don't autoscale; we always send absolute // coordinates and let the kernel route them to the right monitor. func NewUInputInjector(w, h int) (*UInputInjector, error) { if w <= 0 || h <= 0 { return nil, fmt.Errorf("invalid screen size: %dx%d", w, h) } fd, err := unix.Open("/dev/uinput", unix.O_WRONLY|unix.O_NONBLOCK, 0) if err != nil { return nil, fmt.Errorf("open /dev/uinput: %w", err) } if err := setBit(fd, uiSetEvBit, evKey); err != nil { unix.Close(fd) return nil, err } if err := setBit(fd, uiSetEvBit, evAbs); err != nil { unix.Close(fd) return nil, err } if err := setBit(fd, uiSetEvBit, evSyn); err != nil { unix.Close(fd) return nil, err } // Scroll arrives as the RFB wheel button bits, which have no KEY_ code to // press: the kernel expects a relative wheel delta instead. if err := setBit(fd, uiSetEvBit, evRel); err != nil { unix.Close(fd) return nil, err } if err := setBit(fd, uiSetRelBit, relWheel); err != nil { unix.Close(fd) return nil, err } // Advertise key auto-repeat so the kernel input core repeats held // keys at the configured rate (default ~250 ms delay, ~33 ms period). // Without this, holding Backspace etc. only deletes one character. if err := setBit(fd, uiSetEvBit, evRep); err != nil { unix.Close(fd) return nil, err } console, err := readConsoleKeymap(consoleTTY) if err != nil { log.Debugf("console keymap unavailable, assuming a US layout: %v", err) } keymap := buildUInputKeymap() // The console layout may put characters on keys the fixed table does not // list (the ISO <> key, for one); the device has to advertise those too. keymap = appendMissingCodes(keymap, keymapCodes(console)) for _, key := range keymap { if err := setBit(fd, uiSetKeyBit, uint32(key)); err != nil { unix.Close(fd) return nil, fmt.Errorf("UI_SET_KEYBIT %d: %w", key, err) } } for _, btn := range []uint16{btnLeft, btnRight, btnMiddle, btnSide, btnExtra} { if err := setBit(fd, uiSetKeyBit, uint32(btn)); err != nil { unix.Close(fd) return nil, fmt.Errorf("UI_SET_KEYBIT btn %d: %w", btn, err) } } if err := setBit(fd, uiSetAbsBit, absX); err != nil { unix.Close(fd) return nil, err } if err := setBit(fd, uiSetAbsBit, absY); err != nil { unix.Close(fd) return nil, err } if err := writeUInputUserDev(fd, w, h); err != nil { unix.Close(fd) return nil, err } if _, _, e := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uiDevCreate, 0); e != 0 { unix.Close(fd) return nil, fmt.Errorf("UI_DEV_CREATE: %v", e) } // Give udev a moment to settle before sending events. time.Sleep(50 * time.Millisecond) inj := &UInputInjector{ fd: fd, keysymToKey: overlayConsoleKeymap(keymapByKeysym(keymap), console), console: console, screenW: w, screenH: h, } log.Infof("uinput injector ready: %dx%d, %d keys", w, h, len(inj.keysymToKey)) return inj, nil } func setBit(fd int, op uintptr, code uint32) error { if _, _, e := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), op, uintptr(code)); e != 0 { return fmt.Errorf("ioctl 0x%x %d: %v", op, code, e) } return nil } // writeUInputUserDev uses the legacy uinput_user_dev path (write the // whole struct then UI_DEV_CREATE) which is universally supported on // older and current kernels alike. uinput_user_dev is name(80) + id(8) + // ff_effects_max(4) + absmax/absmin/absfuzz/absflat[64] = 92 + 4*64*4 = // 1116 bytes total. func writeUInputUserDev(fd, w, h int) error { const sz = 80 + 8 + 4 + uinputAbsSize*4*4 buf := make([]byte, sz) copy(buf[0:80], []byte("netbird-vnc-uinput")) // id: BUS_VIRTUAL=0x06, vendor=0x0001, product=0x0001, version=1. binary.LittleEndian.PutUint16(buf[80:82], 0x06) binary.LittleEndian.PutUint16(buf[82:84], 0x0001) binary.LittleEndian.PutUint16(buf[84:86], 0x0001) binary.LittleEndian.PutUint16(buf[86:88], 0x0001) // ff_effects_max(4) at 88..92 stays zero. // absmax[64] at 92..348: set absX/absY. absmaxOff := 80 + 8 + 4 absminOff := absmaxOff + uinputAbsSize*4 binary.LittleEndian.PutUint32(buf[absmaxOff+absX*4:], uint32(w-1)) binary.LittleEndian.PutUint32(buf[absmaxOff+absY*4:], uint32(h-1)) binary.LittleEndian.PutUint32(buf[absminOff+absX*4:], 0) binary.LittleEndian.PutUint32(buf[absminOff+absY*4:], 0) if _, err := unix.Write(fd, buf); err != nil { return fmt.Errorf("write uinput_user_dev: %w", err) } return nil } // emit writes a single input_event to the device. Caller-locked. func (u *UInputInjector) emit(typ, code uint16, value int32) error { ev := inputEvent{Type: typ, Code: code, Value: value} buf := (*[unsafe.Sizeof(inputEvent{})]byte)(unsafe.Pointer(&ev))[:] _, err := unix.Write(u.fd, buf) return err } func (u *UInputInjector) sync() { _ = u.emit(evSyn, synReport, 0) } // InjectKey synthesizes a press or release for the given X11 keysym. func (u *UInputInjector) InjectKey(keysym uint32, down bool) { u.mu.Lock() defer u.mu.Unlock() code, ok := u.keysymToKey[keysym] if !ok { return } u.emitKeyCode(code, down) } // InjectKeyScancode injects a press or release using the QEMU scancode. // uinput speaks Linux KEY_* codes natively, so we map QEMU scancode → // KEY_* via qemuToLinuxKey. On miss (scancode we don't have a mapping // for) we fall back to the keysym path, which is exactly the legacy // behaviour. func (u *UInputInjector) InjectKeyScancode(scancode, keysym uint32, down bool) { code := qemuScancodeToLinuxKey(scancode) if code == 0 { u.InjectKey(keysym, down) return } u.mu.Lock() defer u.mu.Unlock() u.emitKeyCode(uint16(code), down) } // emitKeyCode emits one key down/up event plus a sync. Caller holds u.mu. func (u *UInputInjector) emitKeyCode(code uint16, down bool) { value := int32(0) if down { value = 1 } if err := u.emit(evKey, code, value); err != nil { log.Tracef("uinput emit key: %v", err) return } u.sync() } // InjectPointer moves the absolute pointer and presses/releases buttons // based on the RFB button mask delta against the previous mask. func (u *UInputInjector) InjectPointer(buttonMask uint16, x, y, serverW, serverH int) { u.mu.Lock() defer u.mu.Unlock() if serverW <= 1 || serverH <= 1 { return } absXVal := int32(x * (u.screenW - 1) / (serverW - 1)) absYVal := int32(y * (u.screenH - 1) / (serverH - 1)) _ = u.emit(evAbs, absX, absXVal) _ = u.emit(evAbs, absY, absYVal) type btnMap struct { bit uint16 key uint16 } for _, b := range []btnMap{ {0x01, btnLeft}, {0x02, btnMiddle}, {0x04, btnRight}, {1 << 7, btnSide}, {1 << 8, btnExtra}, } { pressed := buttonMask&b.bit != 0 was := u.prevButtons&b.bit != 0 if pressed && !was { _ = u.emit(evKey, b.key, 1) } else if !pressed && was { _ = u.emit(evKey, b.key, 0) } } // RFB spells a wheel notch as a press of button 4 (up) or 5 (down), // immediately released. There is no KEY_ code for either, so each press // edge becomes one relative wheel detent; the release edge carries no // further movement and is ignored. const ( wheelUpBit = 1 << 3 wheelDownBit = 1 << 4 ) if buttonMask&wheelUpBit != 0 && u.prevButtons&wheelUpBit == 0 { _ = u.emit(evRel, relWheel, 1) } if buttonMask&wheelDownBit != 0 && u.prevButtons&wheelDownBit == 0 { _ = u.emit(evRel, relWheel, -1) } u.prevButtons = buttonMask u.sync() } // SetClipboard is a no-op on the framebuffer console: there is no system // clipboard daemon. Use TypeText (Paste button) to deliver host text. func (u *UInputInjector) SetClipboard(_ string) { // no system clipboard daemon on framebuffer console } // GetClipboard returns empty: no clipboard outside X11/Wayland. func (u *UInputInjector) GetClipboard() string { return "" } // TypeText synthesizes the given UTF-8 text as keystrokes. Only ASCII // printable characters and newline are typed; other runes are skipped. // This drives the "paste" button: with no console clipboard available, // keystroke-by-keystroke entry is the only way to deliver a password to // a TTY login prompt. func (u *UInputInjector) TypeText(text string) { u.mu.Lock() defer u.mu.Unlock() const maxChars = 4096 count := 0 for _, r := range text { if count >= maxChars { break } count++ key, ok := u.keyForTypedRune(r) if !ok { continue } if key.shift { _ = u.emit(evKey, keyLeftShift, 1) } if key.altGr { _ = u.emit(evKey, keyRightAlt, 1) } _ = u.emit(evKey, key.code, 1) _ = u.emit(evKey, key.code, 0) if key.altGr { _ = u.emit(evKey, keyRightAlt, 0) } if key.shift { _ = u.emit(evKey, keyLeftShift, 0) } u.sync() } } // keyForTypedRune returns the key and modifiers that type r: from the console's // own layout when it could be read, and from the US table otherwise. Return // stays a key of its own whatever the layout. func (u *UInputInjector) keyForTypedRune(r rune) (consoleKey, bool) { if r == '\n' || r == '\r' { return consoleKey{code: keyEnter}, true } if k, ok := u.console[r]; ok { return k, true } code, shift, ok := keyForRune(r) return consoleKey{code: code, shift: shift}, ok } // Close destroys the virtual uinput device and closes the file descriptor. func (u *UInputInjector) Close() { u.closeOnce.Do(func() { u.mu.Lock() defer u.mu.Unlock() if u.fd >= 0 { _, _, _ = unix.Syscall(unix.SYS_IOCTL, uintptr(u.fd), uiDevDestroy, 0) _ = unix.Close(u.fd) u.fd = -1 } }) } // Linux KEY_* codes live in scancodes.go (shared with the QEMU scancode // path). Don't duplicate them here. // buildUInputKeymap returns every linux KEY_ code we want the virtual // device to advertise during UI_SET_KEYBIT. Order doesn't matter. func buildUInputKeymap() []uint16 { out := make([]uint16, 0, 128) // Letters: KEY_A=30, KEY_B=48, etc; not a clean range. The kernel's // row-by-row layout is qwertyuiop / asdfghjkl / zxcvbnm. letters := []uint16{ 30, 48, 46, 32, 18, 33, 34, 35, 23, 36, 37, 38, 50, // a..m 49, 24, 25, 16, 19, 31, 20, 22, 47, 17, 45, 21, 44, // n..z } out = append(out, letters...) // Top-row digits: KEY_1..KEY_0 = 2..11. for i := uint16(2); i <= 11; i++ { out = append(out, i) } // Function keys F1..F12 = 59..68 + 87, 88. We only register F1..F12 // which the kernel header enumerates as a contiguous block. for i := uint16(59); i <= 68; i++ { out = append(out, i) } out = append(out, 87, 88) out = append(out, []uint16{ keyEsc, keyMinus, keyEqual, keyBackspace, keyTab, keyEnter, keyLeftCtrl, keyRightCtrl, keyLeftShift, keyRightShift, keyLeftAlt, keyRightAlt, keyLeftMeta, keyRightMeta, keySpace, keyCapsLock, keyLeftBracket, keyRightBracket, keyBackslash, keySemicolon, keyApostrophe, keyGrave, keyComma, keyDot, keySlash, keyHome, keyEnd, keyPageUp, keyPageDown, keyUp, keyDown, keyLeft, keyRight, keyInsert, keyDelete, }...) // The QEMU scancode path can emit anything qemuToLinuxKey maps to: the // keypad, the lock keys, PrintScreen, the volume keys and Compose are all // reachable that way and none of them are listed above. A code the device // never advertised is dropped by the kernel without a word, so the set is // closed over that map rather than maintained by hand alongside it. seen := make(map[uint16]struct{}, len(out)+len(qemuToLinuxKey)) for _, code := range out { seen[code] = struct{}{} } extra := make([]uint16, 0, len(qemuToLinuxKey)) add := func(code int) { if code <= 0 || code > keyMaxCode { return } if _, ok := seen[uint16(code)]; ok { return } seen[uint16(code)] = struct{}{} extra = append(extra, uint16(code)) } for _, code := range qemuToLinuxKey { add(int(code)) } // And over the keysym table, for the same reason: every code InjectKey can // emit has to be one the device advertised. for _, code := range keymapByKeysym(nil) { add(int(code)) } // Sorted so the device registers the same set in the same order on every // run; ranging a map alone would not. slices.Sort(extra) return append(out, extra...) } // keymapByKeysym maps X11 keysyms (the values our session receives over // RFB) onto Linux KEY_ codes. Shifted ASCII keysyms (uppercase letters, // "!@#..." etc.) map to the same scan code as their unshifted twin: the // client also sends a separate Shift keysym (0xffe1), so the kernel // composes the final character from the held modifier + scan code. func keymapByKeysym(_ []uint16) map[uint32]uint16 { letters := map[rune]uint16{ 'a': 30, 'b': 48, 'c': 46, 'd': 32, 'e': 18, 'f': 33, 'g': 34, 'h': 35, 'i': 23, 'j': 36, 'k': 37, 'l': 38, 'm': 50, 'n': 49, 'o': 24, 'p': 25, 'q': 16, 'r': 19, 's': 31, 't': 20, 'u': 22, 'v': 47, 'w': 17, 'x': 45, 'y': 21, 'z': 44, } m := map[uint32]uint16{ // Digits. '0': 11, '1': 2, '2': 3, '3': 4, '4': 5, '5': 6, '6': 7, '7': 8, '8': 9, '9': 10, // Shifted digits (US layout). ')': 11, '!': 2, '@': 3, '#': 4, '$': 5, '%': 6, '^': 7, '&': 8, '*': 9, '(': 10, // Punctuation (US layout) and shifted twins. ' ': keySpace, '-': keyMinus, '_': keyMinus, '=': keyEqual, '+': keyEqual, '[': keyLeftBracket, '{': keyLeftBracket, ']': keyRightBracket, '}': keyRightBracket, '\\': keyBackslash, '|': keyBackslash, ';': keySemicolon, ':': keySemicolon, '\'': keyApostrophe, '"': keyApostrophe, '`': keyGrave, '~': keyGrave, ',': keyComma, '<': keyComma, '.': keyDot, '>': keyDot, '/': keySlash, '?': keySlash, // Special keys (X11 keysyms). 0xff08: keyBackspace, 0xff09: keyTab, 0xff0d: keyEnter, 0xff1b: keyEsc, 0xffff: keyDelete, 0xff50: keyHome, 0xff57: keyEnd, 0xff51: keyLeft, 0xff52: keyUp, 0xff53: keyRight, 0xff54: keyDown, 0xff55: keyPageUp, 0xff56: keyPageDown, 0xff63: keyInsert, 0xffe1: keyLeftShift, 0xffe2: keyRightShift, 0xffe3: keyLeftCtrl, 0xffe4: keyRightCtrl, 0xffe9: keyLeftAlt, 0xffea: keyRightAlt, 0xffeb: keyLeftMeta, 0xffec: keyRightMeta, // Caps_Lock is in stickyModifierKeysyms, so a client that drops // mid-press relies on this entry to get the release delivered. 0xffe5: keyCapsLock, // Meta_L / Meta_R. X11 clients send these as well as Super_L/Super_R // above, and Linux has no separate Meta code. 0xffe7: keyLeftMeta, 0xffe8: keyRightMeta, // AltGr arrives as ISO_Level3_Shift or Mode_switch; on a PC keyboard // both are the right Alt key. 0xfe03: keyRightAlt, 0xff7e: keyRightAlt, // Lock, system and menu keys (linux/input-event-codes.h). 0xff7f: 69, // Num_Lock -> KEY_NUMLOCK 0xff14: 70, // Scroll_Lock -> KEY_SCROLLLOCK 0xff61: 99, // Print -> KEY_SYSRQ 0xff15: 99, // Sys_Req -> KEY_SYSRQ 0xff13: 119, // Pause -> KEY_PAUSE 0xff6b: 119, // Break -> KEY_PAUSE 0xff67: 127, // Menu -> KEY_COMPOSE // Keypad. RFB clients send the digit keysyms with Num Lock on and the // navigation ones with it off; both are the same physical keys. 0xffb0: 82, 0xffb1: 79, 0xffb2: 80, 0xffb3: 81, 0xffb4: 75, // KP_0..KP_4 0xffb5: 76, 0xffb6: 77, 0xffb7: 71, 0xffb8: 72, 0xffb9: 73, // KP_5..KP_9 0xffae: 83, 0xffac: 121, // KP_Decimal, KP_Separator -> KEY_KPDOT, KEY_KPCOMMA 0xffab: 78, 0xffad: 74, // KP_Add, KP_Subtract 0xffaa: 55, 0xffaf: 98, // KP_Multiply, KP_Divide 0xff8d: 96, 0xffbd: 117, // KP_Enter, KP_Equal 0xff9e: 82, 0xff9c: 79, 0xff99: 80, 0xff9b: 81, // KP_Insert, KP_End, KP_Down, KP_Next 0xff96: 75, 0xff9d: 76, 0xff98: 77, // KP_Left, KP_Begin, KP_Right 0xff95: 71, 0xff97: 72, 0xff9a: 73, // KP_Home, KP_Up, KP_Prior 0xff9f: 83, // KP_Delete } // Letters: register both lowercase and uppercase keysyms onto the same // KEY_ code. The client sends Shift separately for uppercase. for r, code := range letters { m[uint32(r)] = code m[uint32(r-'a'+'A')] = code } // Function keys F1..F12 (X11 keysyms 0xffbe..0xffc9 → KEY_F1..KEY_F12). xF := uint32(0xffbe) codes := []uint16{59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 87, 88} for i, c := range codes { m[xF+uint32(i)] = c } return m } // keyForRune maps a printable rune to (keycode, needsShift). Used by // TypeText to synthesize keystrokes for a paste payload. func keyForRune(r rune) (uint16, bool, bool) { if r >= 'a' && r <= 'z' { m := map[rune]uint16{ 'a': 30, 'b': 48, 'c': 46, 'd': 32, 'e': 18, 'f': 33, 'g': 34, 'h': 35, 'i': 23, 'j': 36, 'k': 37, 'l': 38, 'm': 50, 'n': 49, 'o': 24, 'p': 25, 'q': 16, 'r': 19, 's': 31, 't': 20, 'u': 22, 'v': 47, 'w': 17, 'x': 45, 'y': 21, 'z': 44, } return m[r], false, true } if r >= 'A' && r <= 'Z' { c, _, ok := keyForRune(unicode.ToLower(r)) return c, true, ok } if r >= '0' && r <= '9' { nums := []uint16{11, 2, 3, 4, 5, 6, 7, 8, 9, 10} idx := int(r - '0') if idx < 0 || idx >= len(nums) { //nolint:gosec // explicit bound disarms G602 return 0, false, false } return nums[idx], false, true } if r == '\n' || r == '\r' { return keyEnter, false, true } if k, ok := punctUnshifted[r]; ok { return k, false, true } if k, ok := punctShifted[r]; ok { return k, true, true } return 0, false, false } // punctUnshifted maps ASCII punctuation that needs no Shift to its uinput // KEY_* code. Split out of keyForRune's switch to keep the function's // cognitive complexity below Sonar's threshold. var punctUnshifted = map[rune]uint16{ ' ': keySpace, '\t': keyTab, '-': keyMinus, '=': keyEqual, '[': keyLeftBracket, ']': keyRightBracket, '\\': keyBackslash, ';': keySemicolon, '\'': keyApostrophe, '`': keyGrave, ',': keyComma, '.': keyDot, '/': keySlash, } // punctShifted maps ASCII punctuation that requires Shift to its base KEY_* // code; the caller adds the shift modifier itself. var punctShifted = map[rune]uint16{ '!': 2, '@': 3, '#': 4, '$': 5, '%': 6, '^': 7, '&': 8, '*': 9, '(': 10, ')': 11, '_': keyMinus, '+': keyEqual, '{': keyLeftBracket, '}': keyRightBracket, '|': keyBackslash, ':': keySemicolon, '"': keyApostrophe, '~': keyGrave, '<': keyComma, '>': keyDot, '?': keySlash, } var _ InputInjector = (*UInputInjector)(nil) // overlayConsoleKeymap replaces the US-table entry for every printable Latin-1 // keysym with the key the console's layout uses for that character. X11 keysyms // in that range are the characters themselves, so the console map applies // directly; the client sends the modifiers separately. Without a console map // the table is returned unchanged. func overlayConsoleKeymap(table map[uint32]uint16, console map[rune]consoleKey) map[uint32]uint16 { for r, k := range console { if r < 0x20 || r > 0xff || r == 0x7f { continue } table[uint32(r)] = k.code } return table } // appendMissingCodes appends each of extra not already in codes. func appendMissingCodes(codes, extra []uint16) []uint16 { seen := make(map[uint16]struct{}, len(codes)) for _, c := range codes { seen[c] = struct{}{} } for _, c := range extra { if c == 0 || c > keyMaxCode { continue } if _, ok := seen[c]; ok { continue } seen[c] = struct{}{} codes = append(codes, c) } return codes }