/* * appsandbox-input.c — Linux input injection for AppSandbox. * * Listens on AF_VSOCK port 3, sends IRDY on accept, and translates * incoming ASIN InputPackets into /dev/uinput events. The host stack * already speaks this protocol (see src/backend_win/vm_display_idd.c). * * Single virtual device exposing absolute pointer, wheel, buttons, and * full keyboard. GNOME / Mutter (Wayland) and Xorg both pick it up via * libinput automatically because uinput presents a real evdev node. * * Wire protocol (host → guest): * #pragma pack(1) * { uint32 magic=0x4E495341 ('ASIN'), uint32 type, * uint32 p1, uint32 p2, uint32 p3 } * * type=0 MOUSE_MOVE : p1=x, p2=y in pixels (frame coords) * type=1 MOUSE_BUTTON : p1=btn (0=L,1=R,2=M), p2=down(1)/up(0) * type=2 MOUSE_WHEEL : p1=delta (signed int32, ~120/notch) * type=3 KEY : p1=Windows VK, p2=scancode, * p3 bit0=extended, bit1=keyup (clear=keydown) * * Build: gcc -O2 -Wall -o appsandbox-input appsandbox-input.c */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define VSOCK_PORT 3 #define INPUT_MAGIC 0x4E495341u /* 'ASIN' */ #define INPUT_READY_MAGIC 0x59445249u /* 'IRDY' */ #define INPUT_MOUSE_MOVE 0 #define INPUT_MOUSE_BUTTON 1 #define INPUT_MOUSE_WHEEL 2 #define INPUT_KEY 3 #define BTN_ID_LEFT 0 #define BTN_ID_RIGHT 1 #define BTN_ID_MIDDLE 2 /* Absolute pointer range. We don't know the guest screen size before * accept, and it can change mid-session. We pick a coordinate space * large enough for any plausible resolution and trust libinput to map * it to the active screen. 32767 is the de-facto absolute-tablet range. */ #define ABS_RANGE 32767 #pragma pack(push, 1) struct input_packet { uint32_t magic; uint32_t type; uint32_t p1, p2, p3; }; #pragma pack(pop) static volatile sig_atomic_t g_stop = 0; static int g_frame_w = 1920; /* updated by hint command if we add one later */ static int g_frame_h = 1080; static void on_signal(int sig) { (void)sig; g_stop = 1; } static void in_log(const char *fmt, ...) { va_list ap; va_start(ap, fmt); fprintf(stderr, "[input] "); vfprintf(stderr, fmt, ap); fprintf(stderr, "\n"); va_end(ap); } /* ---- VK → Linux keycode table ---- * Windows VK values are 8-bit; keys we don't map fall through to 0 * (which uinput silently drops). Reference KEY_* values come from * . */ #include static const uint16_t g_vk_to_key[256] = { [0x08] = KEY_BACKSPACE, [0x09] = KEY_TAB, [0x0D] = KEY_ENTER, [0x10] = KEY_LEFTSHIFT, [0x11] = KEY_LEFTCTRL, [0x12] = KEY_LEFTALT, [0x13] = KEY_PAUSE, [0x14] = KEY_CAPSLOCK, [0x1B] = KEY_ESC, [0x20] = KEY_SPACE, [0x21] = KEY_PAGEUP, [0x22] = KEY_PAGEDOWN, [0x23] = KEY_END, [0x24] = KEY_HOME, [0x25] = KEY_LEFT, [0x26] = KEY_UP, [0x27] = KEY_RIGHT, [0x28] = KEY_DOWN, [0x2C] = KEY_SYSRQ, /* PrintScreen */ [0x2D] = KEY_INSERT, [0x2E] = KEY_DELETE, [0x30] = KEY_0, [0x31] = KEY_1, [0x32] = KEY_2, [0x33] = KEY_3, [0x34] = KEY_4, [0x35] = KEY_5, [0x36] = KEY_6, [0x37] = KEY_7, [0x38] = KEY_8, [0x39] = KEY_9, [0x41] = KEY_A, [0x42] = KEY_B, [0x43] = KEY_C, [0x44] = KEY_D, [0x45] = KEY_E, [0x46] = KEY_F, [0x47] = KEY_G, [0x48] = KEY_H, [0x49] = KEY_I, [0x4A] = KEY_J, [0x4B] = KEY_K, [0x4C] = KEY_L, [0x4D] = KEY_M, [0x4E] = KEY_N, [0x4F] = KEY_O, [0x50] = KEY_P, [0x51] = KEY_Q, [0x52] = KEY_R, [0x53] = KEY_S, [0x54] = KEY_T, [0x55] = KEY_U, [0x56] = KEY_V, [0x57] = KEY_W, [0x58] = KEY_X, [0x59] = KEY_Y, [0x5A] = KEY_Z, [0x5B] = KEY_LEFTMETA, /* Left Win */ [0x5C] = KEY_RIGHTMETA, /* Right Win */ [0x5D] = KEY_COMPOSE, /* App / Menu */ [0x60] = KEY_KP0, [0x61] = KEY_KP1, [0x62] = KEY_KP2, [0x63] = KEY_KP3, [0x64] = KEY_KP4, [0x65] = KEY_KP5, [0x66] = KEY_KP6, [0x67] = KEY_KP7, [0x68] = KEY_KP8, [0x69] = KEY_KP9, [0x6A] = KEY_KPASTERISK, [0x6B] = KEY_KPPLUS, [0x6D] = KEY_KPMINUS, [0x6E] = KEY_KPDOT, [0x6F] = KEY_KPSLASH, [0x70] = KEY_F1, [0x71] = KEY_F2, [0x72] = KEY_F3, [0x73] = KEY_F4, [0x74] = KEY_F5, [0x75] = KEY_F6, [0x76] = KEY_F7, [0x77] = KEY_F8, [0x78] = KEY_F9, [0x79] = KEY_F10, [0x7A] = KEY_F11, [0x7B] = KEY_F12, [0x90] = KEY_NUMLOCK, [0x91] = KEY_SCROLLLOCK, [0xA0] = KEY_LEFTSHIFT, [0xA1] = KEY_RIGHTSHIFT, [0xA2] = KEY_LEFTCTRL, [0xA3] = KEY_RIGHTCTRL, [0xA4] = KEY_LEFTALT, [0xA5] = KEY_RIGHTALT, [0xBA] = KEY_SEMICOLON, [0xBB] = KEY_EQUAL, [0xBC] = KEY_COMMA, [0xBD] = KEY_MINUS, [0xBE] = KEY_DOT, [0xBF] = KEY_SLASH, [0xC0] = KEY_GRAVE, [0xDB] = KEY_LEFTBRACE, [0xDC] = KEY_BACKSLASH, [0xDD] = KEY_RIGHTBRACE, [0xDE] = KEY_APOSTROPHE, }; /* ---- uinput device setup ---- */ static int uinput_open(void) { int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK | O_CLOEXEC); if (fd < 0) { in_log("open /dev/uinput: %s", strerror(errno)); return -1; } /* Enable absolute pointer */ ioctl(fd, UI_SET_EVBIT, EV_ABS); ioctl(fd, UI_SET_ABSBIT, ABS_X); ioctl(fd, UI_SET_ABSBIT, ABS_Y); /* Pointer buttons + wheel */ ioctl(fd, UI_SET_EVBIT, EV_KEY); ioctl(fd, UI_SET_KEYBIT, BTN_LEFT); ioctl(fd, UI_SET_KEYBIT, BTN_RIGHT); ioctl(fd, UI_SET_KEYBIT, BTN_MIDDLE); ioctl(fd, UI_SET_KEYBIT, BTN_TOUCH); /* hint to libinput: absolute pointer */ ioctl(fd, UI_SET_EVBIT, EV_REL); ioctl(fd, UI_SET_RELBIT, REL_WHEEL); ioctl(fd, UI_SET_RELBIT, REL_HWHEEL); /* Keyboard — enable everything in the table that's nonzero */ for (int i = 0; i < 256; i++) { if (g_vk_to_key[i]) ioctl(fd, UI_SET_KEYBIT, g_vk_to_key[i]); } /* Modern uinput setup (UI_DEV_SETUP) gives us absinfo per-axis */ struct uinput_setup usetup = {0}; strncpy(usetup.name, "AppSandbox Virtual Input", UINPUT_MAX_NAME_SIZE - 1); usetup.id.bustype = BUS_VIRTUAL; usetup.id.vendor = 0xA53B; usetup.id.product = 0x0001; usetup.id.version = 1; if (ioctl(fd, UI_DEV_SETUP, &usetup) < 0) { in_log("UI_DEV_SETUP: %s", strerror(errno)); close(fd); return -1; } struct uinput_abs_setup abs = {0}; abs.absinfo.minimum = 0; abs.absinfo.maximum = ABS_RANGE; abs.code = ABS_X; ioctl(fd, UI_ABS_SETUP, &abs); abs.code = ABS_Y; ioctl(fd, UI_ABS_SETUP, &abs); if (ioctl(fd, UI_DEV_CREATE) < 0) { in_log("UI_DEV_CREATE: %s", strerror(errno)); close(fd); return -1; } /* Let udev notice the new device before we start posting events */ struct timespec ts = { .tv_sec = 0, .tv_nsec = 200 * 1000000L }; nanosleep(&ts, NULL); return fd; } static void uinput_close(int fd) { if (fd >= 0) { ioctl(fd, UI_DEV_DESTROY); close(fd); } } static void emit(int fd, uint16_t type, uint16_t code, int32_t value) { struct input_event ev = {0}; ev.type = type; ev.code = code; ev.value = value; /* Don't fail loudly on partial writes — keep streaming. */ (void)!write(fd, &ev, sizeof(ev)); } static void emit_syn(int fd) { emit(fd, EV_SYN, SYN_REPORT, 0); } /* ---- Event translation ---- */ static void do_mouse_move(int ui_fd, uint32_t x, uint32_t y) { /* Host gives us pixel coordinates in the current frame. Map to * 0..ABS_RANGE so the compositor scales correctly regardless of * the actual screen size. */ int32_t ax = (int32_t)((uint64_t)x * ABS_RANGE / (g_frame_w ? g_frame_w : 1)); int32_t ay = (int32_t)((uint64_t)y * ABS_RANGE / (g_frame_h ? g_frame_h : 1)); if (ax < 0) ax = 0; if (ax > ABS_RANGE) ax = ABS_RANGE; if (ay < 0) ay = 0; if (ay > ABS_RANGE) ay = ABS_RANGE; emit(ui_fd, EV_ABS, ABS_X, ax); emit(ui_fd, EV_ABS, ABS_Y, ay); emit_syn(ui_fd); } static void do_mouse_button(int ui_fd, uint32_t btn_id, uint32_t down) { uint16_t code = 0; switch (btn_id) { case BTN_ID_LEFT: code = BTN_LEFT; break; case BTN_ID_RIGHT: code = BTN_RIGHT; break; case BTN_ID_MIDDLE: code = BTN_MIDDLE; break; default: return; } emit(ui_fd, EV_KEY, code, down ? 1 : 0); emit_syn(ui_fd); } static void do_mouse_wheel(int ui_fd, int32_t delta) { /* Windows wheel delta is ~120 per notch; REL_WHEEL is one tick per * notch. Round toward zero. */ int32_t notches = delta / 120; if (notches == 0) notches = (delta > 0) - (delta < 0); emit(ui_fd, EV_REL, REL_WHEEL, notches); emit_syn(ui_fd); } static void do_key(int ui_fd, uint32_t vk, uint32_t scan, uint32_t flags) { (void)scan; uint16_t code = g_vk_to_key[vk & 0xFF]; if (!code) return; int is_up = (flags & 2) != 0; emit(ui_fd, EV_KEY, code, is_up ? 0 : 1); emit_syn(ui_fd); } /* ---- vsock listener ---- */ static int vsock_listen(unsigned port) { int s = socket(AF_VSOCK, SOCK_STREAM, 0); if (s < 0) { in_log("socket: %s", strerror(errno)); return -1; } struct sockaddr_vm sa = {0}; sa.svm_family = AF_VSOCK; sa.svm_cid = VMADDR_CID_ANY; sa.svm_port = port; if (bind(s, (struct sockaddr *)&sa, sizeof(sa)) < 0) { in_log("bind :%u: %s", port, strerror(errno)); close(s); return -1; } if (listen(s, 1) < 0) { in_log("listen: %s", strerror(errno)); close(s); return -1; } return s; } static int recv_exact(int fd, void *buf, size_t len) { uint8_t *p = (uint8_t *)buf; size_t left = len; while (left > 0) { ssize_t n = recv(fd, p, left, 0); if (n < 0) { if (errno == EINTR) continue; return -1; } if (n == 0) return -1; p += n; left -= (size_t)n; } return 0; } static void serve(int client_fd, int ui_fd) { /* Tell the host the guest is ready. */ uint32_t ready = INPUT_READY_MAGIC; if (send(client_fd, &ready, sizeof(ready), MSG_NOSIGNAL) != sizeof(ready)) { in_log("send IRDY: %s", strerror(errno)); return; } while (!g_stop) { struct input_packet pkt; if (recv_exact(client_fd, &pkt, sizeof(pkt)) < 0) break; if (pkt.magic != INPUT_MAGIC) { in_log("bad magic 0x%08x — desync, closing", pkt.magic); break; } switch (pkt.type) { case INPUT_MOUSE_MOVE: do_mouse_move(ui_fd, pkt.p1, pkt.p2); break; case INPUT_MOUSE_BUTTON: do_mouse_button(ui_fd, pkt.p1, pkt.p2); break; case INPUT_MOUSE_WHEEL: do_mouse_wheel(ui_fd, (int32_t)pkt.p1); break; case INPUT_KEY: do_key(ui_fd, pkt.p1, pkt.p2, pkt.p3); break; default: break; } } } int main(void) { signal(SIGPIPE, SIG_IGN); signal(SIGINT, on_signal); signal(SIGTERM, on_signal); int ui = uinput_open(); if (ui < 0) return 1; in_log("uinput device created"); int srv = vsock_listen(VSOCK_PORT); if (srv < 0) { uinput_close(ui); return 1; } in_log("listening on vsock :%u", VSOCK_PORT); while (!g_stop) { struct sockaddr_vm peer; socklen_t plen = sizeof(peer); int c = accept(srv, (struct sockaddr *)&peer, &plen); if (c < 0) { if (errno == EINTR) continue; in_log("accept: %s", strerror(errno)); break; } in_log("client connected (cid=%u)", peer.svm_cid); serve(c, ui); close(c); in_log("client disconnected"); } close(srv); uinput_close(ui); return 0; }