Initial commit

This commit is contained in:
Johannes Leupolz 2025-10-23 22:34:33 +00:00
parent d8b2625ff1
commit 49149e2b48
23 changed files with 3022 additions and 0 deletions

9
Cargo.toml Normal file
View file

@ -0,0 +1,9 @@
[workspace]
members = [
"uinput-ioctls",
"vuinputd",
"vuinput-examples",
]
# Optional: force same version for all crates
resolver = "2"

9
uinput-ioctls/Cargo.toml Normal file
View file

@ -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

68
uinput-ioctls/src/lib.rs Normal file
View file

@ -0,0 +1,68 @@
// SPDX-License-Identifier: MIT
//
// Author: Johannes Leupolz <dev@leupolz.eu>
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::<uinput_setup>());
pub const UI_ABS_SETUP: u64 =
request_code_write!(b'U', 4, ::std::mem::size_of::<uinput_abs_setup>());
//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::<c_uint>());
pub const UI_SET_EVBIT: u64 = request_code_write!(b'U', 100, std::mem::size_of::<c_uint>());
pub const UI_SET_KEYBIT: u64 = request_code_write!(b'U', 101, std::mem::size_of::<c_uint>());
pub const UI_SET_RELBIT: u64 = request_code_write!(b'U', 102, std::mem::size_of::<c_uint>());
pub const UI_SET_ABSBIT: u64 = request_code_write!(b'U', 103, std::mem::size_of::<c_uint>());
pub const UI_SET_MSCBIT: u64 = request_code_write!(b'U', 104, std::mem::size_of::<c_uint>());
pub const UI_SET_LEDBIT: u64 = request_code_write!(b'U', 105, std::mem::size_of::<c_uint>());
pub const UI_SET_SNDBIT: u64 = request_code_write!(b'U', 106, std::mem::size_of::<c_uint>());
pub const UI_SET_FFBIT: u64 = request_code_write!(b'U', 107, std::mem::size_of::<c_uint>());
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::<c_uint>());
pub const UI_SET_PROPBIT: u64 = request_code_write!(b'U', 110, std::mem::size_of::<c_uint>());
pub const UI_BEGIN_FF_UPLOAD: u64 =
request_code_readwrite!(b'U', 200, ::std::mem::size_of::<uinput_ff_upload>());
pub const UI_END_FF_UPLOAD: u64 =
request_code_write!(b'U', 201, ::std::mem::size_of::<uinput_ff_upload>());
pub const UI_BEGIN_FF_ERASE: u64 =
request_code_readwrite!(b'U', 202, ::std::mem::size_of::<uinput_ff_erase>());
pub const UI_END_FF_ERASE: u64 =
request_code_write!(b'U', 203, ::std::mem::size_of::<uinput_ff_erase>());
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);

View file

@ -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

View file

@ -0,0 +1,399 @@
// SPDX-License-Identifier: MIT
//
// Author: Johannes Leupolz <dev@leupolz.eu>
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::<libc::input_event>();
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(())
}

View file

@ -0,0 +1,190 @@
// SPDX-License-Identifier: MIT
//
// Author: Johannes Leupolz <dev@leupolz.eu>
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::<libc::input_event>();
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<String> = 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(())
}

28
vuinputd/Cargo.toml Normal file
View file

@ -0,0 +1,28 @@
[package]
name = "vuinputd"
version = "0.1.0"
edition = "2021"
authors = ["Johannes Leupolz <dev@leupolz.eu>"]
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"

View file

@ -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<dyn Future<Output = ()>>> {
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<HashMap<String,String>> = None;
let mut runtime_data: Option<String> = 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();
}
}

View file

@ -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<dyn Error>> {
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);
}
}
*/

View file

@ -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;

View file

