diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5a7149c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[workspace] +members = [ + "uinput-ioctls", + "vuinputd", + "vuinput-examples", +] + +# Optional: force same version for all crates +resolver = "2" diff --git a/uinput-ioctls/Cargo.toml b/uinput-ioctls/Cargo.toml new file mode 100644 index 0000000..29630a4 --- /dev/null +++ b/uinput-ioctls/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "uinput-ioctls" +version = "0.1.0" +edition = "2021" + + +[dependencies] +nix = { version = "0.30", features = ["ioctl"] } # ioctl & libc bindings +libc = "0.2" # raw system calls \ No newline at end of file diff --git a/uinput-ioctls/src/lib.rs b/uinput-ioctls/src/lib.rs new file mode 100644 index 0000000..4ef6512 --- /dev/null +++ b/uinput-ioctls/src/lib.rs @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +use libc::{c_char, c_uint}; +use libc::{uinput_abs_setup, uinput_ff_erase, uinput_ff_upload, uinput_setup}; + +use nix::{ + ioctl_none, ioctl_read, ioctl_read_buf, ioctl_readwrite, ioctl_write_int, ioctl_write_ptr, + request_code_none, request_code_read, request_code_readwrite, request_code_write, +}; + +pub const UI_DEV_CREATE: u64 = request_code_none!(b'U', 1); +pub const UI_DEV_DESTROY: u64 = request_code_none!(b'U', 2); +pub const UI_DEV_SETUP: u64 = request_code_write!(b'U', 3, ::std::mem::size_of::()); +pub const UI_ABS_SETUP: u64 = + request_code_write!(b'U', 4, ::std::mem::size_of::()); +//pub const UI_ABS_SETUP_WITHOUT_SIZE: u64 = request_code_write!(b'U', 4, 0); + +pub const UI_GET_SYSNAME_WITHOUT_SIZE: u64 = request_code_read!(b'U', 44, 0); +//#define UI_GET_SYSNAME(len) _IOC(_IOC_READ, UINPUT_IOCTL_BASE, 44, len) +pub const UI_GET_VERSION: u64 = request_code_read!(b'U', 45, ::std::mem::size_of::()); + +pub const UI_SET_EVBIT: u64 = request_code_write!(b'U', 100, std::mem::size_of::()); +pub const UI_SET_KEYBIT: u64 = request_code_write!(b'U', 101, std::mem::size_of::()); +pub const UI_SET_RELBIT: u64 = request_code_write!(b'U', 102, std::mem::size_of::()); +pub const UI_SET_ABSBIT: u64 = request_code_write!(b'U', 103, std::mem::size_of::()); +pub const UI_SET_MSCBIT: u64 = request_code_write!(b'U', 104, std::mem::size_of::()); +pub const UI_SET_LEDBIT: u64 = request_code_write!(b'U', 105, std::mem::size_of::()); +pub const UI_SET_SNDBIT: u64 = request_code_write!(b'U', 106, std::mem::size_of::()); +pub const UI_SET_FFBIT: u64 = request_code_write!(b'U', 107, std::mem::size_of::()); +pub const UI_SET_PHYS: u64 = request_code_write!(b'U', 108, ::std::mem::size_of::<*mut c_char>()); +pub const UI_SET_SWBIT: u64 = request_code_write!(b'U', 109, std::mem::size_of::()); +pub const UI_SET_PROPBIT: u64 = request_code_write!(b'U', 110, std::mem::size_of::()); + +pub const UI_BEGIN_FF_UPLOAD: u64 = + request_code_readwrite!(b'U', 200, ::std::mem::size_of::()); +pub const UI_END_FF_UPLOAD: u64 = + request_code_write!(b'U', 201, ::std::mem::size_of::()); +pub const UI_BEGIN_FF_ERASE: u64 = + request_code_readwrite!(b'U', 202, ::std::mem::size_of::()); +pub const UI_END_FF_ERASE: u64 = + request_code_write!(b'U', 203, ::std::mem::size_of::()); + +ioctl_none!(ui_dev_create, b'U', 1); +ioctl_none!(ui_dev_destroy, b'U', 2); +ioctl_write_ptr! {ui_dev_setup, b'U', 3, uinput_setup} +ioctl_write_ptr! { ui_abs_setup, b'U', 4, uinput_abs_setup} + +ioctl_read_buf! { ui_get_sysname, b'U', 44, c_char } +ioctl_read! { ui_get_version, b'U', 45, c_uint } + +ioctl_write_int!(ui_set_evbit, b'U', 100); +ioctl_write_int!(ui_set_keybit, b'U', 101); +ioctl_write_int!(ui_set_relbit, b'U', 102); +ioctl_write_int!(ui_set_absbit, b'U', 103); +ioctl_write_int!(ui_set_mscbit, b'U', 104); +ioctl_write_int!(ui_set_ledbit, b'U', 105); +ioctl_write_int!(ui_set_sndbit, b'U', 106); +ioctl_write_int!(ui_set_ffbit, b'U', 107); +ioctl_write_ptr!(ui_set_phys, b'U', 108, *const c_char); // original macro #define UI_SET_PHYS _IOW(UINPUT_IOCTL_BASE, 108, char*) +ioctl_write_int!(ui_set_swbit, b'U', 109); +ioctl_write_int!(ui_set_propbit, b'U', 110); + +ioctl_readwrite!(ui_begin_ff_upload, b'U', 200, uinput_ff_upload); +ioctl_write_ptr!(ui_end_ff_upload, b'U', 201, uinput_ff_upload); +ioctl_readwrite!(ui_begin_ff_erase, b'U', 202, uinput_ff_erase); +ioctl_write_ptr!(ui_end_ff_erase, b'U', 203, uinput_ff_erase); diff --git a/vuinput-examples/Cargo.toml b/vuinput-examples/Cargo.toml new file mode 100644 index 0000000..074cc49 --- /dev/null +++ b/vuinput-examples/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "vuinput-examples" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "keyboard-advanced" + +[[bin]] +name = "mouse-advanced" + +[dependencies] +uinput-ioctls = { path = "../uinput-ioctls" } +#fuse = "0.3" # FUSE/ CUSE interface +libfuse-sys = { git = "https://github.com/richard-w/libfuse-sys.git", rev = "a9bc85e3c24d44d8577e79759c4ccf0a18050037", features = ["fuse_35","cuse_lowlevel"] } +#fuse-backend-rs = "0.13.0" +nix = { version = "0.30", features = ["ioctl"] } # ioctl & libc bindings +libc = "0.2" # raw system calls +libudev = "0.3" # enumerate-udev diff --git a/vuinput-examples/src/bin/keyboard-advanced.rs b/vuinput-examples/src/bin/keyboard-advanced.rs new file mode 100644 index 0000000..f74bc79 --- /dev/null +++ b/vuinput-examples/src/bin/keyboard-advanced.rs @@ -0,0 +1,399 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +use libc::uinput_setup; +use libc::{c_int, close, open, write, O_NONBLOCK, O_WRONLY}; +use std::ffi::{CStr, CString}; +use std::io; +use std::mem::{size_of, zeroed}; +use std::os::raw::{c_char, c_void}; +use std::ptr; +use std::thread::sleep; +use std::time::Duration; +pub use uinput_ioctls::*; + +// Constants (same numeric values as in linux headers) +const EV_SYN: i32 = 0x00; +const EV_KEY: i32 = 0x01; +const SYN_REPORT: i32 = 0; +const BUS_USB: u16 = 0x03; + +/// Key codes. Those are used by udev to recognize a device as a keyboard. +const KEY_ESC: i32 = 1; +const KEY_1: i32 = 2; +const KEY_2: i32 = 3; +const KEY_3: i32 = 4; +const KEY_4: i32 = 5; +const KEY_5: i32 = 6; +const KEY_6: i32 = 7; +const KEY_7: i32 = 8; +const KEY_8: i32 = 9; +const KEY_9: i32 = 10; +const KEY_0: i32 = 11; +const KEY_MINUS: i32 = 12; +const KEY_EQUAL: i32 = 13; +const KEY_BACKSPACE: i32 = 14; +const KEY_TAB: i32 = 15; +const KEY_Q: i32 = 16; +const KEY_W: i32 = 17; +const KEY_E: i32 = 18; +const KEY_R: i32 = 19; +const KEY_T: i32 = 20; +const KEY_Y: i32 = 21; +const KEY_U: i32 = 22; +const KEY_I: i32 = 23; +const KEY_O: i32 = 24; +const KEY_P: i32 = 25; +const KEY_LEFTBRACE: i32 = 26; +const KEY_RIGHTBRACE: i32 = 27; +const KEY_ENTER: i32 = 28; +const KEY_LEFTCTRL: i32 = 29; +const KEY_A: i32 = 30; +const KEY_S: i32 = 31; + +/// Space and other common keys +const KEY_D: i32 = 32; +const KEY_F: i32 = 33; +const KEY_G: i32 = 34; +const KEY_H: i32 = 35; +const KEY_J: i32 = 36; +const KEY_K: i32 = 37; +const KEY_L: i32 = 38; +const KEY_SEMICOLON: i32 = 39; +const KEY_APOSTROPHE: i32 = 40; +const KEY_GRAVE: i32 = 41; +const KEY_LEFTSHIFT: i32 = 42; +const KEY_BACKSLASH: i32 = 43; +const KEY_Z: i32 = 44; +const KEY_X: i32 = 45; +const KEY_C: i32 = 46; +const KEY_V: i32 = 47; +const KEY_B: i32 = 48; +const KEY_N: i32 = 49; +const KEY_M: i32 = 50; +const KEY_COMMA: i32 = 51; +const KEY_DOT: i32 = 52; +const KEY_SLASH: i32 = 53; +const KEY_RIGHTSHIFT: i32 = 54; +const KEY_KPASTERISK: i32 = 55; +const KEY_LEFTALT: i32 = 56; +const KEY_SPACE: i32 = 57; +const KEY_CAPSLOCK: i32 = 58; + +/// Function keys +const KEY_F1: i32 = 59; +const KEY_F2: i32 = 60; +const KEY_F3: i32 = 61; +const KEY_F4: i32 = 62; +const KEY_F5: i32 = 63; +const KEY_F6: i32 = 64; +const KEY_F7: i32 = 65; +const KEY_F8: i32 = 66; +const KEY_F9: i32 = 67; +const KEY_F10: i32 = 68; +const KEY_NUMLOCK: i32 = 69; +const KEY_SCROLLLOCK: i32 = 70; +const KEY_KP7: i32 = 71; +const KEY_KP8: i32 = 72; +const KEY_KP9: i32 = 73; +const KEY_KPMINUS: i32 = 74; +const KEY_KP4: i32 = 75; +const KEY_KP5: i32 = 76; +const KEY_KP6: i32 = 77; +const KEY_KPPLUS: i32 = 78; +const KEY_KP1: i32 = 79; +const KEY_KP2: i32 = 80; +const KEY_KP3: i32 = 81; +const KEY_KP0: i32 = 82; +const KEY_KPDOT: i32 = 83; + +/// Arrow keys and navigation +const KEY_ZENKAKUHANKAKU: i32 = 85; +const KEY_102ND: i32 = 86; +const KEY_F11: i32 = 87; +const KEY_F12: i32 = 88; +const KEY_RO: i32 = 89; +const KEY_KATAKANA: i32 = 90; +const KEY_HIRAGANA: i32 = 91; +const KEY_HENKAN: i32 = 92; +const KEY_KATAKANAHIRAGANA: i32 = 93; +const KEY_MUHENKAN: i32 = 94; +const KEY_KPJPCOMMA: i32 = 95; +const KEY_KPENTER: i32 = 96; +const KEY_RIGHTCTRL: i32 = 97; +const KEY_KPSLASH: i32 = 98; +const KEY_SYSRQ: i32 = 99; +const KEY_RIGHTALT: i32 = 100; +const KEY_LINEFEED: i32 = 101; +const KEY_HOME: i32 = 102; +const KEY_UP: i32 = 103; +const KEY_PAGEUP: i32 = 104; +const KEY_LEFT: i32 = 105; +const KEY_RIGHT: i32 = 106; +const KEY_END: i32 = 107; +const KEY_DOWN: i32 = 108; +const KEY_PAGEDOWN: i32 = 109; +const KEY_INSERT: i32 = 110; +const KEY_DELETE: i32 = 111; + +/// Configure a full 101-key standard keyboard +unsafe fn set_standard_keyboard_keys(fd: i32) -> Result<(), std::io::Error> { + // We need to set more bits so that systemd recognizes a keyboard as a keyboard. + // At least the first 32 bits are ESC, numbers, and Q to D, except KEY_RESERVED need to be considered. + // udev-builtin-input_id.c consideres the mask = 0xFFFFFFFE + + // EV_KEY + ui_set_evbit(fd, EV_KEY.try_into().unwrap())?; + + // All standard keys (1..101+) + let all_keys = [ + // Modifier + main keys + KEY_ESC, + KEY_1, + KEY_2, + KEY_3, + KEY_4, + KEY_5, + KEY_6, + KEY_7, + KEY_8, + KEY_9, + KEY_0, + KEY_MINUS, + KEY_EQUAL, + KEY_BACKSPACE, + KEY_TAB, + KEY_Q, + KEY_W, + KEY_E, + KEY_R, + KEY_T, + KEY_Y, + KEY_U, + KEY_I, + KEY_O, + KEY_P, + KEY_LEFTBRACE, + KEY_RIGHTBRACE, + KEY_ENTER, + KEY_LEFTCTRL, + KEY_A, + KEY_S, + KEY_D, + KEY_F, + KEY_G, + KEY_H, + KEY_J, + KEY_K, + KEY_L, + KEY_SEMICOLON, + KEY_APOSTROPHE, + KEY_GRAVE, + KEY_LEFTSHIFT, + KEY_BACKSLASH, + KEY_Z, + KEY_X, + KEY_C, + KEY_V, + KEY_B, + KEY_N, + KEY_M, + KEY_COMMA, + KEY_DOT, + KEY_SLASH, + KEY_RIGHTSHIFT, + KEY_KPASTERISK, + KEY_LEFTALT, + KEY_SPACE, + KEY_CAPSLOCK, + // Function keys + KEY_F1, + KEY_F2, + KEY_F3, + KEY_F4, + KEY_F5, + KEY_F6, + KEY_F7, + KEY_F8, + KEY_F9, + KEY_F10, + KEY_F11, + KEY_F12, + KEY_NUMLOCK, + KEY_SCROLLLOCK, + // Keypad + KEY_KP7, + KEY_KP8, + KEY_KP9, + KEY_KPMINUS, + KEY_KP4, + KEY_KP5, + KEY_KP6, + KEY_KPPLUS, + KEY_KP1, + KEY_KP2, + KEY_KP3, + KEY_KP0, + KEY_KPDOT, + KEY_KPENTER, + KEY_KPSLASH, + KEY_KPJPCOMMA, + // Arrows / navigation + KEY_HOME, + KEY_UP, + KEY_PAGEUP, + KEY_LEFT, + KEY_RIGHT, + KEY_END, + KEY_DOWN, + KEY_PAGEDOWN, + KEY_INSERT, + KEY_DELETE, + KEY_RIGHTCTRL, + KEY_RIGHTALT, + // Optional Japanese / additional keys + KEY_ZENKAKUHANKAKU, + KEY_102ND, + KEY_RO, + KEY_KATAKANA, + KEY_HIRAGANA, + KEY_HENKAN, + KEY_KATAKANAHIRAGANA, + KEY_MUHENKAN, + KEY_LINEFEED, + KEY_SYSRQ, + ]; + + for &key in all_keys.iter() { + ui_set_keybit(fd, key.try_into().unwrap())?; + } + + Ok(()) +} + +fn emit(fd: c_int, ev_type: i32, code: i32, val: i32) -> io::Result<()> { + // libc's input_event struct layout: + // struct input_event { + // struct timeval time; + // __u16 type; + // __u16 code; + // __s32 value; + // }; + // + // libc provides input_event as `libc::input_event` on Linux. + let mut ie: libc::input_event = unsafe { zeroed() }; + + // time fields are ignored by kernel for synthetic events - set zero + ie.time.tv_sec = 0; + ie.time.tv_usec = 0; + + // input_event fields: type and code are u16 in C; value is i32 + ie.type_ = ev_type as u16; // note: in libc the field is `type_` + ie.code = code as u16; + ie.value = val as i32; + + // write the struct to the uinput fd + let buf_ptr = &ie as *const libc::input_event as *const c_void; + let bytes = size_of::(); + + let written = unsafe { write(fd, buf_ptr, bytes) }; + if written as usize != bytes { + return Err(io::Error::last_os_error()); + } + Ok(()) +} + +fn main() -> io::Result<()> { + // open device - matches: open("/dev/uinput-test", O_WRONLY | O_NONBLOCK); + let path = CString::new("/dev/uinput-test").unwrap(); + let fd = unsafe { open(path.as_ptr(), O_WRONLY | O_NONBLOCK) }; + if fd < 0 { + eprintln!("error opening uinput"); + return Err(io::Error::last_os_error()); + } + + // In your snippet you supplied value.into() to the wrappers. The wrappers may accept different types. + // We follow your earlier usage pattern: + unsafe { + let mut version_of_uinput = 0; + let pversion_of_uinput = std::ptr::from_mut(&mut version_of_uinput); + eprintln!("ioctl UI_GET_VERSION request"); + ui_get_version(fd, pversion_of_uinput).unwrap_or_else(|e| { + eprintln!("ui_get_version failed: {:?}", e); + std::process::exit(1); + }); + eprintln!("ioctl UI_GET_VERSION {}", version_of_uinput); + + let _ = set_standard_keyboard_keys(fd).unwrap_or_else(|e| { + eprintln!("set_standard_keyboard_keys failed: {:?}", e); + std::process::exit(1); + }); + } + + // Prepare uinput_setup struct + let mut usetup: uinput_setup = unsafe { zeroed() }; + + // Fill id and name fields + // `id` has bustype, vendor, product fields (types may vary slightly by libc version) + // set bustype/vendor/product as in C example + // Note: make sure the fields exist as below in your libc version; adapt if names differ. + usetup.id.bustype = BUS_USB; + usetup.id.vendor = 0xbeef; + usetup.id.product = 0xdead; + + // Copy device name into the C char array in the struct + let name = CString::new("Example device").unwrap(); + // uinput_setup::name is usually [c_char; UINPUT_MAX_NAME_SIZE] + unsafe { + // Fill with zeros first (already zeroed by zeroed()) then copy bytes + let name_ptr = usetup.name.as_mut_ptr() as *mut c_char; + ptr::copy_nonoverlapping(name.as_ptr(), name_ptr, name.to_bytes_with_nul().len()); + } + + // Call IOCTLs to setup and create the device + // Assuming your wrappers accept (fd, ptr_to_usetup) etc. + // We'll pass pointer to usetup + let usetup_ptr = &mut usetup as *mut uinput_setup; + unsafe { + ui_dev_setup(fd, usetup_ptr).unwrap_or_else(|e| { + eprintln!("ui_dev_setup failed: {:?}", e); + close(fd); + std::process::exit(1); + }); + + ui_dev_create(fd).unwrap_or_else(|e| { + eprintln!("ui_dev_create failed: {:?}", e); + close(fd); + std::process::exit(1); + }); + + let mut resultbuf: [i8; 64] = [0i8; 64]; + ui_get_sysname(fd, &mut resultbuf).unwrap(); + let sysname = CStr::from_ptr(resultbuf.as_ptr()).to_string_lossy(); + eprintln!("sysname: {}", sysname); + + // Sleep 1 second to allow userspace to detect the device (same as C example) + sleep(Duration::from_secs(10)); + + // Emit press + syn + release + syn + emit(fd, EV_KEY, KEY_SPACE, 1)?; + emit(fd, EV_SYN, SYN_REPORT, 0)?; + emit(fd, EV_KEY, KEY_SPACE, 0)?; + emit(fd, EV_SYN, SYN_REPORT, 0)?; + + // Give userspace time to read events + sleep(Duration::from_secs(10)); + + // Destroy device and close fd + ui_dev_destroy(fd).unwrap_or_else(|e| { + eprintln!("ui_dev_destroy failed: {:?}", e); + close(fd); + std::process::exit(1); + }); + + close(fd); + } + + Ok(()) +} diff --git a/vuinput-examples/src/bin/mouse-advanced.rs b/vuinput-examples/src/bin/mouse-advanced.rs new file mode 100644 index 0000000..963fa17 --- /dev/null +++ b/vuinput-examples/src/bin/mouse-advanced.rs @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +use libc::uinput_setup; +use libc::{c_int, close, open, write, O_NONBLOCK, O_WRONLY}; +use std::ffi::{CStr, CString}; +use std::io; +use std::mem::{size_of, zeroed}; +use std::os::raw::{c_char, c_void}; +use std::ptr; +use std::thread::sleep; +use std::time::Duration; +pub use uinput_ioctls::*; + +// Constants (same numeric values as in linux headers) +const EV_SYN: i32 = 0x00; +const EV_KEY: i32 = 0x01; +const EV_REL: i32 = 2; +const BTN_LEFT: i32 = 272; +const REL_X: i32 = 0; +const REL_Y: i32 = 1; +const SYN_REPORT: i32 = 0; +const BUS_USB: u16 = 0x03; + +/// + + +fn emit(fd: c_int, ev_type: i32, code: i32, val: i32) -> io::Result<()> { + // libc's input_event struct layout: + // struct input_event { + // struct timeval time; + // __u16 type; + // __u16 code; + // __s32 value; + // }; + // + // libc provides input_event as `libc::input_event` on Linux. + let mut ie: libc::input_event = unsafe { zeroed() }; + + // time fields are ignored by kernel for synthetic events - set zero + ie.time.tv_sec = 0; + ie.time.tv_usec = 0; + + // input_event fields: type and code are u16 in C; value is i32 + ie.type_ = ev_type as u16; // note: in libc the field is `type_` + ie.code = code as u16; + ie.value = val as i32; + + // write the struct to the uinput fd + let buf_ptr = &ie as *const libc::input_event as *const c_void; + let bytes = size_of::(); + + let written = unsafe { write(fd, buf_ptr, bytes) }; + if written as usize != bytes { + return Err(io::Error::last_os_error()); + } + Ok(()) +} + +fn main() -> io::Result<()> { + // open device - matches: open("/dev/uinput-test", O_WRONLY | O_NONBLOCK); + + let args: Vec = std::env::args().collect(); + let device= + match args.len() { + 2 => args[1].clone(), + _ => "/dev/uinput".to_string() + }; + + let path = CString::new(device).unwrap(); + let fd = unsafe { open(path.as_ptr(), O_WRONLY | O_NONBLOCK) }; + if fd < 0 { + eprintln!("error opening uinput"); + return Err(io::Error::last_os_error()); + } + + // In your snippet you supplied value.into() to the wrappers. The wrappers may accept different types. + // We follow your earlier usage pattern: + unsafe { + let mut version_of_uinput = 0; + let pversion_of_uinput = std::ptr::from_mut(&mut version_of_uinput); + eprintln!("ioctl UI_GET_VERSION request"); + ui_get_version(fd, pversion_of_uinput).unwrap_or_else(|e| { + eprintln!("ui_get_version failed: {:?}", e); + std::process::exit(1); + }); + eprintln!("ioctl UI_GET_VERSION {}", version_of_uinput); + + ui_set_evbit(fd, EV_KEY.try_into().unwrap()).unwrap_or_else(|e| { + eprintln!("ui_set_evbit(EV_KEY) failed: {:?}", e); + std::process::exit(1); + }); + + ui_set_keybit(fd, BTN_LEFT.try_into().unwrap()).unwrap_or_else(|e| { + eprintln!("ui_set_keybit(BTN_LEFT) failed: {:?}", e); + std::process::exit(1); + }); + + + ui_set_evbit(fd, EV_REL.try_into().unwrap()).unwrap_or_else(|e| { + eprintln!("ui_set_evbit(EV_REL) failed: {:?}", e); + std::process::exit(1); + }); + + + ui_set_relbit(fd, REL_X.try_into().unwrap()).unwrap_or_else(|e| { + eprintln!("ui_set_relbit(REL_X) failed: {:?}", e); + std::process::exit(1); + }); + + + ui_set_relbit(fd, REL_Y.try_into().unwrap()).unwrap_or_else(|e| { + eprintln!("ui_set_relbit(REL_Y) failed: {:?}", e); + std::process::exit(1); + }); + } + + // Prepare uinput_setup struct + let mut usetup: uinput_setup = unsafe { zeroed() }; + + // Fill id and name fields + // `id` has bustype, vendor, product fields (types may vary slightly by libc version) + // set bustype/vendor/product as in C example + // Note: make sure the fields exist as below in your libc version; adapt if names differ. + usetup.id.bustype = BUS_USB; + usetup.id.vendor = 0xbeef; + usetup.id.product = 0xdead; + + // Copy device name into the C char array in the struct + let name = CString::new("Example device").unwrap(); + // uinput_setup::name is usually [c_char; UINPUT_MAX_NAME_SIZE] + unsafe { + // Fill with zeros first (already zeroed by zeroed()) then copy bytes + let name_ptr = usetup.name.as_mut_ptr() as *mut c_char; + ptr::copy_nonoverlapping(name.as_ptr(), name_ptr, name.to_bytes_with_nul().len()); + } + + // Call IOCTLs to setup and create the device + // Assuming your wrappers accept (fd, ptr_to_usetup) etc. + // We'll pass pointer to usetup + let usetup_ptr = &mut usetup as *mut uinput_setup; + unsafe { + ui_dev_setup(fd, usetup_ptr).unwrap_or_else(|e| { + eprintln!("ui_dev_setup failed: {:?}", e); + close(fd); + std::process::exit(1); + }); + + ui_dev_create(fd).unwrap_or_else(|e| { + eprintln!("ui_dev_create failed: {:?}", e); + close(fd); + std::process::exit(1); + }); + + let mut resultbuf: [i8; 64] = [0i8; 64]; + ui_get_sysname(fd, &mut resultbuf).unwrap(); + let sysname = CStr::from_ptr(resultbuf.as_ptr()).to_string_lossy(); + eprintln!("sysname: {}", sysname); + + // Sleep 1 second to allow userspace to detect the device (same as C example) + sleep(Duration::from_secs(10)); + + for n in 1..3000 { + if n % 10 <= 5 { + emit(fd, EV_REL, REL_X, 5)?; + emit(fd, EV_REL, REL_Y, 5)?; + } else { + emit(fd, EV_REL, REL_X, -5)?; + emit(fd, EV_REL, REL_Y, -5)?; + } + emit(fd, EV_SYN, SYN_REPORT, 0)?; + sleep(Duration::from_millis(300)); + } + + // Give userspace time to read events + sleep(Duration::from_secs(10)); + + // Destroy device and close fd + ui_dev_destroy(fd).unwrap_or_else(|e| { + eprintln!("ui_dev_destroy failed: {:?}", e); + close(fd); + std::process::exit(1); + }); + + close(fd); + } + + Ok(()) +} diff --git a/vuinputd/Cargo.toml b/vuinputd/Cargo.toml new file mode 100644 index 0000000..ae4690d --- /dev/null +++ b/vuinputd/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "vuinputd" +version = "0.1.0" +edition = "2021" +authors = ["Johannes Leupolz "] +license = "MIT" +description = "Container-safe mediation daemon for /dev/uinput using CUSE." +repository = "https://github.com/joleuger/vuinputd" + +[dependencies] +uinput-ioctls = { path = "../uinput-ioctls" } +#fuse = "0.3" # FUSE/ CUSE interface +libfuse-sys = { git = "https://github.com/richard-w/libfuse-sys.git", rev = "a9bc85e3c24d44d8577e79759c4ccf0a18050037", features = ["fuse_35","cuse_lowlevel"] } +#fuse-backend-rs = "0.13.0" +nix = { version = "0.30", features = ["ioctl","process","sched","fs","event","user","socket","uio"] } +libc = "0.2" # raw system calls +time = "0.3" # for Timespec in FUSE replies +#input-linux-sys = "0.9.0" +#linux-raw-sys = {version="0.11.0", features = ["ioctl"]} +log = { version = "0.4", features = ["max_level_debug", "release_max_level_warn"] } +env_logger = "0.11.8" +libudev = "0.3" # enumerate-udev +regex = "1.12.2" +async-channel = "2.5.0" +futures = "0.3.31" +async-io = "2.6.0" +async-pidfd = "0.1.5" +anyhow = "1.0.100" diff --git a/vuinputd/src/container/inject_in_container_job.rs b/vuinputd/src/container/inject_in_container_job.rs new file mode 100644 index 0000000..5b5fefe --- /dev/null +++ b/vuinputd/src/container/inject_in_container_job.rs @@ -0,0 +1,110 @@ +use std::{collections::HashMap, future::Future, pin::Pin, time::Duration}; + +use async_io::Timer; +use async_pidfd::AsyncPidFd; +use log::debug; + +use crate::{ + container::{mknod_input_device::ensure_input_device, netlink_message::send_udev_monitor_message_with_properties, runtime_data::{self, ensure_udev_structure, read_udev_data, write_udev_data}}, jobs::job::{Job, JobTarget}, monitor_udev::EVENT_STORE, namespace::{run_in_net_and_mnt_namespace, Namespaces} +}; + +#[derive(Clone,Debug)] +pub struct InjectInContainerJob { + namespaces: Namespaces, + target: JobTarget, + dev_path: String, + sys_path: String, + major: u64, + minor: u64, +} + +impl InjectInContainerJob { + pub fn new(namespaces: Namespaces,dev_path: String, sys_path: String, major: u64, minor: u64) -> Self { + Self { + namespaces: namespaces.clone(), + target: JobTarget::Container(namespaces), + dev_path: dev_path, + sys_path: sys_path, + major: major , + minor: minor, + } + } +} + +impl Job for InjectInContainerJob { + fn desc(&self) -> &str { + "Inject input device into container" + } + + fn execute_after_cancellation(&self) -> bool { + false + } + + fn create_task(self: &InjectInContainerJob) -> Pin>> { + Box::pin(self.clone().inject_in_container()) + } + + fn job_target(&self) -> JobTarget { + self.target.clone() + } +} + +impl InjectInContainerJob { + async fn inject_in_container(self) { + // temporary hack that needs to be replaced. We try 50 times + // Should be: Wait for the device to be created, the runtime data to be written and the + // netlink message to be sent + let mut netlink_data: Option> = None; + let mut runtime_data: Option = None; + let mut number_of_attempt = 1; + while number_of_attempt<=50 && !(netlink_data.is_some() && runtime_data.is_some()) { + + if netlink_data.is_none() { + + if let Some(netlink_event)=EVENT_STORE.get().unwrap().lock().unwrap().take(&self.sys_path) { + if netlink_event.tombstone || netlink_event.remove_data.is_some() { + debug!("do nothing, because the device has already been removed in the meantime"); + return; + } + netlink_data=netlink_event.add_data; + }; + } + if runtime_data.is_none() { + runtime_data = read_udev_data(self.major,self.minor).ok(); + + } + + number_of_attempt+=1; + // wait a maximum of 5 seconds == 50 attempts + Timer::after(Duration::from_millis(100)).await; + } + if (netlink_data.is_none() || runtime_data.is_none()) { + if netlink_data.is_none() { + debug!("Give up reading netlink data"); + } + if runtime_data.is_none() { + debug!("Give up reading runtime data"); + } + return; + } + + // define for capturing + let major = self.major; + let minor=self.minor; + let runtime_data = runtime_data.unwrap(); + let netlink_data = netlink_data.unwrap(); + + let child_pid = run_in_net_and_mnt_namespace(self.namespaces, Box::new(move || { + + ensure_input_device(self.dev_path.clone(), self.major, self.minor).unwrap(); + ensure_udev_structure().unwrap(); + write_udev_data(runtime_data.as_str(), major, minor).unwrap(); + send_udev_monitor_message_with_properties(netlink_data.clone()); + + })) + .expect("subprocess should work"); + let pid_fd = AsyncPidFd::from_pid(child_pid.as_raw()).unwrap(); + let _exit_info = pid_fd.wait().await.unwrap(); + + } +} diff --git a/vuinputd/src/container/mknod_input_device.rs b/vuinputd/src/container/mknod_input_device.rs new file mode 100644 index 0000000..461e397 --- /dev/null +++ b/vuinputd/src/container/mknod_input_device.rs @@ -0,0 +1,73 @@ +use nix::sys::stat::{makedev, mknod, stat, Mode, SFlag}; +use nix::unistd::{chown, Gid, Uid}; +use std::error::Error; +use std::fs; +use std::os::unix::fs::{MetadataExt, PermissionsExt}; +use std::path::Path; + +pub fn ensure_input_device(dev_path: String, major: u64, minor: u64) -> Result<(), Box> { + let path = Path::new(&dev_path); + let expected_dev = makedev(major, minor); + let expected_mode = 0o666; + + // --- Step 1: Ensure node correctness --- + let needs_replacement = if path.exists() { + match stat(path) { + Ok(st) => { + let is_char = (st.st_mode & libc::S_IFMT as u32) == libc::S_IFCHR as u32; + let dev_ok = st.st_rdev == expected_dev; + !(is_char && dev_ok) + } + Err(_) => true, + } + } else { + true + }; + + if needs_replacement { + println!("Replacing {}", dev_path); + let _ = fs::remove_file(path); + let mode = Mode::from_bits_truncate(expected_mode); + mknod(path, SFlag::S_IFCHR, mode, expected_dev)?; + } else { + println!("{} is already correct device", dev_path); + } + + // --- Step 2: Ensure ownership and permissions --- + if let Ok(meta) = fs::metadata(path) { + let perms = meta.permissions().mode() & 0o777; + + if perms != expected_mode { + println!("Fixing mode of {} (was {:o})", dev_path, perms); + fs::set_permissions(path, fs::Permissions::from_mode(expected_mode))?; + } + + /* TODO: Think about it + let expected_uid = 0; + let expected_gid = 0; + let uid = meta.uid(); + let gid = meta.gid(); + if uid != expected_uid || gid != expected_gid { + println!( + "Fixing ownership of {} (was uid={}, gid={})", + dev_path, uid, gid + ); + chown(path, Some(Uid::from_raw(expected_uid)), Some(Gid::from_raw(expected_gid)))?; + } + */ + } + + Ok(()) +} + +/* +fn main() { + let name = "/dev/input/example0"; + let major = 13; // example values + let minor = 37; + + if let Err(e) = ensure_input_device(name, major, minor) { + eprintln!("Error: {}", e); + } +} + */ diff --git a/vuinputd/src/container/mod.rs b/vuinputd/src/container/mod.rs new file mode 100644 index 0000000..edfdd45 --- /dev/null +++ b/vuinputd/src/container/mod.rs @@ -0,0 +1,5 @@ +pub mod inject_in_container_job; +pub mod remove_from_container_job; +pub mod mknod_input_device; +pub mod runtime_data; +pub mod netlink_message; \ No newline at end of file diff --git a/vuinputd/src/container/netlink_message.rs b/vuinputd/src/container/netlink_message.rs new file mode 100644 index 0000000..412f957 --- /dev/null +++ b/vuinputd/src/container/netlink_message.rs @@ -0,0 +1,210 @@ +use std::collections::HashMap; +use std::mem; +use std::os::fd::{AsRawFd, OwnedFd}; + +use std::io::{Cursor, IoSlice}; + +use log::debug; +use nix::sys::socket::{ + bind, sendmsg, socket, AddressFamily, MsgFlags, NetlinkAddr, SockFlag, SockProtocol, SockType +}; + +/// Netlink constants +pub const UDEV_EVENT_MODE: u32 = 2; +pub const UDEV_MONITOR_MAGIC: u32 = 0xfeedcafe; +pub const MAX_NETLINK_PAYLOAD: usize = 64 * 1024; // 64 KiB + +// to test, use "udevadm --debug monitor -p" + +// Taken from: https://github.com/systemd/systemd/blob/61afc53924dd3263e7b76b1323a5fe61d589ffd2/src/libsystemd/sd-device/device-monitor.c#L67-L86 +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct MonitorNetlinkHeader { + pub prefix: [u8; 8], + pub magic: u32, + pub header_size: u32, + pub properties_off: u32, + pub properties_len: u32, + pub filter_subsystem_hash: u32, + pub filter_devtype_hash: u32, + pub filter_tag_bloom_hi: u32, + pub filter_tag_bloom_lo: u32, +} + +impl MonitorNetlinkHeader { + pub fn new(properties_len: usize, subsystem: Option<&str>, devtype: Option<&str>) -> Self { + let mut prefix = [0u8; 8]; + // "libudev" plus null: matches original implementation + prefix[..7].copy_from_slice(b"libudev"); + prefix[7] = 0; + + let mut hdr = Self { + prefix, + magic: UDEV_MONITOR_MAGIC.to_be(), + header_size: mem::size_of::() as u32, + properties_off: mem::size_of::() as u32, + properties_len: properties_len as u32, + filter_subsystem_hash: 0, + filter_devtype_hash: 0, + filter_tag_bloom_hi: 0, + filter_tag_bloom_lo: 0, + }; + + if let Some(s) = subsystem { + hdr.filter_subsystem_hash = string_hash32(s).to_be(); + } + if let Some(d) = devtype { + hdr.filter_devtype_hash = string_hash32(d).to_be(); + } + + hdr + } + + /// Serialize header to bytes (safe copy) + pub fn to_bytes(&self) -> Vec { + // repr(C) fixed-size struct -> safe to transmute bytes by copying + let ptr = self as *const MonitorNetlinkHeader as *const u8; + unsafe { std::slice::from_raw_parts(ptr, mem::size_of::()).to_vec() } + } +} + +pub fn string_hash32(s: &str) -> u32 { + // Note: needs to be compatible with https://github.com/systemd/systemd/blob/main/src/libudev/libudev-monitor.c + // and https://gitlab.freedesktop.org/libinput/libinput/-/blob/main/src/udev-seat.c?ref_type=heads. + // Because in our use case, only subsystem "input" is relevant, we just hard code the values from murmur hash 2. + match s { + "input" => 3248653424, + "" => 0, + _ => panic!("uncovered use case") + } +} + +/// Open netlink socket, bind to groups +fn open_netlink(groups: u32) -> Result { + // Domain AF_NETLINK, type SOCK_RAW, protocol NETLINK_KOBJECT_UEVENT + let fd = socket( + AddressFamily::Netlink, + SockType::Raw, + SockFlag::empty(), + SockProtocol::NetlinkKObjectUEvent, + ) + .map_err(|e| format!("Could not create netlink socket: {}", e))?; + + // pid 0 => the kernel takes care of assigning it. + let sockaddr=NetlinkAddr::new(0, groups); + let raw_fd= fd.as_raw_fd(); + + bind(raw_fd, &sockaddr).map_err(|e| { + format!("Could not bind netlink socket: {}", e) + })?; + + Ok(fd) +} + +/// Send the monitor header + payload over NETLINK_KOBJECT_UEVENT. +/// - `payload` should be the raw udev-style `\0` separated key=value bytes (no base64) +/// - `subsystem`/`devtype` optionally used to compute filter hashes +pub fn send_udev_monitor_message( + payload: &[u8], + subsystem: Option<&str>, + devtype: Option<&str>, + groups: u32, +) -> Result<(), String> { + if payload.len() + mem::size_of::() > MAX_NETLINK_PAYLOAD { + return Err(format!( + "Total payload too large: {} bytes (max {})", + payload.len() + mem::size_of::(), + MAX_NETLINK_PAYLOAD + )); + } + + let header = MonitorNetlinkHeader::new(payload.len(), subsystem, devtype); + let header_bytes = header.to_bytes(); + + let fd = open_netlink(groups)?; + + // prepare iovecs + let iov = [ + IoSlice::new(&header_bytes), + IoSlice::new(payload), + ]; + + // destination sockaddr (NULL nl_pid => kernel / multicast) + let sockaddr = NetlinkAddr::new(0, groups); + + let _rc = sendmsg(fd.as_raw_fd(), &iov, &[], MsgFlags::empty(), Some(&sockaddr)) + .map_err(|e| format!("Could not send message: {}", e)); + debug!("udev message sent"); + + // ensure cleanup + drop(fd); + + Ok(()) +} + +pub fn send_udev_monitor_message_with_properties(properties:HashMap) { + let device_name = match properties.get("DEVNAME") { + Some(name) => name, + None => "unknown device" + }; + debug!("Sending udev message over netlink for {}",device_name); + let mut payload:Vec = Vec::new(); + for (key,value) in properties.iter() { + payload.extend(key.as_bytes()); + payload.extend("=".as_bytes()); + payload.extend(value.as_bytes()); + payload.push(0); + } + + send_udev_monitor_message(&payload,Some("input"),None,UDEV_EVENT_MODE).unwrap(); +} + +// println!("{:02X?}", payload); +// 746573743D76616C75650043555252454E545F544147533D3A736561745F7675696E7075743A00544147533D3A736561745F7675696E7075743A00444556504154483D2F646576696365732F7669727475616C2F696E7075742F696E7075743133382F6576656E74390049445F5655494E5055545F4D4F5553453D31004D494E4F523D37330049445F494E5055543D31002E494E5055545F434C4153533D6D6F757365005345514E554D3D3134393231002E484156455F485744425F50524F504552544945533D31004D414A4F523D313300414354494F4E3D6164640049445F53455249414C3D6E6F73657269616C004445564E414D453D2F6465762F696E7075742F6576656E743900555345435F494E495449414C495A45443D31373337373733353034373139320049445F5655494E5055543D310049445F534541543D736561745F7675696E7075740053554253595354454D3D696E70757400 +// dGVzdD12YWx1ZQBDVVJSRU5UX1RBR1M9OnNlYXRfdnVpbnB1dDoAVEFHUz06c2VhdF92dWlucHV0OgBERVZQQVRIPS9kZXZpY2VzL3ZpcnR1YWwvaW5wdXQvaW5wdXQxMzgvZXZlbnQ5AElEX1ZVSU5QVVRfTU9VU0U9MQBNSU5PUj03MwBJRF9JTlBVVD0xAC5JTlBVVF9DTEFTUz1tb3VzZQBTRVFOVU09MTQ5MjEALkhBVkVfSFdEQl9QUk9QRVJUSUVTPTEATUFKT1I9MTMAQUNUSU9OPWFkZABJRF9TRVJJQUw9bm9zZXJpYWwAREVWTkFNRT0vZGV2L2lucHV0L2V2ZW50OQBVU0VDX0lOSVRJQUxJWkVEPTE3Mzc3NzM1MDQ3MTkyAElEX1ZVSU5QVVQ9MQBJRF9TRUFUPXNlYXRfdnVpbnB1dABTVUJTWVNURU09aW5wdXQA + + + +/* +UDEV [16427452.069342] add /devices/virtual/input/input97 (input) +ACTION=add +DEVPATH=/devices/virtual/input/input97 +SUBSYSTEM=input +PRODUCT=3/beef/dead/0 +NAME="Example device" +PROP=0 +EV=3 +KEY=ffffffefffff fffffffffffffffe +MODALIAS=input:b0003vBEEFpDEADe0000-e0,1,kramlsfw +SEQNUM=14498 +USEC_INITIALIZED=16427452066918 +ID_VUINPUT_KEYBOARD=1 +ID_INPUT=1 +ID_INPUT_KEY=1 +.INPUT_CLASS=kbd +ID_SERIAL=noserial +ID_SEAT=seat_vuinput +TAGS=:seat: +CURRENT_TAGS=:seat: + +UDEV [16427452.089779] add /devices/virtual/input/input97/event9 (input) +ACTION=add +DEVPATH=/devices/virtual/input/input97/event9 +SUBSYSTEM=input +DEVNAME=/dev/input/event9 +SEQNUM=14499 +USEC_INITIALIZED=16427452068006 +ID_VUINPUT_KEYBOARD=1 +.HAVE_HWDB_PROPERTIES=1 +ID_INPUT=1 +ID_INPUT_KEY=1 +.INPUT_CLASS=kbd +ID_SERIAL=noserial +ID_SEAT=seat_vuinput +MAJOR=13 +MINOR=73 +TAGS=:seat_vuinput:power-switch: +CURRENT_TAGS=:seat_vuinput:power-switch: + + +*/ \ No newline at end of file diff --git a/vuinputd/src/container/remove_from_container_job.rs b/vuinputd/src/container/remove_from_container_job.rs new file mode 100644 index 0000000..153c1fe --- /dev/null +++ b/vuinputd/src/container/remove_from_container_job.rs @@ -0,0 +1,77 @@ +use std::{collections::HashMap, future::Future, pin::Pin, time::Duration}; + +use async_io::Timer; +use async_pidfd::AsyncPidFd; +use log::debug; + +use crate::{ + container::{mknod_input_device::ensure_input_device, netlink_message::send_udev_monitor_message_with_properties, runtime_data::{self, ensure_udev_structure, read_udev_data, write_udev_data}}, jobs::job::{Job, JobTarget}, monitor_udev::EVENT_STORE, namespace::{run_in_net_and_mnt_namespace, Namespaces} +}; + +#[derive(Clone,Debug)] +pub struct RemoveFromContainerJob { + namespaces: Namespaces, + target: JobTarget, + sys_path: String, +} + +impl RemoveFromContainerJob { + pub fn new(namespaces: Namespaces,sys_path: String) -> Self { + Self { + namespaces: namespaces.clone(), + target: JobTarget::Container(namespaces), + sys_path: sys_path, + } + } +} + +impl Job for RemoveFromContainerJob { + fn desc(&self) -> &str { + "Remove input device from container" + } + + fn execute_after_cancellation(&self) -> bool { + false + } + + fn create_task(self: &RemoveFromContainerJob) -> Pin>> { + Box::pin(self.clone().remove_from_container()) + } + + fn job_target(&self) -> JobTarget { + self.target.clone() + } +} + +impl RemoveFromContainerJob { + async fn remove_from_container(self) { + // TODO: Here is a race with inject in container. + let netlink_event = match EVENT_STORE.get().unwrap().lock().unwrap().take(&self.sys_path) { + Some(netlink_event) => netlink_event, + None => { + debug!("do nothing, because the device has never been announced via netlink"); + return; + } + }; + + if netlink_event.tombstone { + debug!("do nothing, because the device has already been removed in the meantime"); + return; + } + let netlink_data=netlink_event.add_data; + + // define for capturing + let mut netlink_data = netlink_data.unwrap().clone(); + + let _ = netlink_data.insert("ACTION".to_string(),"remove".to_string()); + let child_pid = run_in_net_and_mnt_namespace(self.namespaces, Box::new(move || { + + send_udev_monitor_message_with_properties(netlink_data.clone()); + + })) + .expect("subprocess should work"); + let pid_fd = AsyncPidFd::from_pid(child_pid.as_raw()).unwrap(); + let _exit_info = pid_fd.wait().await.unwrap(); + + } +} diff --git a/vuinputd/src/container/runtime_data.rs b/vuinputd/src/container/runtime_data.rs new file mode 100644 index 0000000..db26d7e --- /dev/null +++ b/vuinputd/src/container/runtime_data.rs @@ -0,0 +1,109 @@ +use std::fs::{self, File}; +use std::io::{self, Write}; +use std::path::Path; + +/// Ensure required udev directories and files exist +pub fn ensure_udev_structure() -> io::Result<()> { + // TODO: this _must_ exist, before a service using libinput is run. The time of device creation might be too late + + + let data_dir = Path::new("/run/udev/data"); + let control_file = Path::new("/run/udev/control"); + + // Create directory like `mkdir -p` + if !data_dir.exists() { + fs::create_dir_all(data_dir)?; + } + + // Ensure /run/udev/control exists, create empty if not + if !control_file.exists() { + File::create(control_file)?; + } + + Ok(()) +} + +/// Write udev data entry for a given major/minor number +/// - `content` = original udev data text +/// - `major`, `minor` = device numbers +/// +/// Performs these transforms: +/// - remove all lines containing `ID_SEAT=` +/// - remove all lines containing `seat_` references (G:, Q: lines) +/// - replace ID_VUINPUT_* with ID_INPUT_* +/// - write updated content to `/run/udev/data/c:` +pub fn write_udev_data(content: &str, major: u64, minor: u64) -> io::Result<()> { + let mut cleaned = String::new(); + + for line in content.lines() { + // skip seat-related lines + if line.contains("ID_SEAT=") || line.contains("seat_") { + continue; + } + + // perform replacements + let line = line + .replace("ID_VUINPUT_KEYBOARD=1", "ID_INPUT_KEYBOARD=1") + .replace("ID_VUINPUT_MOUSE=1", "ID_INPUT_MOUSE=1"); + + cleaned.push_str(&line); + cleaned.push('\n'); + } + + + let path = format!("/run/udev/data/c{}:{}", major, minor); + let mut file = File::create(&path)?; + file.write_all(cleaned.as_bytes())?; + + Ok(()) +} + + +pub fn read_udev_data(major: u64, minor: u64) -> io::Result { + let path = format!("/run/udev/data/c{}:{}", major, minor); + fs::read_to_string(path) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_replacement_and_filter() { + let input = r#"I:16429403327735 +E:ID_VUINPUT_KEYBOARD=1 +E:ID_INPUT=1 +E:ID_INPUT_KEY=1 +E:ID_SERIAL=noserial +E:ID_SEAT=seat_vuinput +G:seat_vuinput +G:power-switch +Q:seat_vuinput +Q:power-switch +V:1"#; + + let expected = r#"I:16429403327735 +E:ID_INPUT_KEYBOARD=1 +E:ID_INPUT=1 +E:ID_INPUT_KEY=1 +E:ID_SERIAL=noserial +G:power-switch +Q:power-switch +V:1 +"#; + + let mut cleaned = String::new(); + for line in input.lines() { + if line.contains("ID_SEAT=") || line.contains("seat_") { + continue; + } + let line = line + .replace("ID_VUINPUT_KEYBOARD=1", "ID_INPUT_KEYBOARD=1") + .replace("ID_VUINPUT_MOUSE=1", "ID_INPUT_MOUSE=1"); + cleaned.push_str(&line); + cleaned.push('\n'); + } + + assert_eq!(cleaned, expected); + } +} diff --git a/vuinputd/src/jobs/closure_job.rs b/vuinputd/src/jobs/closure_job.rs new file mode 100644 index 0000000..b7f9136 --- /dev/null +++ b/vuinputd/src/jobs/closure_job.rs @@ -0,0 +1,85 @@ +use std::future::Future; +use std::pin::Pin; + +use crate::jobs::job::{Dispatcher, Job, JobTarget}; + +pub struct ClosureJob { + desc: String, + execute_after_cancellation: bool, + target: JobTarget, + task_creator: Box Pin>> + Send + 'static>, +} + +impl ClosureJob { + pub fn new( + desc: impl Into, + target: JobTarget, + execute_after_cancellation: bool, + f: Box< + dyn Fn(JobTarget) -> Pin>> // closure returns any future + + Send // the closure itself can be sent across threads + + 'static, + >, + ) -> Self +where { + Self { + desc: desc.into(), + execute_after_cancellation, + target, + task_creator: f, + } + } +} + +impl Job for ClosureJob { + fn desc(&self) -> &str { + &self.desc + } + + fn execute_after_cancellation(&self) -> bool { + self.execute_after_cancellation + } + + fn create_task(self: &ClosureJob) -> Pin>> { + let creator = &self.task_creator; + let target = self.job_target(); + let task = creator(target); + task + } + + fn job_target(&self) -> JobTarget { + self.target.clone() + } +} + +/// Example usage +#[allow(dead_code)] +pub fn example() { + let mut dispatcher = Dispatcher::new(); + + // Send a Host job + dispatcher.dispatch(Box::new(ClosureJob::new( + "Host maintenance", + JobTarget::Host, + false, + Box::new(|target| { + Box::pin(async move { + println!("Running host job on {:?}", target); + }) + }), + ))); + + // Sending a Container job works the same + // dispatcher.dispatch(Job::new(JobTarget::Container(ns.clone()), "Container task", false, |target| async move { + // println!("Running container job for {:?}", target); + // })); + + // + // JOB_DISPATCHER.get().unwrap().lock().unwrap().dispatch(Box::new(ClosureJob::new("Monitor udev events", JobTarget::BackgroundLoop,false, + // Box::new(move |_target| Box::pin(monitor_udev::udev_monitor_loop(cancel_token.clone())))))); + + + // Allow loops to run briefly before dropping all senders -> graceful shutdown + dispatcher.close(); + dispatcher.wait_until_finished(); +} diff --git a/vuinputd/src/jobs/job.rs b/vuinputd/src/jobs/job.rs new file mode 100644 index 0000000..e7627c1 --- /dev/null +++ b/vuinputd/src/jobs/job.rs @@ -0,0 +1,203 @@ +use async_channel::{Receiver, Sender}; +use futures::executor::{LocalPool, LocalSpawner}; +use futures::future::RemoteHandle; +use futures::task::LocalSpawnExt; +use std::sync::Mutex; +use std::thread::{self, JoinHandle}; +use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc}; + +use crate::namespace::Namespaces; + +// To discuss: +// what we handle here, could also be named Task. The decision for job was more or less +// because the main goal was to run some short "scripts" that create files etc. +// see e.g., https://blog.yoshuawuyts.com/async-cancellation-1/ + +/// Represents where a job should run. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum JobTarget { + /// A global or host-wide task. + Host, + BackgroundLoop, + /// A specific container or namespace target. + Container(Namespaces), +} + +pub trait Job: Send + 'static { + /// Free-form description, used for logging or debugging + fn desc(&self) -> &str; + + /// Job Target + fn job_target(&self) -> JobTarget; + + /// Whether the job should still execute after cancellation + fn execute_after_cancellation(&self) -> bool { + false + } + + /// Main entry point — creates the future that executes this job + fn create_task(self: &Self) -> Pin>>; +} + +impl std::fmt::Debug for dyn Job { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Job") + .field("target", &self.job_target()) + .field("desc", &self.desc()) + .finish() + } +} + +/// Central dispatcher that manages per-target async loops. +#[derive(Debug)] +pub struct Dispatcher { + thread_handle: Option>, + tx: Option>>, + future_handles: Arc>>>, +} + +impl Dispatcher { + /// Create a new dispatcher and return its sender handle. + pub fn new() -> Self { + let (tx, rx) = async_channel::unbounded(); + + // Map of active per-target senders. + let targets: Arc>>>> = + Arc::new(Mutex::new(HashMap::new())); + + let rx_in_thread: Receiver> = rx.clone(); + let future_handles: Arc>>> = Arc::new(Mutex::new(Vec::new())); + let future_handles_for_thread = future_handles.clone(); + // run dispatcher in a dedicated thread + let thread_handle = thread::spawn(move || { + let mut pool = LocalPool::new(); + let spawner = pool.spawner(); + + let dispatcher_loop_handle = spawner + .spawn_local_with_handle(spawn_dispatcher_loop( + spawner.clone(), + targets, + rx_in_thread, + future_handles_for_thread.clone(), + )) + .unwrap(); + future_handles_for_thread + .lock() + .unwrap() + .push(dispatcher_loop_handle); + pool.run(); // blocks until all tasks complete + }); + + Self { + thread_handle: Some(thread_handle), + tx: Some(tx), + future_handles: future_handles, + } + } + + pub fn dispatch(&mut self, job: Box) { + self.tx + .as_ref() + .expect("Dispatcher already closed") + .send_blocking(job) + .unwrap(); + } + + pub fn close(&mut self) { + self.tx = None; + self.future_handles.lock().unwrap().clear(); + } + + pub fn wait_until_finished(&mut self) { + self.tx = None; + self.future_handles.lock().unwrap().clear(); + let handle = self.thread_handle.take(); + handle.unwrap().join().unwrap(); + } +} + +/// Run the dispatcher: listen for incoming jobs and route them to the right loop. +async fn spawn_dispatcher_loop( + spawner: LocalSpawner, + targets: Arc>>>>, + rx: Receiver>, + future_handles: Arc>>>, +) { + loop { + let received_job = rx.recv().await; + match received_job { + Ok(job) => { + if job.job_target() == JobTarget::BackgroundLoop { + // this is a separate loop that just runs in parallel and does not need a queue to be ordered. + let background_loop_handle = + spawner.spawn_local_with_handle(job.create_task()).unwrap(); + future_handles.lock().unwrap().push(background_loop_handle); + log::info!("Spawned new background loop for {:?}", job.desc()); + } else { + let target = job.job_target(); + let (tx, newly_created) = get_or_spawn_target_loop( + spawner.clone(), + targets.clone(), + target.clone(), + future_handles.clone(), + ) + .await; + if newly_created { + log::info!("Spawned new loop for {:?}", target); + } + if let Err(e) = tx.send(job).await { + log::warn!("Failed to enqueue job: {e}"); + } + } + } + Err(_err) => { + // channel has been closed + log::info!("Channel has been closed {:?}", _err); + break; + } + } + } + log::info!("Global dispatcher shutting down gracefully"); +} + +/// Get or lazily create a target-specific queue and loop. +async fn get_or_spawn_target_loop( + spawner: LocalSpawner, + targets: Arc>>>>, + target: JobTarget, + future_handles: Arc>>>, +) -> (Sender>, bool) { + let mut map = targets.lock().unwrap(); + if let Some(tx) = map.get(&target) { + return (tx.clone(), false); + } + + let (tx, rx) = async_channel::unbounded(); + map.insert(target.clone(), tx.clone()); + drop(map); // release lock before spawning + + let job_target_loop_handle = spawner + .spawn_local_with_handle(job_target_loop(target.clone(), rx)) + .unwrap(); + future_handles.lock().unwrap().push(job_target_loop_handle); + + (tx, true) +} + +/// The main loop for a single job target (container or host). +async fn job_target_loop(target: JobTarget, rx: Receiver>) { + log::info!("Starting loop for {:?}", target); + while let Ok(job) = rx.recv().await { + log::debug!("Executing job: {}", job.desc()); + job.create_task().await; + } + log::info!("Loop for {:?} ended — channel closed", target); +} + +/* +macro_rules! job { + ($desc:expr, async move { $($body:tt)* }) => { + Box::new(ClosureJob::new($desc, |_: JobTarget| async move { $($body)* })) + }; +} + */ diff --git a/vuinputd/src/jobs/job_builder.rs b/vuinputd/src/jobs/job_builder.rs new file mode 100644 index 0000000..6827e89 --- /dev/null +++ b/vuinputd/src/jobs/job_builder.rs @@ -0,0 +1,106 @@ +/* +use std::sync::Arc; +use std::sync::atomic::AtomicBool; +use std::time::Duration; +use futures::FutureExt; // for .timeout() + +pub struct JobBuilder { + inner: J, + timeout: Option, + cancel_token: Option>, + execute_despite_cancellation: bool, + log: bool, +} + +impl JobBuilder { + pub fn new(inner: J) -> Self { + Self { + inner, + timeout: None, + cancel_token: None, + log: false, + } + } + + pub fn with_timeout(mut self, dur: Duration) -> Self { + self.timeout = Some(dur); + self + } + + pub fn with_cancellation(mut self, token: Arc) -> Self { + self.cancel_token = Some(token); + self + } + + pub fn execute_despite_cancellation(mut self, execute: bool) -> Self { + self.execute_despite_cancellation = execute; + self + } + + pub fn with_logging(mut self) -> Self { + self.log = true; + self + } + + pub fn build(self) -> WrappedJob { + WrappedJob { + inner: self.inner, + timeout: self.timeout, + cancel_token: self.cancel_token, + log: self.log, + } + } +} + +pub struct WrappedJob { + inner: J, + timeout: Option, + cancel_token: Option>, + log: bool, +} + +impl Job for WrappedJob { + fn desc(&self) -> &str { + self.inner.desc() + } + + fn create_task(self: Box) -> Pin + Send>> { + Box::pin(async move { + let desc = self.inner.desc().to_string(); + let mut fut = self.inner.create_task(); + + if + + // Logging + if self.log { + println!("[START] {desc}"); + } + + // Cancellation should work cooperatively + if let Some(token) = self.cancel_token.clone() { + fut = Box::pin(async move { + futures::select! { + _ = fut.fuse() => {}, + _ = async { + while !token.load(std::sync::atomic::Ordering::Relaxed) { + futures_timer::Delay::new(Duration::from_millis(50)).await; + } + }.fuse() => {}, + } + }); + } + + // Timeout + if let Some(dur) = self.timeout { + fut = Box::pin(fut.timeout(dur).map(|_| ())); + } + + fut.await; + + if self.log { + println!("[DONE] {desc}"); + } + }) + } +} + */ \ No newline at end of file diff --git a/vuinputd/src/jobs/mod.rs b/vuinputd/src/jobs/mod.rs new file mode 100644 index 0000000..39588ac --- /dev/null +++ b/vuinputd/src/jobs/mod.rs @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz +//! # Design: Async Per-Container Job Executor (Tokio) +//! +//! ## Overview +//! A scalable, structured design for running async jobs per container. +//! +//! - Global dispatcher routes jobs to per-container async loops or to global queue. +//! - Each container has its own unbounded job queue (no backpressure). +//! - Loops are spawned lazily on first job and exit when their sender drops. +//! - Graceful shutdown happens automatically (channel close → loop exit). +//! - Periodic cleanup removes idle container queues. +//! +//! ## Async Jobs +//! - Each `Job` contains an async closure `task: Box Pin>> + Send>` +//! - This allows full async/await usage inside the job body. +//! +//! +//! +--------------------------------------+ +//! | Global dispatcher | +//! +----------+---------------------------+ +//! | | +//! v v +//! +----------+-----------+ +------------+ +//! | Per-container queues | | Host queue | +//! +----+------+----+-----+ +------------+ +//! | | | +//! +----v----+ +---v----+ +---v----+ +//! | Cont A | | Cont B | | Cont C | +//! | loop() | | loop() | | loop() | +//! +---------+ +--------+ +--------+ + +pub mod closure_job; +pub mod job; diff --git a/vuinputd/src/main.rs b/vuinputd/src/main.rs new file mode 100644 index 0000000..9176de4 --- /dev/null +++ b/vuinputd/src/main.rs @@ -0,0 +1,727 @@ +// SPDX-License-Identifier: MIT +// vuinputd: container-safe mediation daemon for /dev/uinput +// +// - Exposes a fake /dev/uinput inside the container (via CUSE). +// - Forwards ioctls + writes to the real /dev/uinput on the host. +// +// Author: Johannes Leupolz + +// TODOS: +// preliminary close +// remove test +// correct char device for vuinput +// renaming +// use in container +// cancellation token +// device removal (udev netlink message) +// distinguish between cleanup jobs that must not be cancelled and other jobs (especially background jobs) +// naming: dev_path vs dev_node. I guess I mean the same. +// Send warning, if udev monitor does not exist + + +// test scenario: +// vuinputd +// mouse-advanced +// ls -lh /dev/input/event9 +// cat /run/udev/data/c13:73 +// machinectl shell johannes@gamestreamingserver +// systemctl --user stop headless-labwc.service +// machinectl shell gamestreamingserver +// mkdir -p /run/udev/data/ +// touch /run/udev/control +// mknod /dev/input/event9 c 13 73 +// vim /run/udev/data/c13:73 +// machinectl shell johannes@gamestreamingserver +// systemctl --user start headless-labwc.service +// systemctl --user start wayvnc.service + + +//libinput backend +// seatd +//libinput-tools +// groups +// 777 for input9 +// char-input to systemd + +use libc::O_CLOEXEC; +use libc::{iovec, off_t, size_t, EBADRQC, EIO, ENOENT}; +use libc::{uinput_abs_setup, uinput_ff_erase, uinput_ff_upload, uinput_setup}; +use libfuse_sys::cuse_lowlevel; +use libfuse_sys::fuse_lowlevel; +use log::{debug, error, info, trace}; +use std::collections::HashMap; +use std::ffi::{CStr, CString}; +use std::fs; +use std::fs::{File, OpenOptions}; +use std::io::Write; +use std::io::{self, ErrorKind}; +use std::os::fd::AsRawFd; +use std::os::raw::{c_char, c_int, c_uint, c_void}; +use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt}; +use std::path::Path; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Arc, Mutex, OnceLock, RwLock}; +use uinput_ioctls::*; + +pub mod namespace; +pub mod monitor_udev; +use crate::container::inject_in_container_job::InjectInContainerJob; +use crate::container::netlink_message; +use crate::container::remove_from_container_job::RemoveFromContainerJob; +use crate::jobs::closure_job::ClosureJob; +use crate::monitor_udev::MonitorBackgroundLoop; +use crate::namespace::*; + +pub mod jobs; +use crate::jobs::job::*; + +pub mod container; + + +#[derive(Debug)] +struct VuInputDevice { + cuse_fh : u64, + major : u64, + minor : u64, + syspath: String, + devnode: String, + runtime_data: Option, + netlink_data: Option +} + +#[derive(Debug)] +struct VuInputState { + file: File, + ns_of_requestor: Namespaces, + input_device: Option +} + +#[derive(Debug,Eq, Hash, PartialEq, Clone)] +enum VuFileHandle { + Fh(u64) +} + +impl VuFileHandle { + fn from_fuse_file_info(fi: &fuse_lowlevel::fuse_file_info) -> VuFileHandle { + VuFileHandle::Fh(fi.fh) + } +} + +impl std::fmt::Display for VuFileHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + VuFileHandle::Fh(fh) => writeln!(f, "VuFileHandle({:?})",fh)?, + } + Ok(()) + } +} + +static VUINPUT_COUNTER: OnceLock = OnceLock::new(); +static VUINPUT_STATE: OnceLock>>>> = OnceLock::new(); +static JOB_DISPATCHER: OnceLock>= OnceLock::new(); +static SELF_NAMESPACES: OnceLock= OnceLock::new(); + + +const SYS_INPUT_DIR: &str = "/sys/devices/virtual/input/"; + +fn get_vuinput_state( + fh:&VuFileHandle, +) -> Result>, String> { + let map = VUINPUT_STATE + .get() + .ok_or("global not initialized".to_string())?; + let guard = map.read().map_err(|e| e.to_string())?; + guard + .get(&fh) + .cloned() + .ok_or("handle not opened".to_string()) +} + +fn get_fresh_filehandle() -> u64 { + let ctr = VUINPUT_COUNTER.get().unwrap(); + ctr.fetch_add(1, Ordering::SeqCst).into() +} + +fn insert_vuinput_state( + fh:&VuFileHandle, + state: VuInputState, +) -> Result<(), String> { + let map = VUINPUT_STATE + .get() + .ok_or("global not initialized".to_string())?; + let mut guard = map.write().map_err(|e| e.to_string())?; + + if guard.contains_key(&fh) { + return Err(format!( + "file handle {} already exists. file handles must not be reused!", + &fh + )); + } + + let _ = guard.insert(fh.clone(), Arc::new(Mutex::new(state))); + Ok(()) +} + +fn remove_vuinput_state( + fh:&VuFileHandle, +) -> Result>, String> { + let map = VUINPUT_STATE + .get() + .ok_or("global not initialized".to_string())?; + let mut guard = map.write().map_err(|e| e.to_string())?; + let old_value = guard.remove(&fh).ok_or("fh unknown")?; + Ok(old_value) +} + +fn fetch_device_node(path: &str) -> io::Result { + for entry in fs::read_dir(path)? { + let entry = entry?; // propagate per-entry errors + if let Some(name) = entry.file_name().to_str() { + if name.starts_with("event") { + return Ok(format!("/dev/input/{}", name)); + } + } + } + // If no device is found, return an error + Err(io::Error::new(ErrorKind::NotFound, "no device found")) +} + +/// Returns (major, minor) numbers of a device node at `path` +fn fetch_major_minor(path: &str) -> io::Result<(u64, u64)> { + let metadata = fs::metadata(path)?; + + // Ensure it's a character device + if !metadata.file_type().is_char_device() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Not a character device", + )); + } + + let rdev = metadata.rdev(); + let major = ((rdev >> 8) & 0xfff) as u64; + let minor = ((rdev & 0xff) | ((rdev >> 12) & 0xfff00)) as u64; + + Ok((major, minor)) +} + +unsafe extern "C" fn vuinput_open( + _req: fuse_lowlevel::fuse_req_t, + _fi: *mut fuse_lowlevel::fuse_file_info, +) { + let fh = get_fresh_filehandle(); + let ctx = fuse_lowlevel::fuse_req_ctx(_req); + debug!("fh {}: opened by process id {} (host view)", fh, (*ctx).pid); + let namespaces = get_namespaces(Some((*ctx).pid)); + debug!("fh {}: namespaces {}", fh, namespaces); + // namespaces net:4026531840, uts:4026531838, ipc:4026531839, pid:4026531836, pid_for_children:4026531836, user:4026531837, mnt:4026531841, cgroup:4026531835, time:4026531834, time_for_children:4026531834 + (*_fi).fh = fh; + // Open the path in read-only mode, returns `io::Result` + let open_vuinput_result = OpenOptions::new() + .read(true) + .write(true) + //.custom_flags(O_NONBLOCK) + .custom_flags(O_CLOEXEC) + .open(Path::new("/dev/uinput")); + match open_vuinput_result { + Ok(v) => { + insert_vuinput_state( + &VuFileHandle::from_fuse_file_info(_fi.as_ref().unwrap()), + VuInputState { + file: v, + ns_of_requestor: namespaces, + input_device: None + }, + ) + .unwrap(); + fuse_lowlevel::fuse_reply_open(_req, _fi); + } + Err(e) => { + error!("couldn't open /dev/uinput: {}", e); + fuse_lowlevel::fuse_reply_err(_req, ENOENT); + } + } +} +unsafe extern "C" fn vuinput_write( + _req: fuse_lowlevel::fuse_req_t, + _buf: *const c_char, + _size: size_t, + _off: off_t, + _fi: *mut fuse_lowlevel::fuse_file_info, +) { + assert!( + _off == 0, + "vuinput_write: offset needs to be 0 but is {}", + _off + ); + let slice = std::slice::from_raw_parts(_buf as *const u8, _size); + let vuinput_state_mutex = get_vuinput_state(&VuFileHandle::from_fuse_file_info(_fi.as_ref().unwrap())).unwrap(); + let mut vuinput_state = vuinput_state_mutex.lock().unwrap(); + + assert!( + vuinput_state.input_device.is_some(), + "legacy device setup not supported, yet!" + ); + let result = vuinput_state.file.write_all(slice); + + match result { + Ok(_) => { + trace!("wrote {} bytes", _size); + fuse_lowlevel::fuse_reply_write(_req, _size); + } + Err(e) => { + debug!("error writing to uinput: {e:?}"); + fuse_lowlevel::fuse_reply_err(_req, EIO); + } + } +} + +unsafe extern "C" fn vuinput_release( + _req: fuse_lowlevel::fuse_req_t, + _fi: *mut fuse_lowlevel::fuse_file_info, +) { + let fh = &(*_fi).fh; + let vuinput_state_mutex = remove_vuinput_state(&VuFileHandle::from_fuse_file_info(_fi.as_ref().unwrap())).unwrap(); + + let mut vuinput_state = vuinput_state_mutex.lock().unwrap(); + + // remove device in container, if the request was really from another namespace + if ! SELF_NAMESPACES.get().unwrap().equal_mnt_and_net(&vuinput_state.ns_of_requestor) { + let remove_job=RemoveFromContainerJob::new(vuinput_state.ns_of_requestor.clone(),vuinput_state.input_device.as_ref().unwrap().syspath.clone()); + JOB_DISPATCHER.get().unwrap().lock().unwrap().dispatch(Box::new(remove_job)); + } + + drop(vuinput_state); + + debug!( + "{}: references left before releasing device {} (expected is 1)", + fh, + Arc::strong_count(&vuinput_state_mutex) + ); + drop(vuinput_state_mutex); // this also closes the file when no other references are open + // TODO: maybe also ensure that nothing is left in the containers + fuse_lowlevel::fuse_reply_err(_req, 0); +} + +unsafe extern "C" fn vuinput_ioctl( + _req: fuse_lowlevel::fuse_req_t, + _cmd: c_int, + _arg: *mut c_void, //note: this is a pointer in the application space and should not be dereferenced at all + _fi: *mut fuse_lowlevel::fuse_file_info, + _flags: c_uint, + _in_buf: *const c_void, // note: this was mapped by the kernel and can be read from + _in_bufsz: size_t, + _out_bufsz: size_t, +) { + // fuse_reply_ioctl_retry is only necessary for variable length commands; + // see comment "Now check variable-length commands" in uinput.c of the linux kernel. + // Those are UI_GET_SYSNAME and UI_ABS_SETUP as of v0.4. + + // ioctl to map are listed on https://www.freedesktop.org/software/libevdev/doc/latest/ioctls.html + // https://docs.rs/linux-raw-sys/0.11.0/src/linux_raw_sys/x86_64/ioctl.rs.html#529 + + let cmd_u64 = (_cmd as c_uint).into(); + // normalize the variable length ones + let cmd_without_size = cmd_u64 & !(nix::sys::ioctl::SIZEMASK << nix::sys::ioctl::SIZESHIFT); + let cmd_normalized = match cmd_without_size { + UI_GET_SYSNAME_WITHOUT_SIZE => UI_GET_SYSNAME_WITHOUT_SIZE, + //UI_ABS_SETUP => UI_ABS_SETUP_WITHOUT_SIZE, + _ => cmd_u64, + }; + let vufh= VuFileHandle::from_fuse_file_info(_fi.as_ref().unwrap()); + let vuinput_state_mutex = get_vuinput_state(&vufh).unwrap(); + let fh = &(*_fi).fh; + let mut vuinput_state = vuinput_state_mutex.lock().unwrap(); + + // ensure for all ioctls that need mapped data, that we have the data correctly mapped + match (_in_bufsz, _out_bufsz, cmd_normalized) { + (0, _, UI_ABS_SETUP) => { + //todo: i guess this needs to be reworked as this is variable size. i guess it is not reachable at all + debug!("fh {}: submitting _in_bufsz for UI_ABS_SETUP", fh); + let iov = iovec { + iov_base: _arg, + iov_len: ::std::mem::size_of::(), + }; + fuse_lowlevel::fuse_reply_ioctl_retry(_req, &iov, 1, std::ptr::null(), 0); + return; + } + (_, 0, UI_GET_SYSNAME_WITHOUT_SIZE) => { + let size = (cmd_u64 & nix::sys::ioctl::SIZEMASK) >> nix::sys::ioctl::SIZESHIFT; + debug!( + "fh {}: submitting _out_bufsz for UI_GET_SYSNAME({}) ", + fh, size + ); + let iov = iovec { + iov_base: _arg, + iov_len: 64, + }; + fuse_lowlevel::fuse_reply_ioctl_retry(_req, std::ptr::null(), 0, &iov, 1); + return; + } + (_, 0, UI_GET_VERSION) => { + let size = (cmd_u64 & nix::sys::ioctl::SIZEMASK) >> nix::sys::ioctl::SIZESHIFT; + debug!( + "fh {}: submitting _out_bufsz for UI_GET_VERSION({}) ", + fh, size + ); + let iov = iovec { + iov_base: _arg, + iov_len: std::mem::size_of::(), + }; + fuse_lowlevel::fuse_reply_ioctl_retry(_req, std::ptr::null(), 0, &iov, 1); + return; + } + (0, _, UI_DEV_SETUP) => { + debug!("fh {}: submitting _in_bufsz for UI_DEV_SETUP", fh); + let iov = iovec { + iov_base: _arg, + iov_len: ::std::mem::size_of::(), + }; + fuse_lowlevel::fuse_reply_ioctl_retry(_req, &iov, 1, std::ptr::null(), 0); + return; + } + (0, _, UI_SET_PHYS) => { + debug!("fh {}: submitting _in_bufsz for UI_SET_PHYS", fh); + let iov = iovec { + iov_base: _arg, + iov_len: ::std::mem::size_of::() * 1024, + }; + fuse_lowlevel::fuse_reply_ioctl_retry(_req, &iov, 1, std::ptr::null(), 0); + return; + } + (0, _, UI_BEGIN_FF_UPLOAD) => { + debug!("fh {}: submitting _in_bufsz for UI_BEGIN_FF_UPLOAD", fh); + let iov = iovec { + iov_base: _arg, + iov_len: ::std::mem::size_of::(), + }; + fuse_lowlevel::fuse_reply_ioctl_retry(_req, &iov, 1, &iov, 1); + return; + } + (0, _, UI_END_FF_UPLOAD) => { + debug!("fh {}: submitting _in_bufsz for UI_END_FF_UPLOAD", fh); + let iov = iovec { + iov_base: _arg, + iov_len: ::std::mem::size_of::(), + }; + fuse_lowlevel::fuse_reply_ioctl_retry(_req, &iov, 1, std::ptr::null(), 0); + return; + } + (0, _, UI_BEGIN_FF_ERASE) => { + debug!("fh {}: submitting _in_bufsz for UI_BEGIN_FF_ERASE", fh); + let iov = iovec { + iov_base: _arg, + iov_len: ::std::mem::size_of::(), + }; + fuse_lowlevel::fuse_reply_ioctl_retry(_req, &iov, 1, &iov, 1); + return; + } + (0, _, UI_END_FF_ERASE) => { + debug!("fh {}: submitting _in_bufsz for UI_END_FF_ERASE", fh); + let iov = iovec { + iov_base: _arg, + iov_len: ::std::mem::size_of::(), + }; + fuse_lowlevel::fuse_reply_ioctl_retry(_req, &iov, 1, std::ptr::null(), 0); + return; + } + _ => { + //nothing to map + } + } + + let fd = vuinput_state.file.as_raw_fd(); + + // now we can assume that the data is mapped or it is not required + match cmd_normalized { + UI_DEV_CREATE => { + debug!("fh {}: ioctl UI_DEV_CREATE", fh); + ui_dev_create(fd).unwrap(); + + let mut resultbuf: [i8; 64] = [0i8; 64]; + ui_get_sysname(fd, resultbuf.as_mut_slice()).unwrap(); + let sysname = format!( + "{}{}", + SYS_INPUT_DIR, + CStr::from_ptr(resultbuf.as_ptr()).to_string_lossy() + ); + debug!("fh {}: syspath: {}", fh, sysname); + let devnode = fetch_device_node(&sysname).unwrap(); + debug!("fh {}: devnode: {}", fh, devnode); + let (major,minor) = fetch_major_minor(&devnode).unwrap(); + debug!("fh {}: major: {} minor: {} ", fh, major,minor); + vuinput_state.input_device = Some(VuInputDevice {cuse_fh:*fh, major: major, minor: minor, syspath: sysname.clone(), devnode: devnode.clone(), runtime_data: None, netlink_data: None }); + + // Create device in container, if the request was really from another namespace + if ! SELF_NAMESPACES.get().unwrap().equal_mnt_and_net(&vuinput_state.ns_of_requestor) { + let inject_job=InjectInContainerJob::new(vuinput_state.ns_of_requestor.clone(),devnode.clone(),sysname.clone(),major,minor); + JOB_DISPATCHER.get().unwrap().lock().unwrap().dispatch(Box::new(inject_job)); + } + + // hard code 2 second sleep + std::thread::sleep(std::time::Duration::from_secs(2)); + + fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0); + } + UI_DEV_DESTROY => { + debug!("fh {}: ioctl UI_DEV_DESTROY", fh); + + // Remove device in container, if the request was really from another namespace + if ! SELF_NAMESPACES.get().unwrap().equal_mnt_and_net(&vuinput_state.ns_of_requestor) { + let remove_job=RemoveFromContainerJob::new(vuinput_state.ns_of_requestor.clone(),vuinput_state.input_device.as_ref().unwrap().syspath.clone()); + JOB_DISPATCHER.get().unwrap().lock().unwrap().dispatch(Box::new(remove_job)); + } + + + ui_dev_destroy(fd).unwrap(); + fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0); + } + UI_DEV_SETUP => { + debug!("fh {}: ioctl UI_DEV_SETUP", fh); + assert!(_in_bufsz != 0, "should have _in_bufsz"); + let setup_ptr = _in_buf as *mut uinput_setup; + debug!( + "product: {:x} vendor: {:x}", + (*setup_ptr).id.product, + (*setup_ptr).id.vendor + ); + // replace vendor and product id to the values from sunshine (see inputtino_common.h of sunshine) + (*setup_ptr).id.product = 0xdead; + (*setup_ptr).id.vendor = 0xbeef; + ui_dev_setup(fd, setup_ptr).unwrap(); + fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0); + } + UI_ABS_SETUP => { + //todo: i guess this needs to be reworked as this is variable size. i guess it is not reachable at all + debug!("fh {}: ioctl UI_ABS_SETUP", fh); + assert!(_in_bufsz != 0, "should have _in_bufsz"); + fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0); + } + UI_GET_SYSNAME_WITHOUT_SIZE => { + debug!("fh {}: ioctl UI_GET_SYSNAME {_out_bufsz}", fh); + assert!( + _out_bufsz == 64, + "should have _out_bufsz of length 64 (currently hardcoded)" + ); + let mut resultbuf: [i8; 64] = [0i8; 64]; + ui_get_sysname(fd, resultbuf.as_mut_slice()).unwrap(); + let sysname = CStr::from_ptr(resultbuf.as_ptr()).to_string_lossy(); + debug!("fh {}: sysname: {}", fh, sysname); + fuse_lowlevel::fuse_reply_ioctl( + _req, + 0, + resultbuf.as_mut_ptr() as *mut c_void, + _out_bufsz, + ); + } + UI_GET_VERSION => { + let mut version_of_kernel = 0; + let pversion_of_kernel = std::ptr::from_mut(&mut version_of_kernel); + ui_get_version(fd, pversion_of_kernel).unwrap(); + debug!("fh {}: ioctl UI_GET_VERSION {}", fh, version_of_kernel); + let reply_arg = 5; + let preply_arg = std::ptr::from_ref(&reply_arg); + fuse_lowlevel::fuse_reply_ioctl( + _req, + 0, + preply_arg as *const c_void, + std::mem::size_of::(), + ); + } + UI_SET_EVBIT => { + let value = _arg as c_uint; + debug!("fh {}: ioctl UI_SET_EVBIT {}", fh, value); + ui_set_evbit(fd, value.into()).unwrap(); + fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0); + } + UI_SET_KEYBIT => { + let value = _arg as c_uint; + debug!("fh {}: ioctl UI_SET_KEYBIT {}", fh, value); + ui_set_keybit(fd, value.into()).unwrap(); + fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0); + } + UI_SET_RELBIT => { + let value = _arg as c_uint; + debug!("fh {}: ioctl UI_SET_RELBIT {}", fh, value); + ui_set_relbit(fd, value.into()).unwrap(); + fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0); + } + UI_SET_ABSBIT => { + let value = _arg as c_uint; + debug!("fh {}: ioctl UI_SET_ABSBIT {}", fh, value); + ui_set_absbit(fd, value.into()).unwrap(); + fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0); + } + UI_SET_MSCBIT => { + let value = _arg as c_uint; + debug!("fh {}: ioctl UI_SET_MSCBIT {}", fh, value); + ui_set_mscbit(fd, value.into()).unwrap(); + fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0); + } + UI_SET_LEDBIT => { + let value = _arg as c_uint; + debug!("fh {}: ioctl UI_SET_LEDBIT {}", fh, value); + ui_set_ledbit(fd, value.into()).unwrap(); + fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0); + } + UI_SET_SNDBIT => { + let value = _arg as c_uint; + debug!("fh {}: ioctl UI_SET_SNDBIT {}", fh, value); + ui_set_sndbit(fd, value.into()).unwrap(); + fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0); + } + UI_SET_FFBIT => { + let value = _arg as c_uint; + debug!("fh {}: ioctl UI_SET_FFBIT {}", fh, value); + ui_set_ffbit(fd, value.into()).unwrap(); + fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0); + } + UI_SET_PHYS => { + assert!(_in_bufsz != 0, "should have _in_bufsz"); + debug!("fh {}: ioctl UI_SET_PHYS", fh); + // inbuf is actually a *const c_char, but + // but the macro to generate ui_set_phys expects a ptr to the actual data structure. + let phys = _in_buf as *const *const c_char; + ui_set_phys(fd, phys).unwrap(); + fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0); + } + UI_SET_SWBIT => { + let value = _arg as c_uint; + debug!("fh {}: ioctl UI_SET_SWBIT {}", fh, value); + ui_set_swbit(fd, value.into()).unwrap(); + fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0); + } + UI_SET_PROPBIT => { + let value = _arg as c_uint; + debug!("fh {}: ioctl UI_SET_PROPBIT {}", fh, value); + ui_set_propbit(fd, value.into()).unwrap(); + fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0); + } + UI_BEGIN_FF_UPLOAD => { + assert!(_in_bufsz != 0, "should have _in_bufsz"); + debug!("fh {}: ioctl UI_BEGIN_FF_UPLOAD", fh); + let ff_upload_ptr = _in_buf as *mut uinput_ff_upload; + debug!("request_id: {:x}", (*ff_upload_ptr).request_id); + ui_begin_ff_upload(fd, ff_upload_ptr).unwrap(); + fuse_lowlevel::fuse_reply_ioctl(_req, 0, ff_upload_ptr as *mut c_void, _out_bufsz); + } + UI_END_FF_UPLOAD => { + assert!(_in_bufsz != 0, "should have _in_bufsz"); + debug!("fh {}: ioctl UI_END_FF_UPLOAD", fh); + let ff_upload_ptr = _in_buf as *const uinput_ff_upload; + debug!("request_id: {:x}", (*ff_upload_ptr).request_id); + ui_end_ff_upload(fd, ff_upload_ptr).unwrap(); + fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0); + } + UI_BEGIN_FF_ERASE => { + assert!(_in_bufsz != 0, "should have _in_bufsz"); + debug!("fh {}: ioctl UI_BEGIN_FF_ERASE", fh); + let ff_erase_ptr = _in_buf as *mut uinput_ff_erase; + debug!("request_id: {:x}", (*ff_erase_ptr).request_id); + ui_begin_ff_erase(fd, ff_erase_ptr).unwrap(); + fuse_lowlevel::fuse_reply_ioctl(_req, 0, ff_erase_ptr as *mut c_void, _out_bufsz); + } + UI_END_FF_ERASE => { + assert!(_in_bufsz != 0, "should have _in_bufsz"); + debug!("fh {}: ioctl UI_END_FF_ERASE", fh); + let ff_erase_ptr = _in_buf as *const uinput_ff_erase; + debug!("request_id: {:x}", (*ff_erase_ptr).request_id); + ui_end_ff_erase(fd, ff_erase_ptr).unwrap(); + fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0); + } + _ => { + debug!("fh {}: ioctl cmd {}", fh, _cmd); + fuse_lowlevel::fuse_reply_err(_req, EBADRQC); + } + } +} + +// Instance of cuse_lowlevel_ops with all stubs assigned. +// Setting to None leads to e.g. "write error: Function not implemented". +// You can find the implementations of the uinput default (open, release ,read, write, poll, +// and ioctl) in uinput_fops of uinput.c. +// See: https://github.com/torvalds/linux/blob/master/drivers/input/misc/uinput.c, +pub fn vuinput_make_cuse_ops() -> cuse_lowlevel::cuse_lowlevel_ops { + cuse_lowlevel::cuse_lowlevel_ops { + init: None, + init_done: None, + destroy: None, + open: Some(vuinput_open), + read: None, + write: Some(vuinput_write), + flush: None, + release: Some(vuinput_release), + fsync: None, + ioctl: Some(vuinput_ioctl), + poll: None, + } +} + + + +fn main() -> std::io::Result<()> { + + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init(); + + let args: Vec = std::env::args().collect(); + + VUINPUT_STATE.set(RwLock::new(HashMap::new())).unwrap(); + VUINPUT_COUNTER.set(AtomicU64::new(3)).unwrap(); + JOB_DISPATCHER.set(Mutex::new(Dispatcher::new())).unwrap(); + SELF_NAMESPACES.set(get_namespaces(None)).unwrap(); + JOB_DISPATCHER.get().unwrap().lock().unwrap().dispatch(Box::new(MonitorBackgroundLoop::new())); + + info!("Starting vuinputd"); + + let cuse_ops = vuinput_make_cuse_ops(); + + let vuinput_devicename = CString::new(format!("DEVNAME=vuinput")).unwrap(); + + let mut dev_info_argv: Vec<*const c_char> = vec![ + vuinput_devicename.as_ptr(), // pointer to the C string + std::ptr::null(), // null terminator, often required by C APIs + ]; + + // setting dev_major and dev_minor to 0 leads to a dynamic assignment of the major and minor, very likely beginning with 234:0 + // see in https://www.kernel.org/doc/Documentation/admin-guide/devices.txt + // major 120 is reserved for local/experimental use. I picked minor 414795 with the use + // of a random number generator to omit conflicts. + let ci = cuse_lowlevel::cuse_info { + dev_major: 120, + dev_minor: 414795, + dev_info_argc: 1, + dev_info_argv: dev_info_argv.as_mut_ptr(), + flags: cuse_lowlevel::CUSE_UNRESTRICTED_IOCTL, + }; + + let arg_program_name = CString::new(args[0].clone()).unwrap(); + let parg_program_name = arg_program_name.into_raw(); + let arg_foreground = CString::new("-f").unwrap(); + let parg_foreground = arg_foreground.into_raw(); + let arg_singlethreaded = CString::new("-s").unwrap(); + let parg_singlethreaded = arg_singlethreaded.into_raw(); + let mut stripped_argv: Vec<*mut c_char> = vec![ + parg_program_name, + parg_foreground, + parg_singlethreaded, + std::ptr::null_mut(), // null terminator, often required by C APIs + ]; + + unsafe { + cuse_lowlevel::cuse_lowlevel_main( + 3, + stripped_argv.as_mut_ptr(), + &ci, + &cuse_ops, + std::ptr::null_mut(), + ); + let _reclaim_arg_program_name = CString::from_raw(parg_program_name); + let _reclaim_arg_foreground = CString::from_raw(parg_foreground); + let _reclaim_arg_foreground = CString::from_raw(parg_singlethreaded); + } + info!("Stopping vuinputd"); + JOB_DISPATCHER.get().unwrap().lock().unwrap().close(); + JOB_DISPATCHER.get().unwrap().lock().unwrap().wait_until_finished(); + + Ok(()) +} diff --git a/vuinputd/src/monitor_udev.rs b/vuinputd/src/monitor_udev.rs new file mode 100644 index 0000000..0d648b0 --- /dev/null +++ b/vuinputd/src/monitor_udev.rs @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +use std::{ + collections::HashMap, + future::Future, + os::fd::{AsFd, AsRawFd, BorrowedFd, RawFd}, + pin::Pin, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, OnceLock, + }, + time::{Duration, Instant}, +}; + +use async_io::Async; +use libudev::Monitor; +use log::debug; +use regex::Regex; + +use crate::jobs::job::{Job, JobTarget}; + +// === Basic types === + +#[derive(Debug, Clone)] +pub enum EventKind { + Add, + Remove, +} + +#[derive(Debug, Clone)] +pub struct UdevEvent { + pub syspath: String, + pub seqnum: u64, + pub kind: EventKind, + pub payload: HashMap, +} + +#[derive(Debug, Clone)] +pub struct Entry { + pub syspath: String, + pub seqnum: u64, + pub add_data: Option>, + pub remove_data: Option>, + pub add_processed: bool, + pub tombstone: bool, + pub last_update: Instant, +} + +// === EventStore === + +#[derive(Debug)] +pub struct EventStore { + entries: HashMap, + ttl: Duration, +} + +impl EventStore { + pub fn new(ttl: Duration) -> Self { + Self { + entries: HashMap::new(), + ttl, + } + } + + pub fn on_event(&mut self, event: UdevEvent) { + let now = Instant::now(); + let e = self + .entries + .entry(event.syspath.clone()) + .or_insert_with(|| Entry { + syspath: event.syspath.clone(), + seqnum: event.seqnum, + add_data: None, + remove_data: None, + add_processed: false, + tombstone: false, + last_update: now, + }); + + e.seqnum = event.seqnum; + e.last_update = now; + e.tombstone = false; + + match event.kind { + EventKind::Add => { + e.add_data = Some(event.payload); + e.add_processed = false; + e.remove_data = None; + } + EventKind::Remove => { + e.remove_data = Some(event.payload); + } + } + } + + pub fn take(&mut self, syspath: &str) -> Option { + let e = self.entries.get_mut(syspath)?; + + let result = e.clone(); + + if e.tombstone { + return Some(result); + } + + if !e.add_processed { + e.add_processed = true; + } + if e.remove_data.is_some() { + e.tombstone = true; + } + + Some(result) + } + + pub fn cleanup(&mut self) { + let now = Instant::now(); + self.entries.retain(|_, e| { + if e.tombstone { + return false; + } + now.duration_since(e.last_update) < self.ttl + }); + } +} + +// === Global store === + +pub static EVENT_STORE: OnceLock>> = OnceLock::new(); + +pub struct MonitorBackgroundLoop { +} +impl MonitorBackgroundLoop { + pub fn new() -> Self { + Self { + } + } +} + +impl Job for MonitorBackgroundLoop { + fn desc(&self) -> &str { + "Monitor udev events" + } + + fn execute_after_cancellation(&self) -> bool { + false + } + fn create_task(self: &MonitorBackgroundLoop) -> Pin>> { + let cancel_token: Arc = Arc::new(AtomicBool::new(false)); + Box::pin(udev_monitor_loop(cancel_token)) + } + + fn job_target(&self) -> JobTarget { + JobTarget::BackgroundLoop + } +} + +pub async fn udev_monitor_loop(cancel_token: Arc) { + // Clone a reference to the shared store which should already be initialized in main. + + // Initialize shared store + let store = Arc::new(Mutex::new(EventStore::new(Duration::from_secs(60)))); + EVENT_STORE.set(store.clone()).unwrap(); + + // Create monitor that listens for kernel events. + // Use match_subsystem to filter for "input" subsystem as requested. + debug!("Monitor started"); + let mut next_cleanup = Instant::now() + Duration::from_secs(60); + + let context = libudev::Context::new().unwrap(); + let mut monitor = Monitor::new(&context).unwrap(); + monitor.match_subsystem("input").unwrap(); + let mut monitor_socket = monitor.listen().expect("Failed to create udev monitor"); + + // Wrap the monitor in a small AsFd adapter + struct FdWrap(RawFd); + impl AsRawFd for FdWrap { + fn as_raw_fd(&self) -> RawFd { + self.0 + } + } + impl AsFd for FdWrap { + fn as_fd(&self) -> BorrowedFd<'_> { + // SAFETY: FdWrap owns the fd and lives long enough + unsafe { BorrowedFd::borrow_raw(self.0) } + } + } + + let async_monitor = Async::new(FdWrap(monitor_socket.as_raw_fd())).unwrap(); + + let re = Regex::new(r"^/devices/virtual/input/input(\d+)/event(\d+)$").unwrap(); + + loop { + // check cancel token first + if cancel_token.load(Ordering::Relaxed) { + debug!("Cancellation requested, shutting down udev monitor thread."); + break; + } + debug!("Waiting for event"); + async_monitor.readable().await.unwrap(); + debug!("Event registered"); + + if let Some(event) = monitor_socket.receive_event() { + let mut properties: HashMap<_, _> = HashMap::new(); + for property in event.properties() { + let key: String = property.name().to_str().unwrap().to_string(); + let key = match key.as_str() { + "ID_VUINPUT_KEYBOARD" => "ID_INPUT_KEYBOARD".to_string(), + "ID_VUINPUT_MOUSE" => "ID_INPUT_MOUSE".to_string(), + _ => key + }; + + let value: String = property.value().to_str().unwrap().to_string(); + if key!="ID_SEAT" { + properties.insert(key, value); + } + } + + + + let value_of_devpath = properties.get("DEVPATH").unwrap(); + + if let Some(caps) = re.captures(value_of_devpath) { + // result is something like /devices/virtual/input/input126/event9 + // println!("devpath {}",value_of_devpath); + let input_number: u32 = caps[1].parse().unwrap(); + // let event_number: u32 = caps[2].parse().unwrap(); + let syspath = format!("/sys/devices/virtual/input/input{}", input_number); + let seqnum: u64 = properties.get("SEQNUM").unwrap().parse().unwrap(); + let kind = match properties.get("ACTION").unwrap().as_str() { + "ADD" => EventKind::Add, + "REMOVE" => EventKind::Remove, + _ => EventKind::Add, + }; + + let mut event_store = EVENT_STORE.get().unwrap().lock().unwrap(); + let udev_event = UdevEvent { + syspath: syspath, + seqnum: seqnum, + kind: kind, + payload: properties, + }; + event_store.on_event(udev_event); + } + } + + if Instant::now() > next_cleanup { + next_cleanup = Instant::now() + Duration::from_secs(60); + EVENT_STORE.get().unwrap().lock().unwrap().cleanup(); + } + } // loop + + debug!("udev monitor thread exiting."); +} + +// === Example threads === +/* +fn producer_thread(stop: Arc) { + let store = EVENT_STORE.get().unwrap().clone(); + let mut seq = 0u64; + + while !stop.load(Ordering::Relaxed) { + // Simulate some udev events + let syspath = if seq % 2 == 0 { + "/devices/input/event9" + } else { + "/devices/input/event10" + }; + + let kind = if seq % 3 == 0 { + EventKind::Remove + } else { + EventKind::Add + }; + + let event = UdevEvent { + syspath: syspath.to_string(), + seqnum: seq, + kind, + payload: format!("payload for seq {seq}"), + }; + + { + let mut guard = store.lock().unwrap(); + guard.on_event(event); + } + + seq += 1; + thread::sleep(Duration::from_millis(400)); + } + + println!("[producer] exiting"); +} + +fn consumer_thread(stop: Arc) { + let store = EVENT_STORE.get().unwrap().clone(); + + while !stop.load(Ordering::Relaxed) { + { + let mut guard = store.lock().unwrap(); + + // Example: iterate over all known syspaths + let syspaths: Vec = guard.entries.keys().cloned().collect(); + for syspath in syspaths { + if let Some(entry) = guard.take(&syspath) { + println!("[consumer] Got actionable event: {:?}", entry); + } + } + + guard.cleanup(); + } + + thread::sleep(Duration::from_millis(200)); + } + + println!("[consumer] exiting"); +} + +fn main() { + // Initialize shared store + let store = Arc::new(Mutex::new(EventStore::new(Duration::from_secs(60)))); + EVENT_STORE.set(store.clone()).unwrap(); + + // Shared stop flag + let stop_flag = Arc::new(AtomicBool::new(false)); + + let p_stop = stop_flag.clone(); + let producer = thread::spawn(move || producer_thread(p_stop)); + + let c_stop = stop_flag.clone(); + let consumer = thread::spawn(move || consumer_thread(c_stop)); + + // Let it run for 5 seconds + thread::sleep(Duration::from_secs(5)); + stop_flag.store(true, Ordering::Relaxed); + + producer.join().unwrap(); + consumer.join().unwrap(); + + println!("Main exiting"); +} + */ diff --git a/vuinputd/src/namespace.rs b/vuinputd/src/namespace.rs new file mode 100644 index 0000000..bbc510d --- /dev/null +++ b/vuinputd/src/namespace.rs @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +use nix::{ + sched::{setns, CloneFlags}, + unistd::{fork, ForkResult, Pid}, +}; +use std::{ + fs::{self, File}, + os::fd::AsFd, +}; + +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct Namespaces { + pub nspath: String, + pub net: Option, + pub uts: Option, + pub ipc: Option, + pub pid: Option, + pub pid_for_children: Option, + pub user: Option, + pub mnt: Option, + pub cgroup: Option, + pub time: Option, + pub time_for_children: Option, +} + +impl Namespaces { + pub fn equal_mnt_and_net(&self, other: &Namespaces) -> bool { + self.mnt == other.mnt && self.net == other.net + } +} + +impl std::fmt::Display for Namespaces { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Namespaces:")?; + writeln!(f, " net: {:?}", self.net)?; + writeln!(f, " uts: {:?}", self.uts)?; + writeln!(f, " ipc: {:?}", self.ipc)?; + writeln!(f, " pid: {:?}", self.pid)?; + writeln!(f, " pid_for_children: {:?}", self.pid_for_children)?; + writeln!(f, " user: {:?}", self.user)?; + writeln!(f, " mnt: {:?}", self.mnt)?; + writeln!(f, " cgroup: {:?}", self.cgroup)?; + writeln!(f, " time: {:?}", self.time)?; + writeln!(f, " time_for_children: {:?}", self.time_for_children)?; + Ok(()) + } +} + +pub fn get_namespaces(pid: Option) -> Namespaces { + let pid: String = match pid { + Some(pid) => pid.to_string(), + None => "self".to_string(), + }; + let nspath = format!("/proc/{}/ns", pid); + + let mut ns = Namespaces { + nspath: nspath.clone(), + net: None, + uts: None, + ipc: None, + pid: None, + pid_for_children: None, + user: None, + mnt: None, + cgroup: None, + time: None, + time_for_children: None, + }; + + for entry in fs::read_dir(&nspath).expect("proc not found") { + let entry = entry.expect("`msg`"); + let link = fs::read_link(entry.path()).expect("problem parsing inode"); + let link_str = link.to_string_lossy(); + if let (Some(start), Some(end)) = (link_str.find('['), link_str.find(']')) { + if let Ok(inode) = link_str[start + 1..end].parse::() { + match entry.file_name().into_string().unwrap_or_default().as_str() { + "net" => ns.net = Some(inode), + "uts" => ns.uts = Some(inode), + "ipc" => ns.ipc = Some(inode), + "pid" => ns.pid = Some(inode), + "pid_for_children" => ns.pid_for_children = Some(inode), + "user" => ns.user = Some(inode), + "mnt" => ns.mnt = Some(inode), + "cgroup" => ns.cgroup = Some(inode), + "time" => ns.time = Some(inode), + "time_for_children" => ns.time_for_children = Some(inode), + _ => (), + } + } + } + } + ns +} + +/// Runs a function inside the given network and mount namespaces. +/// Returns the child PID so the caller can `waitpid` on it. +pub fn run_in_net_and_mnt_namespace(ns: Namespaces, func: Box) -> nix::Result { + //Note: The child process is created with a single thread—the one that called fork(). + match unsafe { fork()? } { + ForkResult::Parent { child } => { + // Parent: return the PID of the child + Ok(child) + } + ForkResult::Child => { + // enter namespace + let net = File::open(ns.nspath.clone() + "/net").expect("net not found"); + let mnt = File::open(ns.nspath.clone() + "/mnt").expect("mnt not found"); + setns(net.as_fd(), CloneFlags::CLONE_NEWNET).expect("couldn't enter net"); + setns(mnt.as_fd(), CloneFlags::CLONE_NEWNS).expect("couldn't enter mnt"); + // execute your function + func(); + std::process::exit(0); + } + } +} diff --git a/vuinputd/systemd/vuinputd.service b/vuinputd/systemd/vuinputd.service new file mode 100644 index 0000000..befd251 --- /dev/null +++ b/vuinputd/systemd/vuinputd.service @@ -0,0 +1,20 @@ +[Unit] +Description=uinput proxy +After=systemd-udevd.service +Requires=systemd-udevd.service + +[Service] +ExecStart=/usr/bin/vuinputd +Restart=on-failure + +# Needs CAP_SYS_ADMIN for CUSE + /dev/uinput (I am still missing a capability for the correct working mode) +#CapabilityBoundingSet=CAP_SYS_ADMIN CAP_MKNOD CAP_DAC_OVERRIDE CAP_FOWNER + +# Allow full privileges (dangerous, but okay for debugging) +CapabilityBoundingSet=~ # remove all limits +DeviceAllow=/dev/uinput rw +DeviceAllow=/dev/cuse rw + + +[Install] +WantedBy=multi-user.target diff --git a/vuinputd/udev/90-vuinputd-protect.rules b/vuinputd/udev/90-vuinputd-protect.rules new file mode 100644 index 0000000..c61d30d --- /dev/null +++ b/vuinputd/udev/90-vuinputd-protect.rules @@ -0,0 +1,47 @@ +# =========================================================== +# Default permissions for /dev/vuinput +# ----------------------------------------------------------- +# Rule details: +# For now, everyone can use it. + +SUBSYSTEM=="cuse", KERNEL=="vuinput", MODE="0666" + +# =========================================================== +# Cleanup rule for our virtual keyboards +# ----------------------------------------------------------- +# Purpose: +# The builtin input_id sets ID_INPUT_KEYBOARD=1 by default. +# We want our virtual keyboards to be treated differently, +# so we clear the default keyboard flag. +# +# Rule details: +# - SUBSYSTEM=="input" -> matches input devices +# - KERNEL=="event*" -> matches event nodes +# - ENV{ID_VUINPUT}=="1" -> only affects our virtual keyboards +# - ENV{ID_INPUT_KEYBOARD}="" -> clears the default keyboard flag +# - ENV{ID_SEAT}="seat_vuinput" -> assign to virtual seat for vuinput +# +# Rule ordering: +# - This runs after the hwdb entry and input_id builtin rules. +# - Ensures other keyboards are unaffected. +# +# Update procedure after editing: +# 1. sudo udevadm control --reload +# 2. sudo udevadm trigger -s input +# To check seat status: +# loginctl seat-status +# +# Note: +# - Quote from logind: Seats are identified by seat names, which are +# strings (<= 255 characters), that start with the four characters "seat" +# followed by at least one character from the range [a-zA-Z0-9], "_" and "-". +# - Even though the device is listed under the seat, without a graphical device, +# or a master-of-seat-tag, the seat won't be created and won't disturb. +# - in libinput, ID_INPUT_KEY leads to EVDEV_UDEV_TAG_KEYBOARD, which means +# that a device is tagged as keyboard. We don't want that for the host system. + +SUBSYSTEMS=="input", ENV{ID_VUINPUT}=="1", ENV{ID_INPUT_KEYBOARD}=="1" \ +ENV{ID_VUINPUT_KEYBOARD}="1", ENV{ID_INPUT_KEYBOARD}="", ENV{ID_SEAT}="seat_vuinput" + +SUBSYSTEMS=="input", ENV{ID_VUINPUT}=="1", ENV{ID_INPUT_MOUSE}=="1" \ +ENV{ID_VUINPUT_MOUSE}="1", ENV{ID_INPUT_MOUSE}="", ENV{ID_SEAT}="seat_vuinput" \ No newline at end of file diff --git a/vuinputd/udev/90-vuinputd.hwdb b/vuinputd/udev/90-vuinputd.hwdb new file mode 100644 index 0000000..6992951 --- /dev/null +++ b/vuinputd/udev/90-vuinputd.hwdb @@ -0,0 +1,32 @@ +# =========================================================== +# Custom HWDB entry for BEEF:DEAD virtual devices +# ----------------------------------------------------------- +# Purpose: +# This entry targets the virtual devices created via uinput +# with vendor_id=0xBEEF and product_id=0xDEAD. +# +# Why HWDB is used: +# - uinput devices are virtual, so ATTRS{idVendor} / ATTRS{idProduct} +# may not always be reliable in udev rules due to timing/order. +# - HWDB entries are applied after the builtin input_id, ensuring +# that the modalias is available and can be matched reliably. +# - Using the modalias avoids problems with virtual sysfs attributes. +# +# Properties set: +# - ID_VUINPUT=1 -> marks the device as our virtual device +# +# Notes: +# - The first rule adds to the input device, e.g., +# /devices/virtual/input/input92. +# - The second rule adds to the event device, e.g., +# /devices/virtual/input/input92/event9 +# +# Update procedure after editing: +# 1. sudo systemd-hwdb update +# =========================================================== + +evdev:input:b0003vBEEFpDEADe0000-* + ID_VUINPUT=1 + +input:b0003vBEEFpDEADe0000-* + ID_VUINPUT=1