diff --git a/client/vnc/server/input_uinput_keymap_linux.go b/client/vnc/server/input_uinput_keymap_linux.go new file mode 100644 index 000000000..7baf76769 --- /dev/null +++ b/client/vnc/server/input_uinput_keymap_linux.go @@ -0,0 +1,130 @@ +//go:build linux + +package server + +import ( + "errors" + "fmt" + "unsafe" + + "golang.org/x/sys/unix" +) + +// consoleTTY is the console device whose keymap uinput events are decoded +// with: /dev/tty0 always names the foreground virtual terminal. +const consoleTTY = "/dev/tty0" + +// kdgkbent is KDGKBENT from linux/kd.h, which reads one keymap entry. +const kdgkbent = 0x4B46 + +// Keymap tables (linux/keyboard.h): the character a key produces unmodified, +// with Shift, and with AltGr. +const ( + kNormTab = 0 + kShiftTab = 1 + kAltGrTab = 2 +) + +// Key types (linux/keyboard.h). KT_LATIN and KT_LETTER carry a Latin-1 +// character in the low byte; ktNrTypes is NR_TYPES, one past the last real +// type, which is how Unicode entries are told apart. +const ( + ktLatin = 0 + ktLetter = 11 + ktNrTypes = 15 +) + +// kbEntry mirrors struct kbentry. +type kbEntry struct { + table uint8 + index uint8 + value uint16 +} + +// consoleKey is the key that types a character on the console's active layout, +// and the modifier that has to be held for it. +type consoleKey struct { + code uint16 + shift bool + altGr bool +} + +// readConsoleKeymap reads the active console keymap and returns, for every +// printable character it can type, the key that types it. The unmodified table +// wins over the Shift one, and both over AltGr, so a character reachable more +// than one way gets the simplest. +// +// The kernel decodes uinput key codes with this same keymap, which is what makes +// it the right source. A fixed US table types the wrong characters on any other +// layout, including into a password prompt on the console. +func readConsoleKeymap(tty string) (map[rune]consoleKey, error) { + fd, err := unix.Open(tty, unix.O_RDONLY|unix.O_NOCTTY, 0) + if err != nil { + return nil, fmt.Errorf("open %s: %w", tty, err) + } + defer unix.Close(fd) + + tables := []struct { + table uint8 + key consoleKey + }{ + {kNormTab, consoleKey{}}, + {kShiftTab, consoleKey{shift: true}}, + {kAltGrTab, consoleKey{altGr: true}}, + } + + out := make(map[rune]consoleKey) + for _, tab := range tables { + for code := 1; code < 256; code++ { + e := kbEntry{table: tab.table, index: uint8(code)} + if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), kdgkbent, uintptr(unsafe.Pointer(&e))); errno != 0 { + if tab.table == kNormTab && code == 1 { + return nil, fmt.Errorf("KDGKBENT on %s: %w", tty, errno) + } + continue + } + r, ok := consoleKeymapRune(e.value) + if !ok { + continue + } + if _, taken := out[r]; taken { + continue + } + key := tab.key + key.code = uint16(code) + out[r] = key + } + } + if len(out) == 0 { + return nil, errors.New("console keymap has no printable entries") + } + return out, nil +} + +// consoleKeymapRune decodes a KDGKBENT value into the character it types. +// Unicode entries come back as the code point XOR 0xf000, which leaves their +// type byte at NR_TYPES or above, the same test dumpkeys applies; Latin-1 ones +// carry a real type and the character in the low byte. Everything else +// (function keys, modifiers, dead keys, holes) types no character of its own. +func consoleKeymapRune(v uint16) (rune, bool) { + var r rune + switch t := v >> 8; { + case t >= ktNrTypes: + r = rune(v ^ 0xf000) + case t == ktLatin || t == ktLetter: + r = rune(v & 0xff) + default: + return 0, false + } + return r, r >= 0x20 && r != 0x7f +} + +// keymapCodes returns the key codes a console keymap uses, so the uinput device +// can advertise every one of them. +func keymapCodes(km map[rune]consoleKey) []uint16 { + codes := make([]uint16, 0, len(km)) + for _, k := range km { + codes = append(codes, k.code) + } + return codes +} diff --git a/client/vnc/server/input_uinput_keymap_linux_test.go b/client/vnc/server/input_uinput_keymap_linux_test.go new file mode 100644 index 000000000..476c51dcc --- /dev/null +++ b/client/vnc/server/input_uinput_keymap_linux_test.go @@ -0,0 +1,62 @@ +//go:build linux + +package server + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// KDGKBENT values come in two encodings: a type byte with a Latin-1 character +// below it, or a Unicode code point XOR 0xf000. Anything that is not a +// character (function keys, modifiers, holes) must not map to one. +func TestConsoleKeymapRune(t *testing.T) { + tests := []struct { + name string + value uint16 + want rune + wantOK bool + }{ + {"latin a", 0x0061, 'a', true}, + {"letter a (caps-lock aware)", 0x0b61, 'a', true}, + {"unicode e-acute", 0x00e9 ^ 0xf000, 'é', true}, + {"unicode euro", 0x20ac ^ 0xf000, '€', true}, + {"unicode y-umlaut", 0x00ff ^ 0xf000, 'ÿ', true}, + {"function key", 0x0100, 0, false}, + {"braille type is not unicode", 0x0e01, 0, false}, + {"hole", 0x0200, 0, false}, + {"control char", 0x0009, 0, false}, + {"delete", 0x007f, 0, false}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, ok := consoleKeymapRune(tc.value) + assert.Equal(t, tc.wantOK, ok, "decodes to a character") + if tc.wantOK { + assert.Equal(t, tc.want, got, "decoded character") + } + }) + } +} + +// On a German console 'y' and 'z' swap places. The keysym table has to follow +// the console, or every z typed remotely comes out as a y. +func TestOverlayConsoleKeymap_FollowsLayout(t *testing.T) { + const keyY, keyZ = 21, 44 + table := map[uint32]uint16{'y': keyY, 'z': keyZ, 0xff0d: keyEnter} + german := map[rune]consoleKey{'z': {code: keyY}, 'y': {code: keyZ}, 'Z': {code: keyY, shift: true}} + + got := overlayConsoleKeymap(table, german) + + assert.Equal(t, uint16(keyY), got['z'], "z is on the US y key on a German layout") + assert.Equal(t, uint16(keyZ), got['y'], "y is on the US z key on a German layout") + assert.Equal(t, uint16(keyY), got['Z'], "uppercase follows its key; the client sends Shift itself") + assert.Equal(t, uint16(keyEnter), got[0xff0d], "non-character keysyms are left alone") +} + +// Without a console keymap the US table stands. +func TestOverlayConsoleKeymap_NilKeepsTable(t *testing.T) { + table := map[uint32]uint16{'y': 21} + assert.Equal(t, uint16(21), overlayConsoleKeymap(table, nil)['y']) +} diff --git a/client/vnc/server/input_uinput_linux.go b/client/vnc/server/input_uinput_linux.go index 1f8ab5bf0..2b708f8d8 100644 --- a/client/vnc/server/input_uinput_linux.go +++ b/client/vnc/server/input_uinput_linux.go @@ -77,6 +77,9 @@ type UInputInjector struct { 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 @@ -125,7 +128,15 @@ func NewUInputInjector(w, h int) (*UInputInjector, error) { 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) @@ -160,7 +171,8 @@ func NewUInputInjector(w, h int) (*UInputInjector, error) { inj := &UInputInjector{ fd: fd, - keysymToKey: keymapByKeysym(keymap), + keysymToKey: overlayConsoleKeymap(keymapByKeysym(keymap), console), + console: console, screenW: w, screenH: h, } @@ -331,22 +343,42 @@ func (u *UInputInjector) TypeText(text string) { break } count++ - code, shift, ok := keyForRune(r) + key, ok := u.keyForTypedRune(r) if !ok { continue } - if shift { + if key.shift { _ = u.emit(evKey, keyLeftShift, 1) } - _ = u.emit(evKey, code, 1) - _ = u.emit(evKey, code, 0) - if shift { + 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() { @@ -407,16 +439,24 @@ func buildUInputKeymap() []uint16 { seen[code] = struct{}{} } extra := make([]uint16, 0, len(qemuToLinuxKey)) - for _, code := range qemuToLinuxKey { + add := func(code int) { if code <= 0 || code > keyMaxCode { - continue + return } if _, ok := seen[uint16(code)]; ok { - continue + 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) @@ -471,6 +511,29 @@ func keymapByKeysym(_ []uint16) map[uint32]uint16 { // 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. @@ -554,3 +617,37 @@ var punctShifted = map[rune]uint16{ } 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 +}