mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-23 23:29:08 +02:00
Map uinput keysyms and typed text through the console's active keymap, and cover the keypad, lock and AltGr keysyms
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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'])
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user