伪造用于测试目的的输入设备

时间:2018-12-05 18:58:12

标签: c linux evdev input-devices

我想做什么

我正在编写一个守护程序,该守护程序监听输入设备的按键并通过D-Bus发送信号。主要目标是通过请求更改或通知更改来管理音频音量和屏幕背光亮度。 我使用 libevdev 处理输入设备事件。

我编写了一个用于打开位于指定路径的输入设备的函数:

Device device_open(const char *path);

该函数运行良好,但是当我为此编写单元测试时,我想创建具有不同属性(文件的存在,读取访问等)的文件固定装置,以检查函数和内存的错误处理管理(因为我将数据存储在结构中)。

我已经做过的事

但是使用真实的输入设备(位于/ dev / input / event *)进行测试需要 root 访问权限。在/ dev / input / event *文件上为每个人设置读取访问权限都可以,但是对我来说似乎很冒险。以root用户身份执行我的测试更糟糕!

使用mknod创建设备是可行的,但需要以root用户身份完成。

我还尝试使用字符特殊文件(因为输入设备是其中的一种)允许所有人读取(例如/ dev / random,/ dev / zero,/ dev / null甚至是我当前使用的终端设备) :/ dev / tty2)。

但是这些设备无法处理libevdev所需的ioctl请求: EVIOCGBIT 是第一个返回错误“设备的不合适的ioctl”的请求。

我在寻找什么

我希望能够以普通用户(执行单元测试的用户)创建设备文件。然后,通过设置访问权限,我应该能够测试不同类型文件的功能行为(只读,不允许读取,错误的设备类型等)。 如果看起来不可能,我肯定会使用私人助手来重构我的功能。但是怎么做。有例子吗?

谢谢。

编辑:我试图更好地表达自己的需求。

1 个答案:

答案 0 :(得分:4)

为允许访问该设备的用户创建一个组,并使用udev规则将该输入事件设备的所有权设置为该组。


我使用teensy(系统)组:

sudo groupadd -r teensy

并使用例如

将每个用户添加到其中
sudo usermod -a -g teensy my-user-name

或我可用的任何图形用户界面。

通过管理哪些用户和服务守护程序属于teensy组,您可以轻松地管理对设备的访问。


对于我的Teensy微控制器(具有本地USB,并且用于HID测试),我具有以下/lib/udev/rules.d/49-teensy.rules

ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789B]?", ENV{ID_MM_DEVICE_IGNORE}="1"
ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789A]?", ENV{MTP_NO_PROBE}="1"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789ABCD]?", GROUP:="teensy", MODE:="0660"
KERNEL=="ttyACM*", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="04[789B]?", GROUP:="teensy", MODE:="0660"

不过,对于HID设备,您只需要第三行(SUBSYSTEMS=="usb",一个)。确保idVendoridProduct与您的USB HID设备匹配。您可以使用lsusb列出当前连接的USB设备供应商和产品编号。匹配使用glob模式,就像文件名一样。

添加以上内容后,不要忘记运行sudo udevadm control --reload-rules && sudo udevadm trigger以重新加载规则。下次插入USB HID设备时,组中的所有成员(上述teensy)都可以直接访问它。


请注意,默认情况下,在大多数发行版中,udev还会使用USB设备类型和串行在/dev/input/by-id/中创建持久性符号链接。在我的情况下,我的Teensy LC(串行4298820)中的一个结合了键盘鼠标喜乐设备,为键盘事件设备提供/dev/input/by-id/usb-Teensyduino_Keyboard_Mouse_Joystick_4298820-event-kbd,为鼠标事件设备提供/dev/input/by-id/usb-Teensyduino_Keyboard_Mouse_Joystick_4298820-if01-event-mouse,为{{1} }和/dev/input/by-id/usb-Teensyduino_Keyboard_Mouse_Joystick_4298820-if03-event-joystick用于两个游戏杆界面。

(通过“持久”,我并不意味着这些符号链接始终存在;我的意思是,每当插入该特定设备时,都会存在该名称的符号链接,并指向实际的Linux输入事件字符设备。)< / p>


可以使用Linux uinput设备通过简单的特权守护程序来实现虚拟输入事件设备。

