Подделка устройства ввода для целей тестирования

Что я хочу сделать

Я пишу демон, который слушает устройства ввода для нажатия клавиш и отправляет сигналы через 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).

Но эти устройства не обрабатывает ioctl запросы, необходимые для libevdev: EVIOCGBIT является первым запросом, возвращающим ошибку "Неправильный ioctl для устройства".

Что я ищу

Я хочу иметь возможность создавать файлы устройств как обычный пользователь (пользователь, выполняющий модульные тесты). Затем, установив права доступа, я смогу проверить поведение своей функции для разных типов файлов (только чтение, чтение не разрешено, неправильный тип устройства и т. Д.). Если это окажется невозможным, я, безусловно, проведу рефакторинг своей функции с помощью личных помощников. Но как это сделать. Есть примеры?

Благодарю.

Изменить: я пытался лучше выразить свои потребности.

1 ответ

Решение

Создайте группу для пользователей, которым разрешен доступ к устройству, и правило 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"

Вам нужна только третья строка (SUBSYSTEMS=="usb", 1) для устройств HID. Убедитесь, что idVendor а также idProduct соответствует вашему USB HID устройству. Ты можешь использовать lsusb перечислить текущих подключенных USB-устройств и номера продуктов. При сопоставлении используются шаблоны глобуса, как и имена файлов.

После добавления вышесказанного не забудьте запустить sudo udevadm control --reload-rules && sudo udevadm trigger перезагрузить правила. В следующий раз, когда вы подключите устройство USB HID, все члены вашей группы (teensy в приведенном выше) можете получить к нему доступ напрямую.


Обратите внимание, что по умолчанию в большинстве дистрибутивов udev также создает постоянные символические ссылки в /dev/input/by-id/ используя тип устройства USB и серийный номер. В моем случае один из моих 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 для устройства событий мыши, и /dev/input/by-id/usb-Teensyduino_Keyboard_Mouse_Joystick_4298820-if03-event-joystick а также /dev/input/by-id/usb-Teensyduino_Keyboard_Mouse_Joystick_4298820-if04-event-joystick для двух интерфейсов джойстика.

(Под "постоянным" я не подразумеваю, что эти символические ссылки всегда существуют; я имею в виду, что всякий раз, когда это конкретное устройство подключено, символическая ссылка именно с таким именем существует и указывает на фактическое символьное устройство ввода события Linux).


Linux uinput device может использоваться для реализации виртуального устройства ввода событий с использованием простого привилегированного демона.

Процесс создания нового виртуального устройства ввода USB-событий происходит следующим образом.

  1. открыто /dev/uinput для письма (или чтения и письма):

    fd = open("/dev/uinput", O_RDWR);
    if (fd == -1) {
        fprintf(stderr, "Cannot open /dev/uinput: %s.\n", strerror(errno));
        exit(EXIT_FAILURE);
    }
    

    Вышесказанное требует привилегий суперпользователя. Однако сразу после открытия устройства вы можете отказаться от всех привилегий, и вместо этого ваш демон / служба будет работать как выделенный пользователь.

  2. Использовать UI_SET_EVBIT ioctl для каждого типа событий допускается.

    Вы хотите разрешить хотя бы EV_SYN; а также EV_KEY для клавиатур и кнопок мыши, и EV_REL для движения мыши и так далее.

    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);
    }
    

    Я лично использую статический константный массив с кодами для более легкого управления.

  3. Использовать UI_SET_KEYBIT ioctl для каждого кода ключа, который может выдавать устройство, и UI_SET_RELBIT ioctl для каждого кода относительного движения (код мыши). Например, чтобы оставить пробел, левую кнопку мыши, горизонтальное и вертикальное движение мыши и колесо мыши:

    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 и один для UI_SET_RELBIT коды) гораздо проще в обслуживании.

  4. Определить struct uinput_user_devи запишите это на устройство.

    Если у вас есть name содержащий строку имени устройства, vendor а также product с USB-поставщиком и номерами продуктов, version с номером версии (0 в порядке), используйте

    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);
    }
    

    Более поздние ядра имеют ioctl, чтобы делать то же самое (очевидно, что участие в разработке systemd приводит к такому "сливу");

    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);
    }
    

    Идея состоит в том, что вместо того, чтобы использовать первое, вы можете сначала попробовать второе, а в случае неудачи сделайте первое. Вы знаете, потому что одного интерфейса может быть недостаточно. ( Во всяком случае, так говорится в документации и коммите.)

    Я могу показаться немного капризным здесь, но это только потому, что я подписываюсь и на философию Unix, и на принцип KISS (или минималистский подход) и считаю такие бородавки совершенно ненужными. И слишком часто из той же слабо связанной группы разработчиков. Гм. Личное оскорбление не предусмотрено; Я просто думаю, что они делают плохую работу.

  5. Создайте виртуальное устройство, выполнив UI_DEV_CREATE IOCTL:

    if (ioctl(fd, UI_DEV_CREATE) == -1) {
        fprintf(stderr, "Cannot create the virtual uinput device: %s.\n", strerror(errno));
        close(fd);
        exit(EXIT_FAILURE);
    }
    

    На этом этапе ядро ​​создаст устройство, предоставит соответствующее событие демону udev, а демон udev создаст узел устройства и символическую ссылку (ссылки) в соответствии с его конфигурацией. Все это займет немного времени - доли секунды в реальном мире, но этого достаточно, чтобы попытка немедленно вызвать события могла привести к потере некоторых из них.

  6. Испускать входные события (struct input_event) записав на входное устройство.

    Вы можете написать один или несколько struct input_events за один раз, и никогда не должны видеть короткие записи (если вы не пытаетесь написать частичную структуру события). Частичные структуры событий полностью игнорируются. (См. Драйверы /input/misc/uinput.c:uinput_write() uinput_inject_events(), чтобы узнать, как ядро ​​обрабатывает такие записи.)

    Много действий состоит из более чем одного 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 } событие ввода после каждого отдельного действия (движение мыши, нажатие клавиши, отпускание клавиши и т. д.). Думайте об этом как о эквиваленте новой строки для буферизованного ввода.

    Например, следующий код перемещает мышь по диагонали вниз на один пиксель вправо.

    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");
    

    Случай неудачи не смертелен; это только означает, что события не были введены (хотя я не понимаю, как это могло бы произойти в современных ядрах; на всякий случай лучше защититься). Вы можете просто повторить то же самое снова или проигнорировать сбой (но сообщите об этом пользователю, чтобы он мог провести расследование, если это когда-нибудь произойдет). Так что регистрируйте его или выводите предупреждение, но нет необходимости вызывать демон / сервис для выхода.

  7. Уничтожить устройство:

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

    Устройство автоматически уничтожается, когда закрывается последний дубликат исходного открытого дескриптора, но я рекомендую сделать это явно, как указано выше.

Помещая шаги 1-5 в функцию, вы получаете что-то вроде

#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);
}

который, кажется, работает нормально, и не требует никаких библиотек (кроме стандартной библиотеки C).

Важно понимать, что подсистема ввода Linux, включая uinput и struct input_eventявляются двоичными интерфейсами к ядру Linux и, следовательно, будут обратно совместимыми (за исключением неотложных технических причин, таких как проблемы безопасности или серьезные конфликты с другими частями ядра). (Желание обернуть все под зонтик freedesktop.org или systemd не одно).

Другие вопросы по тегам