@ -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::<MonitorNetlinkHeader>() as u32,
properties_off: mem::size_of::<MonitorNetlinkHeader>() 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<u8> {
// 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::<MonitorNetlinkHeader>()).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<OwnedFd, String> {
// 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::<MonitorNetlinkHeader>() > MAX_NETLINK_PAYLOAD {
return Err(format!(
"Total payload too large: {} bytes (max {})",
payload.len() + mem::size_of::<MonitorNetlinkHeader>(),
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<String, String>) {
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<u8> = 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:
*/

View file

@ -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<dyn Future<Output = ()>>> {
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();
}
}

View file

@ -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<major>:<minor>`
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<String> {
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);
}
}

View file

@ -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<dyn Fn(JobTarget) -> Pin<Box<dyn Future<Output = ()>>> + Send + 'static>,
}
impl ClosureJob {
pub fn new(
desc: impl Into<String>,
target: JobTarget,
execute_after_cancellation: bool,
f: Box<
dyn Fn(JobTarget) -> Pin<Box<dyn Future<Output = ()>>> // 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<Box<dyn Future<Output = ()>>> {
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();
}

203
vuinputd/src/jobs/job.rs Normal file
View file

@ -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<Box<dyn Future<Output = ()>>>;
}
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<JoinHandle<()>>,
tx: Option<Sender<Box<dyn Job>>>,
future_handles: Arc<Mutex<Vec<RemoteHandle<()>>>>,
}
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<Mutex<HashMap<JobTarget, Sender<Box<dyn Job>>>>> =
Arc::new(Mutex::new(HashMap::new()));
let rx_in_thread: Receiver<Box<dyn Job>> = rx.clone();
let future_handles: Arc<Mutex<Vec<RemoteHandle<()>>>> = 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<dyn Job>) {
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<Mutex<HashMap<JobTarget, Sender<Box<dyn Job>>>>>,
rx: Receiver<Box<dyn Job>>,
future_handles: Arc<Mutex<Vec<RemoteHandle<()>>>>,
) {
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<Mutex<HashMap<JobTarget, Sender<Box<dyn Job>>>>>,
target: JobTarget,
future_handles: Arc<Mutex<Vec<RemoteHandle<()>>>>,
) -> (Sender<Box<dyn Job>>, 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<Box<dyn Job>>) {
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)* }))
};
}
*/

View file

@ -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<J: Job> {
inner: J,
timeout: Option<Duration>,
cancel_token: Option<Arc<AtomicBool>>,
execute_despite_cancellation: bool,
log: bool,
}
impl<J: Job> JobBuilder<J> {
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<AtomicBool>) -> 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<J> {
WrappedJob {
inner: self.inner,
timeout: self.timeout,
cancel_token: self.cancel_token,
log: self.log,
}
}
}
pub struct WrappedJob<J: Job> {
inner: J,
timeout: Option<Duration>,
cancel_token: Option<Arc<AtomicBool>>,
log: bool,
}
impl<J: Job> Job for WrappedJob<J> {
fn desc(&self) -> &str {
self.inner.desc()
}
fn create_task(self: Box<Self>) -> Pin<Box<dyn Future<Output = ()> + 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}");
}
})
}
}
*/

35
vuinputd/src/jobs/mod.rs Normal file
View file

@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
//
// Author: Johannes Leupolz <dev@leupolz.eu>
//! # 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<dyn FnOnce(JobTarget) -> Pin<Box<dyn Future<Output = ()>>> + 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;

727
vuinputd/src/main.rs Normal file
View file

@ -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 <dev@leupolz.eu>
// 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<String>,
netlink_data: Option<String>
}
#[derive(Debug)]
struct VuInputState {
file: File,
ns_of_requestor: Namespaces,
input_device: Option<VuInputDevice>
}
#[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<AtomicU64> = OnceLock::new();
static VUINPUT_STATE: OnceLock<RwLock<HashMap<VuFileHandle, Arc<Mutex<VuInputState>>>>> = OnceLock::new();
static JOB_DISPATCHER: OnceLock<Mutex<Dispatcher>>= OnceLock::new();
static SELF_NAMESPACES: OnceLock<Namespaces>= OnceLock::new();
const SYS_INPUT_DIR: &str = "/sys/devices/virtual/input/";
fn get_vuinput_state(
fh:&VuFileHandle,
) -> Result<Arc<Mutex<VuInputState>>, 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<Arc<Mutex<VuInputState>>, 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<String> {
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<File>`
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::<uinput_abs_setup>(),
};
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::<c_uint>(),
};
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::<uinput_setup>(),
};
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::<c_char>() * 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::<uinput_ff_upload>(),
};
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::<uinput_ff_upload>(),
};
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::<uinput_ff_erase>(),
};
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::<uinput_ff_erase>(),
};
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::<c_uint>(),
);
}
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<String> = 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(())
}

View file

@ -0,0 +1,343 @@
// SPDX-License-Identifier: MIT
//
// Author: Johannes Leupolz <dev@leupolz.eu>
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<String, String>,
}
#[derive(Debug, Clone)]
pub struct Entry {
pub syspath: String,
pub seqnum: u64,
pub add_data: Option<HashMap<String, String>>,
pub remove_data: Option<HashMap<String, String>>,
pub add_processed: bool,
pub tombstone: bool,
pub last_update: Instant,
}
// === EventStore ===
#[derive(Debug)]
pub struct EventStore {
entries: HashMap<String, Entry>,
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<Entry> {
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<Arc<Mutex<EventStore>>> = 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<Box<dyn Future<Output = ()>>> {
let cancel_token: Arc<AtomicBool> = 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<AtomicBool>) {
// 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<AtomicBool>) {
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<AtomicBool>) {
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<String> = 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");
}
*/

118
vuinputd/src/namespace.rs Normal file
View file

@ -0,0 +1,118 @@
// SPDX-License-Identifier: MIT
//
// Author: Johannes Leupolz <dev@leupolz.eu>
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<u64>,
pub uts: Option<u64>,
pub ipc: Option<u64>,
pub pid: Option<u64>,
pub pid_for_children: Option<u64>,
pub user: Option<u64>,
pub mnt: Option<u64>,
pub cgroup: Option<u64>,
pub time: Option<u64>,
pub time_for_children: Option<u64>,
}
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<i32>) -> 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::<u64>() {
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<dyn Fn()>) -> nix::Result<Pid> {
//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);
}
}
}

View file

@ -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

View file

@ -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"

View file

@ -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