创建新的虚拟USB输入事件设备的过程如下。

  1. 打开/dev/input/by-id/usb-Teensyduino_Keyboard_Mouse_Joystick_4298820-if04-event-joystick进行写作(或阅读和写作):

    /dev/uinput

    以上要求具有超级用户特权。但是,打开设备后,您可以立即放弃所有特权,并让守护进程/服务以专用用户的身份运行。

  2. 对允许的每种事件类型使用fd = open("/dev/uinput", O_RDWR); if (fd == -1) { fprintf(stderr, "Cannot open /dev/uinput: %s.\n", strerror(errno)); exit(EXIT_FAILURE); } ioctl。

    您将希望至少允许UI_SET_EVBIT;和EV_SYN用于键盘和鼠标按钮,EV_KEY用于鼠标移动,等等。

    EV_REL

    我个人使用带有代码的静态常量数组,以便于管理。

  3. 对设备可能发出的每个键码使用if (ioctl(fd, UI_SET_EVBIT, EV_SYN) == -1 || ioctl(fd, UI_SET_EVBIT, EV_KEY) == -1 || ioctl(fd, UI_SET_EVBIT, EV_REL) == -1) { fprintf(stderr, "Uinput event types not allowed: %s.\n", strerror(errno)); close(fd); exit(EXIT_FAILURE); } ioctl,对每个相对移动码(鼠标代码)使用UI_SET_KEYBIT ioctl。例如,要留出空间,请使用鼠标左键,鼠标水平和垂直移动以及鼠标滚轮:

    UI_SET_RELBIT

    同样,静态常量数组(一个用于if (ioctl(fd, UI_SET_KEYBIT, KEY_SPACE) == -1 || ioctl(fd, UI_SET_KEYBIT, BTN_LEFT) == -1 || ioctl(fd, UI_SET_RELBIT, REL_X) == -1 || ioctl(fd, UI_SET_RELBIT, REL_Y) == -1 || ioctl(fd, UI_SET_RELBIT, REL_WHEEL) == -1) { fprintf(stderr, "Uinput event types not allowed: %s.\n", strerror(errno)); close(fd); exit(EXIT_FAILURE); } 和一个用于UI_SET_KEYBIT代码)更易于维护。

  4. 定义一个UI_SET_RELBIT,并将其写入设备。

    如果struct uinput_user_dev包含设备名称字符串,namevendor以及USB供应商和产品ID号,则product包含版本号(0即可),使用

    version

    以后的内核都有一个ioctl来做同样的事情(显然,参与systemd开发会导致这种浪费);

    struct uinput_user_dev  dev;
    
    memset(&dev, 0, sizeof dev);
    strncpy(dev.name, name, UINPUT_MAX_NAME_SIZE);
    dev.id.bustype = BUS_USB;
    dev.id.vendor = vendor;
    dev.id.product = product;
    dev.id.version = version;
    
    if (write(fd, &dev, sizeof dev) != sizeof dev) {
        fprintf(stderr, "Cannot write an uinput device description: %s.\n", strerror(errno));
        close(fd);
        exit(EXIT_FAILURE);
    }
    

    这个想法似乎是,您可以先尝试使用前者,而不是使用前者,如果失败,则改为使用前者。您知道,因为有一天某个接口可能还不够。 (无论如何,documentationcommit就是这样。)

    在这里,我可能听起来有些胡思乱想,但这仅仅是因为我同时订阅了Unix philosophyKISS principle(或minimalist方法),并且看到了完全不需要这样的疣。而且常常来自同一松散相关的开发人员组。啊没有故意的侮辱;我只是认为他们做得不好。

  5. 通过发布struct uinput_setup dev; memset(&dev, 0, sizeof dev); strncpy(dev.name, name, UINPUT_MAX_NAME_SIZE); dev.id.bustype = BUS_USB; dev.id.vendor = vendor; dev.id.product = product; dev.id.version = version; if (ioctl(fd, UI_DEV_SETUP, &dev) == -1) { fprintf(stderr, "Cannot write an uinput device description: %s.\n", strerror(errno)); close(fd); exit(EXIT_FAILURE); } ioctl创建虚拟设备:

    UI_DEV_CREATE

    这时,内核将构造设备,并将相应的事件提供给udev守护程序,udev守护程序将根据其配置来构造设备节点和symlink。所有这一切都将花费一些时间-在现实世界中只有一秒钟的时间,但是足以使尝试立即发出事件可能导致其中一些事件丢失。

  6. 通过写入uinput设备来发出输入事件(if (ioctl(fd, UI_DEV_CREATE) == -1) { fprintf(stderr, "Cannot create the virtual uinput device: %s.\n", strerror(errno)); close(fd); exit(EXIT_FAILURE); }

    您一次可以写一个或多个struct input_event,并且永远不会看到简短的写法(除非您尝试编写部分事件结构)。部分事件结构被完全忽略。 (有关内核如何处理此类写入的信息,请参见drivers/input/misc/uinput.c:uinput_write() uinput_inject_events()。)

    许多动作由一个以上的struct input_event组成。例如,您可以对角移动鼠标(一次移动同时发射struct input_event{ .type == EV_REL, .code == REL_X, .value = xdelta })。同步事件({ .type == EV_REL, .code == REL_Y, .value = ydelta })用作标记或分隔符,表示相关事件的结束。

    因此,您需要在每个单独的操作(鼠标移动,按键,释放键等)之后附加一个{ .type == EV_SYN, .code == 0, .value == 0 }输入事件。对于行缓冲输入,可以将其视为换行符。

    例如,以下代码将鼠标沿对角线右下移动一个像素。

    { .type == EV_SYN, .code == 0, .value == 0 }

    失败案例并非致命;这仅表示未注入事件(尽管我看不到在当前内核中会如何发生;为防万一,最好采取防御措施)。您可以简单地重试一次,或者忽略失败(但是让用户知道,以便他们进行调查,如果发生了)。因此,将其记录下来或输出警告,但不需要它导致守护程序/服务退出。

  7. 销毁设备:

    struct input_event  event[3];
    memset(event, 0, sizeof event);
    
    event[0].type  = EV_REL;
    event[0].code  = REL_X;
    event[0].value = +1; /* Right */
    
    event[1].type  = EV_REL;
    event[1].code  = REL_Y;
    event[1].value = +1; /* Down */
    
    event[2].type  = EV_SYN;
    event[2].code  = 0;
    event[2].value = 0;
    
    if (write(fd, event, sizeof event) != sizeof event)
        fprintf(stderr, "Failed to inject mouse movement event.\n");
    

    当原始打开的描述符的最后一个副本关闭时,该设备确实会自动销毁,但我建议如上所述进行显式操作。

将第1-5步放入一个函数中,您将得到类似

ioctl(fd, UI_DEV_DESTROY);
close(fd);

看起来不错,不需要任何库(标准C库除外)。

重要的是要意识到Linux输入子系统(包括uinput和#define _POSIX_C_SOURCE 200809L #define _GNU_SOURCE #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <linux/uinput.h> #include <string.h> #include <errno.h> #include <stdio.h> static const unsigned int allow_event_type[] = { EV_KEY, EV_SYN, EV_REL, }; #define ALLOWED_EVENT_TYPES (sizeof allow_event_type / sizeof allow_event_type[0]) static const unsigned int allow_key_code[] = { KEY_SPACE, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, }; #define ALLOWED_KEY_CODES (sizeof allow_key_code / sizeof allow_key_code[0]) static const unsigned int allow_rel_code[] = { REL_X, REL_Y, REL_WHEEL, }; #define ALLOWED_REL_CODES (sizeof allow_rel_code / sizeof allow_rel_code[0]) static int uinput_open(const char *name, const unsigned int vendor, const unsigned int product, const unsigned int version) { struct uinput_user_dev dev; int fd; size_t i; if (!name || strlen(name) < 1 || strlen(name) >= UINPUT_MAX_NAME_SIZE) { errno = EINVAL; return -1; } fd = open("/dev/uinput", O_RDWR); if (fd == -1) return -1; memset(&dev, 0, sizeof dev); strncpy(dev.name, name, UINPUT_MAX_NAME_SIZE); dev.id.bustype = BUS_USB; dev.id.vendor = vendor; dev.id.product = product; dev.id.version = version; do { for (i = 0; i < ALLOWED_EVENT_TYPES; i++) if (ioctl(fd, UI_SET_EVBIT, allow_event_type[i]) == -1) break; if (i < ALLOWED_EVENT_TYPES) break; for (i = 0; i < ALLOWED_KEY_CODES; i++) if (ioctl(fd, UI_SET_KEYBIT, allow_key_code[i]) == -1) break; if (i < ALLOWED_KEY_CODES) break; for (i = 0; i < ALLOWED_REL_CODES; i++) if (ioctl(fd, UI_SET_RELBIT, allow_rel_code[i]) == -1) break; if (i < ALLOWED_REL_CODES) break; if (write(fd, &dev, sizeof dev) != sizeof dev) break; if (ioctl(fd, UI_DEV_CREATE) == -1) break; /* Success. */ return fd; } while (0); /* FAILED: */ { const int saved_errno = errno; close(fd); errno = saved_errno; return -1; } } static void uinput_close(const int fd) { ioctl(fd, UI_DEV_DESTROY); close(fd); } )是Linux内核的 binary接口,因此将保持向后兼容(除非按技术原因,例如安全性问题或与内核其他部分的严重冲突)。 (将所有内容包装在freedesktop.org或systemd保护伞下的愿望不是一个。)