From 821d1d24d9ec390b3ab29bf5e965613a99c4d072 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Thu, 20 Aug 2026 13:24:30 -0600 Subject: [PATCH] feat: configurable allow-list for dbus interfaces --- Cargo.lock | 1 + Cargo.toml | 1 + Makefile | 4 +- data/ei-clients.toml.example | 48 +++++ src/dbus/a11y_keyboard_monitor.rs | 32 +-- src/dbus/client_allow_list.rs | 329 ++++++++++++++++++++++++++++++ src/dbus/ei.rs | 54 +++-- src/dbus/mod.rs | 19 +- src/dbus/name_owners.rs | 32 ++- src/libei.rs | 8 +- 10 files changed, 489 insertions(+), 39 deletions(-) create mode 100644 data/ei-clients.toml.example create mode 100644 src/dbus/client_allow_list.rs diff --git a/Cargo.lock b/Cargo.lock index f2364e9eb..bf43e3f6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -828,6 +828,7 @@ dependencies = [ "smithay-egui", "thiserror 2.0.18", "tiny-skia", + "toml 0.9.12+spec-1.1.0", "tracing", "tracing-journald", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index 7d4ff3af4..06e8ae577 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ serde_json = "1" thiserror = "2.0.18" jiff = "0.2" tiny-skia = "0.11" +toml = { version = "0.9", default-features = false, features = ["parse", "serde"] } tracing = { version = "0.1.44", features = [ "max_level_debug", "release_max_level_info", diff --git a/Makefile b/Makefile index 16f59ee67..6a03911c3 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ TARGET_BIN="$(DESTDIR)$(bindir)/$(BINARY)" KEYBINDINGS_CONF="$(DESTDIR)$(sharedir)/cosmic/com.system76.CosmicSettings.Shortcuts/v1/defaults" TILING_EXCEPTIONS_CONF="$(DESTDIR)$(sharedir)/cosmic/com.system76.CosmicSettings.WindowRules/v1/tiling_exception_defaults" +EI_CLIENTS_EXAMPLE="$(DESTDIR)$(sharedir)/doc/cosmic-comp/ei-clients.toml.example" all: extract-vendor cargo build $(ARGS) @@ -52,6 +53,7 @@ install: install -Dm0755 "$(CARGO_TARGET_DIR)/$(TARGET)/$(BINARY)" "$(TARGET_BIN)" install -Dm0644 "data/keybindings.ron" "$(KEYBINDINGS_CONF)" install -Dm0644 "data/tiling-exceptions.ron" "$(TILING_EXCEPTIONS_CONF)" + install -Dm0644 "data/ei-clients.toml.example" "$(EI_CLIENTS_EXAMPLE)" install-bare-session: install install -Dm0644 "data/cosmic.desktop" "$(DESTDIR)$(sharedir)/wayland-sessions/cosmic.desktop" @@ -61,7 +63,7 @@ install-bare-session: install install -Dm0755 "data/cosmic-service" "$(DESTDIR)/$(bindir)/cosmic-service" uninstall: - rm "$(TARGET_BIN)" "$(KEYBINDINGS_CONF)" + rm "$(TARGET_BIN)" "$(KEYBINDINGS_CONF)" "$(EI_CLIENTS_EXAMPLE)" uninstall-bare-session: rm "$(DESTDIR)$(sharedir)/wayland-sessions/cosmic.desktop" diff --git a/data/ei-clients.toml.example b/data/ei-clients.toml.example new file mode 100644 index 000000000..cd7f69df4 --- /dev/null +++ b/data/ei-clients.toml.example @@ -0,0 +1,48 @@ +# Additional clients allowed on the compositor's D-Bus interfaces: +# +# [[ei.client]] "com.system76.CosmicComp.Ei" open an EI +# (libei) sender socket, i.e. synthesize +# keyboard, pointer and touch input. +# [[a11y_keyboard_monitor.client]] "org.freedesktop.a11y.KeyboardMonitor" +# observe and grab keys. Grant this only to +# assistive tech you trust with every +# keystroke typed in the session, passwords +# included. +# +# Copy this file to a drop-in directory to activate it. +# Use `install -m0644`. The compositor ignores a file left group-writable. +# The compositor reads two drop-in directories, in this order: +# +# /usr/share/cosmic-comp/ei-clients.d/*.toml # for packages +# /etc/cosmic-comp/ei-clients.d/*.toml # for local administration +# +# It applies the files in order of their file name across both directories. A +# file in /etc replaces a file of the same name under /usr/share, so you can mask +# a grant shipped by a package with an empty file of that name in /etc. +# +# The compositor picks up changes at start-up only. +# Log out and back in after editing. +# +# The compositor always allows its own built-in clients, which need no entry +# here: the XDG desktop portal implementation and the on-screen keyboard for +# [ei], Orca for [a11y_keyboard_monitor]. Entries here add access. They cannot +# narrow or remove a built-in. + +# The compositor identifies a client by a well-known bus name it owns on the +# session bus. The client must request that name before calling GetSenderSocket. +[[ei.client]] +bus_name = "org.example.MyRemoteTool" + +# `device_types` limits what the client may ask for. Omit it and the client may +# request every type. Valid values are "keyboard", "pointer" and "touchscreen". +# +# The client below can move the pointer, but cannot inject keystrokes. +[[ei.client]] +bus_name = "com.example.Kiosk" +device_types = ["pointer"] + +# You name a keyboard monitor the same way. There is no `device_types` here. +# The interface carries no per-device policy, so a client watches every key +# or nothing. +[[a11y_keyboard_monitor.client]] +bus_name = "org.example.MyScreenReader" diff --git a/src/dbus/a11y_keyboard_monitor.rs b/src/dbus/a11y_keyboard_monitor.rs index c97aa5901..61ba00873 100644 --- a/src/dbus/a11y_keyboard_monitor.rs +++ b/src/dbus/a11y_keyboard_monitor.rs @@ -10,17 +10,9 @@ use std::{ }; use tracing::debug; use xkbcommon::xkb::Keysym; -use zbus::{ - message::Header, - names::{UniqueName, WellKnownName}, - object_server::SignalEmitter, -}; - -use super::name_owners::NameOwners; +use zbus::{message::Header, names::UniqueName, object_server::SignalEmitter}; -static ALLOWED_NAMES: &[WellKnownName] = &[WellKnownName::from_static_str_unchecked( - "org.gnome.Orca.KeyboardMonitor", -)]; +use super::{client_allow_list::NameAllowList, name_owners::NameOwners}; // As defined in at-spi2-core const ATSPI_DEVICE_A11Y_MANAGER_VIRTUAL_MOD_START: u32 = 15; @@ -76,6 +68,7 @@ pub struct A11yKeyboardMonitorState { active_virtual_mods: HashSet, conn: zbus::Connection, name_owners: NameOwners, + allow_list: NameAllowList, } impl A11yKeyboardMonitorState { @@ -83,12 +76,14 @@ impl A11yKeyboardMonitorState { conn: &zbus::Connection, name_owners: &NameOwners, executor: &calloop::futures::Scheduler<()>, + allow_list: NameAllowList, ) -> zbus::Result { let clients = Arc::new(Mutex::new(Clients::default())); let keyboard_monitor = KeyboardMonitor { clients: clients.clone(), name_owners: name_owners.clone(), + allow_list: allow_list.clone(), }; conn.object_server() .at("/org/freedesktop/a11y/Manager", keyboard_monitor) @@ -101,6 +96,7 @@ impl A11yKeyboardMonitorState { active_virtual_mods: HashSet::new(), conn: conn.clone(), name_owners: name_owners.clone(), + allow_list, }) } @@ -188,22 +184,26 @@ impl A11yKeyboardMonitorState { pub fn refresh(&mut self) { // Remove clients and associated grabs when unique names are no longer // present on bus, or no longer hold approved name on bus. - self.clients - .lock() - .unwrap() - .0 - .retain(|k, _| self.name_owners.check_owner_no_poll(k, ALLOWED_NAMES)) + self.clients.lock().unwrap().0.retain(|k, _| { + self.name_owners + .check_owner_no_poll(k, self.allow_list.names()) + }) } } struct KeyboardMonitor { clients: Arc>, name_owners: NameOwners, + allow_list: NameAllowList, } impl KeyboardMonitor { async fn check_sender_allowed(&self, sender: &UniqueName<'_>) -> zbus::fdo::Result<()> { - if self.name_owners.check_owner(sender, ALLOWED_NAMES).await { + if self + .name_owners + .check_owner(sender, self.allow_list.names()) + .await + { Ok(()) } else { Err(zbus::fdo::Error::AccessDenied("Access denied".to_string())) diff --git a/src/dbus/client_allow_list.rs b/src/dbus/client_allow_list.rs new file mode 100644 index 000000000..9ee8ca6c9 --- /dev/null +++ b/src/dbus/client_allow_list.rs @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: GPL-3.0-only + +//! Configurable allow lists for the D-Bus interfaces. +//! +//! Alongside the clients compiled in below, you can grant access to further clients by +//! dropping TOML files into [`ETC_DROP_IN_DIR`] (for administrators) or +//! [`VENDOR_DROP_IN_DIR`] (for packages): + +use std::{ + collections::{BTreeMap, HashMap}, + ffi::OsString, + fs, + os::unix::fs::MetadataExt, + path::{Path, PathBuf}, +}; + +use serde::Deserialize; +use tracing::{debug, info, warn}; +use zbus::names::WellKnownName; + +use crate::libei::{ + DEVICE_TYPE_ALL, DEVICE_TYPE_KEYBOARD, DEVICE_TYPE_POINTER, DEVICE_TYPE_TOUCHSCREEN, +}; + +const VENDOR_DROP_IN_DIR: &str = "/usr/share/cosmic-comp/ei-clients.d"; +const ETC_DROP_IN_DIR: &str = "/etc/cosmic-comp/ei-clients.d"; + +static BUILT_IN_EI_CLIENTS: &[WellKnownName] = &[ + WellKnownName::from_static_str_unchecked("org.freedesktop.impl.portal.desktop.cosmic"), + WellKnownName::from_static_str_unchecked("com.system76.CosmicOSK"), +]; + +static BUILT_IN_A11Y_CLIENTS: &[WellKnownName] = &[WellKnownName::from_static_str_unchecked( + "org.gnome.Orca.KeyboardMonitor", +)]; + +/// Every allow list, parsed in a single pass over the drop-in files. +#[derive(Debug)] +pub struct ClientAllowLists { + /// Clients permitted on `com.system76.CosmicComp.Ei`. + pub ei: EiAllowList, + /// Clients permitted on `org.freedesktop.a11y.KeyboardMonitor`. + pub a11y_keyboard_monitor: NameAllowList, +} + +impl ClientAllowLists { + /// Built-in clients plus every valid entry found in the drop-in directories. + /// + /// Never fails. The loader logs and skips unreadable, untrusted or malformed files, so + /// the built-in clients keep working whatever is on disk. + pub fn load() -> Self { + Self::load_from(&[Path::new(VENDOR_DROP_IN_DIR), Path::new(ETC_DROP_IN_DIR)]) + } + + fn load_from(dirs: &[&Path]) -> Self { + let mut ei_device_types: HashMap, u32> = BUILT_IN_EI_CLIENTS + .iter() + .map(|name| (name.to_owned(), DEVICE_TYPE_ALL)) + .collect(); + let mut a11y_names: Vec> = BUILT_IN_A11Y_CLIENTS + .iter() + .map(|name| name.to_owned()) + .collect(); + + for path in drop_in_files(dirs) { + let Some(config) = parse_file(&path) else { + continue; + }; + for (name, mask) in ei_clients(config.ei, &path) { + // A name granted by several files gets the union of what they permit, + // since each file is an independent grant. + *ei_device_types.entry(name).or_insert(0) |= mask; + } + for name in a11y_clients(config.a11y_keyboard_monitor, &path) { + if !a11y_names.contains(&name) { + a11y_names.push(name); + } + } + } + + let mut ei_names: Vec<_> = ei_device_types.keys().cloned().collect(); + ei_names.sort(); + a11y_names.sort(); + debug!(names = ?ei_names, "Loaded EI client allow list"); + debug!(names = ?a11y_names, "Loaded a11y keyboard monitor allow list"); + + Self { + ei: EiAllowList { + names: ei_names, + device_types: ei_device_types, + }, + a11y_keyboard_monitor: NameAllowList { names: a11y_names }, + } + } +} + +/// The set of bus names allowed to open an EI sender socket, and what each may request. +#[derive(Debug)] +pub struct EiAllowList { + /// Flattened bus names, in the form [`super::name_owners::NameOwners`] wants them. + names: Vec>, + /// Permitted device-type mask per bus name. + device_types: HashMap, u32>, +} + +impl EiAllowList { + /// Every allowed bus name, for [`super::name_owners::NameOwners::check_owner`]. + pub fn names(&self) -> &[WellKnownName<'static>] { + &self.names + } + + /// The device types `name` may request, or `None` if it isn't on the allow list. + pub fn device_types_for(&self, name: &WellKnownName<'_>) -> Option { + self.device_types.get(name).copied() + } +} + +/// A plain list of allowed bus names, for interfaces with no per-client policy. +#[derive(Clone, Debug)] +pub struct NameAllowList { + names: Vec>, +} + +impl NameAllowList { + /// Every allowed bus name, for [`super::name_owners::NameOwners::check_owner`]. + pub fn names(&self) -> &[WellKnownName<'static>] { + &self.names + } +} + +fn drop_in_files(dirs: &[&Path]) -> Vec { + let mut files: BTreeMap = BTreeMap::new(); + for dir in dirs { + match dir.metadata() { + Ok(metadata) if !is_trusted(dir, &metadata) => continue, + Ok(_) => {} + // A missing drop-in directory is the normal case + Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue, + Err(err) => { + warn!( + ?err, + "Failed to stat client config directory {}", + dir.display() + ); + continue; + } + } + let entries = match fs::read_dir(dir) { + Ok(entries) => entries, + Err(err) => { + warn!( + ?err, + "Failed to read client config directory {}", + dir.display() + ); + continue; + } + }; + + for entry in entries { + let entry = match entry { + Ok(entry) => entry, + Err(err) => { + warn!(?err, "Failed to read entry in {}", dir.display()); + continue; + } + }; + let path = entry.path(); + if path.extension().is_some_and(|ext| ext == "toml") { + files.insert(entry.file_name(), path); + } + } + } + files.into_values().collect() +} + +fn parse_file(path: &Path) -> Option { + let metadata = match path.metadata() { + Ok(metadata) => metadata, + Err(err) => { + warn!(?err, "Failed to stat {}", path.display()); + return None; + } + }; + if !metadata.is_file() || !is_trusted(path, &metadata) { + return None; + } + + let contents = match fs::read_to_string(path) { + Ok(contents) => contents, + Err(err) => { + warn!(?err, "Failed to read {}", path.display()); + return None; + } + }; + match toml::from_str(&contents) { + Ok(config) => Some(config), + Err(err) => { + warn!(?err, "Failed to parse {}", path.display()); + None + } + } +} + +/// Validated EI entries from one file's `[[ei.client]]` tables. +fn ei_clients(section: Section, path: &Path) -> Vec<(WellKnownName<'static>, u32)> { + section + .client + .into_iter() + .filter_map(|client| { + let name = parse_bus_name(&client.bus_name, path)?; + let mask = match &client.device_types { + Some(types) => device_type_mask(types, path), + None => DEVICE_TYPE_ALL, + }; + if mask == 0 { + warn!( + "EI client {} in {} permits no device types and will be denied access", + name, + path.display() + ); + } + info!( + "Allowing EI client {} (device types {:#b}) from {}", + name, + mask, + path.display() + ); + Some((name, mask)) + }) + .collect() +} + +fn a11y_clients(section: Section, path: &Path) -> Vec> { + section + .client + .into_iter() + .filter_map(|client| { + let name = parse_bus_name(&client.bus_name, path)?; + if client.device_types.is_some() { + warn!( + "Ignoring `device_types` on keyboard monitor client {} in {}: the \ + interface carries no per-device policy", + name, + path.display() + ); + } + info!( + "Allowing a11y keyboard monitor client {} from {}", + name, + path.display() + ); + Some(name) + }) + .collect() +} + +fn parse_bus_name(raw: &str, path: &Path) -> Option> { + match WellKnownName::try_from(raw.to_owned()) { + Ok(name) => Some(name), + Err(err) => { + warn!( + ?err, + "Ignoring invalid bus name {:?} in {}", + raw, + path.display() + ); + None + } + } +} + +fn device_type_mask(types: &[String], path: &Path) -> u32 { + let mut mask = 0; + for device_type in types { + match device_type.as_str() { + "keyboard" => mask |= DEVICE_TYPE_KEYBOARD, + "pointer" => mask |= DEVICE_TYPE_POINTER, + "touchscreen" => mask |= DEVICE_TYPE_TOUCHSCREEN, + other => warn!( + "Ignoring unknown device type {:?} in {}", + other, + path.display() + ), + } + } + mask +} + +fn is_trusted(path: &Path, metadata: &fs::Metadata) -> bool { + if metadata.mode() & 0o022 != 0 { + warn!( + "Ignoring client config path {}: it is group- or world-writable", + path.display() + ); + return false; + } + let uid = metadata.uid(); + if uid != 0 && uid != rustix::process::geteuid().as_raw() { + warn!( + "Ignoring client config path {}: it is owned by uid {}", + path.display(), + uid + ); + return false; + } + true +} + +#[derive(Debug, Default, Deserialize)] +struct ConfigFile { + #[serde(default)] + ei: Section, + #[serde(default)] + a11y_keyboard_monitor: Section, +} + +#[derive(Debug, Default, Deserialize)] +struct Section { + #[serde(default)] + client: Vec, +} + +#[derive(Debug, Deserialize)] +struct ClientEntry { + bus_name: String, + /// Only meaningful under `[ei]` + device_types: Option>, +} diff --git a/src/dbus/ei.rs b/src/dbus/ei.rs index 263bced4b..42d97e8f5 100644 --- a/src/dbus/ei.rs +++ b/src/dbus/ei.rs @@ -4,14 +4,11 @@ use std::{ }; use smithay::reexports::calloop; -use zbus::names::{UniqueName, WellKnownName}; +use tracing::warn; +use zbus::names::UniqueName; -use super::name_owners::NameOwners; - -static ALLOWED_NAMES: &[WellKnownName] = &[ - WellKnownName::from_static_str_unchecked("org.freedesktop.impl.portal.desktop.cosmic"), - WellKnownName::from_static_str_unchecked("com.system76.CosmicOSK"), -]; +use super::{client_allow_list::EiAllowList, name_owners::NameOwners}; +use crate::libei::DEVICE_TYPE_ALL; /// Channel for handing the EI socketpair (and requested device types) /// It's `None` until the EI sender side has been set up @@ -20,15 +17,25 @@ type EiSender = Arc) -> zbus::fdo::Result<()> { - if self.name_owners.check_owner(sender, ALLOWED_NAMES).await { - Ok(()) - } else { - Err(zbus::fdo::Error::AccessDenied("Access denied".to_string())) + /// Check `sender` against the allow list, returning the device types it may request. + async fn check_sender_allowed(&self, sender: &UniqueName<'_>) -> zbus::fdo::Result { + if !self + .name_owners + .check_owner(sender, self.allow_list.names()) + .await + { + return Err(zbus::fdo::Error::AccessDenied("Access denied".to_string())); } + // `check_owner` has already polled the owners, so the second lookup is cache-only. + Ok(self + .name_owners + .matched_name_no_poll(sender, self.allow_list.names()) + .and_then(|name| self.allow_list.device_types_for(&name)) + .unwrap_or(DEVICE_TYPE_ALL)) } } @@ -40,8 +47,27 @@ impl Ei { device_types: u32, #[zbus(header)] header: zbus::message::Header<'_>, ) -> zbus::fdo::Result { + let mut device_types = device_types; if let Some(sender) = header.sender() { - self.check_sender_allowed(sender).await?; + // Cap the capabilities with the configured list + let permitted = self.check_sender_allowed(sender).await?; + let allowed = device_types & permitted; + if allowed != device_types { + warn!( + requested = format!("{device_types:#b}"), + permitted = format!("{permitted:#b}"), + "Restricting EI device types for {sender}", + ); + // The mask covers nothing the client asked for, so there is nothing useful + // to hand back. + if allowed == 0 { + return Err(zbus::fdo::Error::AccessDenied( + "None of the requested device types are permitted for this client" + .to_string(), + )); + } + } + device_types = allowed; } let (comp_stream, client_stream) = UnixStream::pair().map_err(|err| { @@ -67,10 +93,12 @@ pub async fn init( conn: &zbus::Connection, name_owners: &NameOwners, ei_sender: EiSender, + allow_list: EiAllowList, ) -> zbus::Result<()> { let ei = Ei { ei_sender, name_owners: name_owners.clone(), + allow_list, }; conn.object_server() .at("/com/system76/CosmicComp/Ei", ei) diff --git a/src/dbus/mod.rs b/src/dbus/mod.rs index 9138caee8..b153f1bb5 100644 --- a/src/dbus/mod.rs +++ b/src/dbus/mod.rs @@ -15,6 +15,7 @@ use tracing::{error, warn}; pub mod a11y_keyboard_monitor; use a11y_keyboard_monitor::A11yKeyboardMonitorState; +mod client_allow_list; pub mod ei; #[cfg(feature = "logind")] pub mod logind; @@ -90,10 +91,22 @@ impl DBusState { async fn init_session(state: &DBusState) -> zbus::Result<()> { let conn = state.session_conn().await?; let name_owners = name_owners::NameOwners::new(conn, &state.0.executor).await?; - let a11y_keyboard_monitor_state = - A11yKeyboardMonitorState::new(conn, &name_owners, &state.0.executor).await?; + let allow_lists = client_allow_list::ClientAllowLists::load(); + let a11y_keyboard_monitor_state = A11yKeyboardMonitorState::new( + conn, + &name_owners, + &state.0.executor, + allow_lists.a11y_keyboard_monitor, + ) + .await?; *state.0.a11y_keyboard_monitor.borrow_mut() = Some(a11y_keyboard_monitor_state); - ei::init(conn, &name_owners, state.0.ei_sender.clone()).await?; + ei::init( + conn, + &name_owners, + state.0.ei_sender.clone(), + allow_lists.ei, + ) + .await?; Ok(()) } diff --git a/src/dbus/name_owners.rs b/src/dbus/name_owners.rs index a2715c738..a048ad684 100644 --- a/src/dbus/name_owners.rs +++ b/src/dbus/name_owners.rs @@ -37,6 +37,18 @@ impl Drop for Inner { } impl Inner { + /// Which of `allowed_names` the unique name `name` owns, if any. + fn matched_name( + &self, + name: &UniqueName<'_>, + allowed_names: &[WellKnownName<'_>], + ) -> Option> { + allowed_names + .iter() + .find(|n| self.name_owners.get(*n).and_then(|x| x.as_ref()) == Some(name)) + .map(|n| n.to_owned()) + } + /// Process all events so far on `stream`, and update `name_owners`. fn update_if_needed(&mut self) { let mut context = Context::from_waker(&self.waker); @@ -165,10 +177,24 @@ impl NameOwners { // more to check. true } else { - allowed_names - .iter() - .any(|n| inner.name_owners.get(n).and_then(|x| x.as_ref()) == Some(name)) + inner.matched_name(name, allowed_names).is_some() + } + } + + /// Which of `allowed_names` the unique name `name` owns, if any. + /// + /// Look up per-client policy with this after an allow check has passed. + pub fn matched_name_no_poll( + &self, + name: &UniqueName<'_>, + allowed_names: &[WellKnownName<'_>], + ) -> Option> { + let mut inner = self.0.lock().unwrap(); + inner.update_if_needed(); + if !inner.unique_names.contains(name) { + return None; } + inner.matched_name(name, allowed_names) } /// Lazily populate `name_owenrs` with owners of well known names diff --git a/src/libei.rs b/src/libei.rs index ad3a674d4..89c504afb 100644 --- a/src/libei.rs +++ b/src/libei.rs @@ -15,9 +15,11 @@ use crate::state::{BackendData, State}; use crate::utils::{geometry::RectGlobalExt, prelude::OutputExt}; // Requested device types for an EI connection, mirroring the XDG RemoteDesktop portal `DeviceType` bitmask -const DEVICE_TYPE_KEYBOARD: u32 = 1; -const DEVICE_TYPE_POINTER: u32 = 2; -const DEVICE_TYPE_TOUCHSCREEN: u32 = 4; +pub const DEVICE_TYPE_KEYBOARD: u32 = 1; +pub const DEVICE_TYPE_POINTER: u32 = 2; +pub const DEVICE_TYPE_TOUCHSCREEN: u32 = 4; +pub const DEVICE_TYPE_ALL: u32 = + DEVICE_TYPE_KEYBOARD | DEVICE_TYPE_POINTER | DEVICE_TYPE_TOUCHSCREEN; // Name of the EI absolute-pointer device. Shared so the connect path and the // re-advertise-on-output-change path recreate the same device.