The host makes an X25519 sealing key for each session and states it in its screen statement; the gate decides every input by all nine rules of remote-desktop §5.3 (wai-rs's decide_opened), opening a sealed input with the session's opening key, and the host injects what it seals. The opening key stays in the session unless --opening-key writes it (mode 0600, never over a file) for a verifier. The headless viewer seals its key, text and clipboard actions to the screen's sealing key, keeps the records it receives, and follows another principal's events when told. The agent reaches the room service over https:// and the relay over wss://: rustls with ring, the system's roots and any certificate a PEM file names (--ca), every chain and name verified. tls::front is a TLS front for a plain backend. Lock files gain tokio-rustls and the new edges alone. tests/tls.rs: the service and the relay through a TLS front, trusted by a PEM file; refused under the system's roots alone, and for a certificate that names another host. tests/loopback.rs: alice's typing sealed, injected as typed, absent from what bob receives; her text in the clear refused input_unsealed; the session verified at the opened level. Co-Authored-By: Claude Opus 5.5 <[email protected]>
215 lines
8.9 KiB
Rust
215 lines
8.9 KiB
Rust
//! Input injected on Linux through the kernel's user-level input device (WAI remote-desktop
|
||
//! §5.4). allow-name: OS API names throughout this file
|
||
//!
|
||
//! One virtual device with an absolute pointer the size of the captured frame, its buttons and
|
||
//! wheels, and the keyboard keys of the HID usages this crate maps. The kernel takes no text, so
|
||
//! composed text is not injected, nor touch; clipboard text goes through the display's clipboard
|
||
//! tool when one is installed. The screen statement's `accepts` says which (§3.3).
|
||
|
||
use crate::inject::{Injector, Mapping};
|
||
use std::os::fd::{AsRawFd, OwnedFd};
|
||
use wai::remote_desktop::{accepts_bit, action_type, Action};
|
||
|
||
const EV_SYN: u16 = 0;
|
||
const EV_KEY: u16 = 1;
|
||
const EV_REL: u16 = 2;
|
||
const EV_ABS: u16 = 3;
|
||
const SYN_REPORT: u16 = 0;
|
||
const REL_HWHEEL: u16 = 6;
|
||
const REL_WHEEL: u16 = 8;
|
||
const ABS_X: u16 = 0;
|
||
const ABS_Y: u16 = 1;
|
||
const BTN: [u16; 5] = [0x110, 0x112, 0x111, 0x113, 0x114]; // primary, auxiliary, secondary, fourth, fifth
|
||
const UI_SET_EVBIT: libc::c_ulong = 0x4004_5564;
|
||
const UI_SET_KEYBIT: libc::c_ulong = 0x4004_5565;
|
||
const UI_SET_RELBIT: libc::c_ulong = 0x4004_5566;
|
||
const UI_SET_ABSBIT: libc::c_ulong = 0x4004_5567;
|
||
const UI_DEV_CREATE: libc::c_ulong = 0x5501;
|
||
const UI_DEV_DESTROY: libc::c_ulong = 0x5502;
|
||
|
||
/// The kernel's key code of each HID usage of the keyboard page.
|
||
const KEYS: &[(u16, u16)] = &[
|
||
(0x04, 30), (0x05, 48), (0x06, 46), (0x07, 32), (0x08, 18), (0x09, 33), (0x0A, 34), (0x0B, 35), (0x0C, 23),
|
||
(0x0D, 36), (0x0E, 37), (0x0F, 38), (0x10, 50), (0x11, 49), (0x12, 24), (0x13, 25), (0x14, 16), (0x15, 19),
|
||
(0x16, 31), (0x17, 20), (0x18, 22), (0x19, 47), (0x1A, 17), (0x1B, 45), (0x1C, 21), (0x1D, 44), (0x1E, 2),
|
||
(0x1F, 3), (0x20, 4), (0x21, 5), (0x22, 6), (0x23, 7), (0x24, 8), (0x25, 9), (0x26, 10), (0x27, 11), (0x28, 28),
|
||
(0x29, 1), (0x2A, 14), (0x2B, 15), (0x2C, 57), (0x2D, 12), (0x2E, 13), (0x2F, 26), (0x30, 27), (0x31, 43),
|
||
(0x33, 39), (0x34, 40), (0x35, 41), (0x36, 51), (0x37, 52), (0x38, 53), (0x39, 58), (0x3A, 59), (0x3B, 60),
|
||
(0x3C, 61), (0x3D, 62), (0x3E, 63), (0x3F, 64), (0x40, 65), (0x41, 66), (0x42, 67), (0x43, 68), (0x44, 87),
|
||
(0x45, 88), (0x49, 110), (0x4A, 102), (0x4B, 104), (0x4C, 111), (0x4D, 107), (0x4E, 109), (0x4F, 106), (0x50, 105),
|
||
(0x51, 108), (0x52, 103), (0xE0, 29), (0xE1, 42), (0xE2, 56), (0xE3, 125), (0xE4, 97), (0xE5, 54), (0xE6, 100),
|
||
(0xE7, 126),
|
||
];
|
||
|
||
#[repr(C)]
|
||
struct InputId {
|
||
bustype: u16,
|
||
vendor: u16,
|
||
product: u16,
|
||
version: u16,
|
||
}
|
||
|
||
#[repr(C)]
|
||
struct UserDev {
|
||
name: [u8; 80],
|
||
id: InputId,
|
||
ff_effects_max: u32,
|
||
absmax: [i32; 64],
|
||
absmin: [i32; 64],
|
||
absfuzz: [i32; 64],
|
||
absflat: [i32; 64],
|
||
}
|
||
|
||
#[repr(C)]
|
||
struct InputEvent {
|
||
sec: libc::time_t,
|
||
usec: libc::suseconds_t,
|
||
kind: u16,
|
||
code: u16,
|
||
value: i32,
|
||
}
|
||
|
||
pub struct UinputInjector {
|
||
fd: OwnedFd,
|
||
keys_down: Vec<u16>,
|
||
buttons_down: Vec<u16>,
|
||
clipboard: Option<&'static str>,
|
||
}
|
||
|
||
fn clipboard_tool() -> Option<&'static str> {
|
||
["/usr/bin/wl-copy", "/usr/bin/xclip"].into_iter().find(|p| std::path::Path::new(p).exists())
|
||
}
|
||
|
||
impl UinputInjector {
|
||
/// A device whose pointer spans `width` × `height`.
|
||
pub fn new(width: u32, height: u32) -> Result<UinputInjector, String> {
|
||
let path = c"/dev/uinput";
|
||
let fd = unsafe { libc::open(path.as_ptr(), libc::O_WRONLY | libc::O_NONBLOCK) };
|
||
if fd < 0 {
|
||
return Err(format!("/dev/uinput could not be opened: {} (the host needs write access to it)", std::io::Error::last_os_error()));
|
||
}
|
||
let fd = unsafe { <OwnedFd as std::os::fd::FromRawFd>::from_raw_fd(fd) };
|
||
let raw = fd.as_raw_fd();
|
||
let io = |req: libc::c_ulong, v: libc::c_int| -> Result<(), String> {
|
||
if unsafe { libc::ioctl(raw, req as _, v) } < 0 { Err(format!("an input device ioctl: {}", std::io::Error::last_os_error())) } else { Ok(()) }
|
||
};
|
||
for ev in [EV_KEY, EV_REL, EV_ABS, EV_SYN] {
|
||
io(UI_SET_EVBIT, ev as i32)?;
|
||
}
|
||
for b in BTN {
|
||
io(UI_SET_KEYBIT, b as i32)?;
|
||
}
|
||
for (_, k) in KEYS {
|
||
io(UI_SET_KEYBIT, *k as i32)?;
|
||
}
|
||
io(UI_SET_RELBIT, REL_WHEEL as i32)?;
|
||
io(UI_SET_RELBIT, REL_HWHEEL as i32)?;
|
||
io(UI_SET_ABSBIT, ABS_X as i32)?;
|
||
io(UI_SET_ABSBIT, ABS_Y as i32)?;
|
||
let mut dev: UserDev = unsafe { std::mem::zeroed() };
|
||
dev.name[..15].copy_from_slice(b"wai-remote-host");
|
||
dev.id = InputId { bustype: 0x06, vendor: 0, product: 0, version: 1 };
|
||
dev.absmax[ABS_X as usize] = width.saturating_sub(1) as i32;
|
||
dev.absmax[ABS_Y as usize] = height.saturating_sub(1) as i32;
|
||
let b = unsafe { std::slice::from_raw_parts(&dev as *const UserDev as *const u8, std::mem::size_of::<UserDev>()) };
|
||
if unsafe { libc::write(raw, b.as_ptr() as *const libc::c_void, b.len()) } != b.len() as isize {
|
||
return Err("the input device could not be described".into());
|
||
}
|
||
if unsafe { libc::ioctl(raw, UI_DEV_CREATE as _) } < 0 {
|
||
return Err(format!("the input device could not be made: {}", std::io::Error::last_os_error()));
|
||
}
|
||
Ok(UinputInjector { fd, keys_down: Vec::new(), buttons_down: Vec::new(), clipboard: clipboard_tool() })
|
||
}
|
||
|
||
fn emit(&self, kind: u16, code: u16, value: i32) -> Result<(), String> {
|
||
let e = InputEvent { sec: 0, usec: 0, kind, code, value };
|
||
let n = std::mem::size_of::<InputEvent>();
|
||
if unsafe { libc::write(self.fd.as_raw_fd(), &e as *const InputEvent as *const libc::c_void, n) } != n as isize {
|
||
return Err(format!("an input event: {}", std::io::Error::last_os_error()));
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn sync(&self) -> Result<(), String> {
|
||
self.emit(EV_SYN, SYN_REPORT, 0)
|
||
}
|
||
}
|
||
|
||
impl Drop for UinputInjector {
|
||
fn drop(&mut self) {
|
||
unsafe {
|
||
libc::ioctl(self.fd.as_raw_fd(), UI_DEV_DESTROY as _);
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Injector for UinputInjector {
|
||
fn accepts(&self) -> u8 {
|
||
let mut a = accepts_bit(action_type::MOVE) | accepts_bit(action_type::BUTTON) | accepts_bit(action_type::WHEEL) | accepts_bit(action_type::KEY);
|
||
if self.clipboard.is_some() {
|
||
a |= accepts_bit(action_type::CLIPBOARD);
|
||
}
|
||
a
|
||
}
|
||
|
||
fn inject(&mut self, a: &Action, m: &Mapping) -> Result<(), String> {
|
||
if let Some((x, y)) = a.at() {
|
||
let (tx, ty) = m.map(x, y);
|
||
self.emit(EV_ABS, ABS_X, tx)?;
|
||
self.emit(EV_ABS, ABS_Y, ty)?;
|
||
}
|
||
match a {
|
||
Action::Move { .. } => {}
|
||
Action::Button { button, down, .. } => {
|
||
let code = BTN[*button as usize];
|
||
self.emit(EV_KEY, code, *down as i32)?;
|
||
if *down {
|
||
self.buttons_down.push(code);
|
||
} else {
|
||
self.buttons_down.retain(|c| *c != code);
|
||
}
|
||
}
|
||
Action::Wheel { dx, dy, .. } => {
|
||
// One detent each 120 pixels, the unit a wheel event in pixels is counted in.
|
||
if *dy != 0 {
|
||
self.emit(EV_REL, REL_WHEEL, -(*dy as i32) / 120)?;
|
||
}
|
||
if *dx != 0 {
|
||
self.emit(EV_REL, REL_HWHEEL, *dx as i32 / 120)?;
|
||
}
|
||
}
|
||
Action::Key { usage, down, .. } => {
|
||
let Some(&(_, k)) = KEYS.iter().find(|(u, _)| u == usage) else { return Ok(()) };
|
||
self.emit(EV_KEY, k, *down as i32)?;
|
||
if *down {
|
||
self.keys_down.push(k);
|
||
} else {
|
||
self.keys_down.retain(|c| *c != k);
|
||
}
|
||
}
|
||
Action::Clipboard(s) => {
|
||
use std::io::Write;
|
||
let tool = self.clipboard.ok_or("no clipboard tool")?;
|
||
let mut c = std::process::Command::new(tool);
|
||
if tool.ends_with("xclip") {
|
||
c.args(["-selection", "clipboard"]);
|
||
}
|
||
let mut c = c.stdin(std::process::Stdio::piped()).spawn().map_err(|e| format!("the clipboard tool: {e}"))?;
|
||
c.stdin.as_mut().ok_or("no pipe")?.write_all(s.as_bytes()).map_err(|e| e.to_string())?;
|
||
drop(c.stdin.take());
|
||
return c.wait().map_err(|e| e.to_string()).and_then(|st| if st.success() { Ok(()) } else { Err(format!("the clipboard tool exited {st}")) });
|
||
}
|
||
Action::Text(_) | Action::Touch { .. } => return Err("not injected on this device".into()),
|
||
// The host opens a sealed action before it injects it (WAI remote-desktop §5.5).
|
||
Action::Sealed { .. } => return Err("a sealed action, not opened".into()),
|
||
}
|
||
self.sync()
|
||
}
|
||
|
||
fn release_all(&mut self) -> Result<(), String> {
|
||
for c in std::mem::take(&mut self.buttons_down).into_iter().chain(std::mem::take(&mut self.keys_down)) {
|
||
self.emit(EV_KEY, c, 0)?;
|
||
}
|
||
self.sync()
|
||
}
|
||
}
|