diff --git a/example/server-with-allowlist.toml b/example/server-with-allowlist.toml new file mode 100644 index 0000000..0f3b385 --- /dev/null +++ b/example/server-with-allowlist.toml @@ -0,0 +1,27 @@ +listen = "0.0.0.0:5258" +# See `switch-keys.md` in the repository root for the list of all possible keys. +switch-keys = ["left-alt", "left-ctrl"] +# Whether switch key presses should be propagated on the server and its clients. +# Optional, defaults to true. +# propagate-switch-keys = true +certificate = "/etc/rkvm/certificate.pem" +key = "/etc/rkvm/key.pem" + +# This is to prevent malicious clients from connecting to the server. +# Make sure this matches your client's config. +# +# Change this to your own value before deploying rkvm. +password = "123456789" + +# A device must match one of the listed devices to be forwarded +# +# Filling out all fields means all fields must match exactly for it to be +# considered a match +[[device-allowlist]] +name = "device name" +vendor-id = 123 +product-id = 456 + +# Filling out only one field means it must match, and the others are not checked +[[device-allowlist]] +vendor-id = 7 diff --git a/rkvm-input/src/device.rs b/rkvm-input/src/device.rs new file mode 100644 index 0000000..dc51ce6 --- /dev/null +++ b/rkvm-input/src/device.rs @@ -0,0 +1,42 @@ +use serde::Deserialize; + +/// Describes parts of a device +#[derive(Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct DeviceSpec { + pub name: Option, + pub vendor_id: Option, + pub product_id: Option, +} + +impl DeviceSpec { + /// Compares the given values to this DeviceSpec + /// + /// A None value means we skip that comparison + pub fn matches( + &self, + other_name: &std::ffi::CStr, + other_vendor_id: &u16, + other_product_id: &u16, + ) -> bool { + if let Some(name) = &self.name { + if name.as_c_str() != other_name { + return false; + } + } + + if let Some(vendor_id) = &self.vendor_id { + if vendor_id != other_vendor_id { + return false; + } + } + + if let Some(product_id) = &self.product_id { + if product_id != other_product_id { + return false; + } + } + + true + } +} diff --git a/rkvm-input/src/interceptor.rs b/rkvm-input/src/interceptor.rs index 2c034e5..827d11b 100644 --- a/rkvm-input/src/interceptor.rs +++ b/rkvm-input/src/interceptor.rs @@ -4,6 +4,7 @@ pub use caps::{AbsCaps, KeyCaps, RelCaps, Repeat}; use crate::abs::{AbsAxis, AbsEvent, ToolType}; use crate::convert::Convert; +use crate::device::DeviceSpec; use crate::evdev::Evdev; use crate::event::Event; use crate::glue; @@ -177,9 +178,28 @@ impl Interceptor { } } - #[tracing::instrument(skip(registry))] - pub(crate) async fn open(path: &Path, registry: &Registry) -> Result { + #[tracing::instrument(skip(registry, device_allowlist))] + pub(crate) async fn open( + path: &Path, + registry: &Registry, + device_allowlist: &[DeviceSpec], + ) -> Result { let evdev = Evdev::open(path).await?; + + // An empty allowlist means we allow all devices + if !device_allowlist.is_empty() { + let name = evdev.name(); + let vendor_id = evdev.vendor(); + let product_id = evdev.product(); + + if !device_allowlist + .iter() + .any(|check| check.matches(&name, &vendor_id, &product_id)) + { + return Err(OpenError::NotMatchingAllowlist); + } + } + let metadata = evdev.file().unwrap().get_ref().metadata()?; let reader_handle = registry @@ -276,6 +296,8 @@ unsafe impl Send for Interceptor {} pub(crate) enum OpenError { #[error("Not appliable")] NotAppliable, + #[error("Device doesn't match allowlist")] + NotMatchingAllowlist, #[error(transparent)] Io(#[from] Error), } diff --git a/rkvm-input/src/lib.rs b/rkvm-input/src/lib.rs index bed81ab..87b171b 100644 --- a/rkvm-input/src/lib.rs +++ b/rkvm-input/src/lib.rs @@ -1,4 +1,5 @@ pub mod abs; +pub mod device; pub mod event; pub mod interceptor; pub mod key; diff --git a/rkvm-input/src/monitor.rs b/rkvm-input/src/monitor.rs index 403a5d3..7a3239c 100644 --- a/rkvm-input/src/monitor.rs +++ b/rkvm-input/src/monitor.rs @@ -1,3 +1,4 @@ +use crate::device::DeviceSpec; use crate::interceptor::{Interceptor, OpenError}; use crate::registry::Registry; @@ -16,9 +17,9 @@ pub struct Monitor { } impl Monitor { - pub fn new() -> Self { + pub fn new(device_allowlist: Vec) -> Self { let (sender, receiver) = mpsc::channel(1); - tokio::spawn(monitor(sender)); + tokio::spawn(monitor(sender, device_allowlist)); Self { receiver } } @@ -31,7 +32,7 @@ impl Monitor { } } -async fn monitor(sender: Sender>) { +async fn monitor(sender: Sender>, device_allowlist: Vec) { let run = async { let registry = Registry::new(); @@ -70,10 +71,11 @@ async fn monitor(sender: Sender>) { continue; } - let interceptor = match Interceptor::open(&path, ®istry).await { + let interceptor = match Interceptor::open(&path, ®istry, &device_allowlist).await { Ok(interceptor) => interceptor, Err(OpenError::Io(err)) => return Err(err), Err(OpenError::NotAppliable) => continue, + Err(OpenError::NotMatchingAllowlist) => continue, }; if sender.send(Ok(interceptor)).await.is_err() { diff --git a/rkvm-server/src/config.rs b/rkvm-server/src/config.rs index 98ef27f..f442023 100644 --- a/rkvm-server/src/config.rs +++ b/rkvm-server/src/config.rs @@ -1,3 +1,4 @@ +use rkvm_input::device::DeviceSpec; use rkvm_input::key::{Button, Key, Keyboard}; use serde::Deserialize; use std::collections::HashSet; @@ -13,6 +14,8 @@ pub struct Config { pub password: String, pub switch_keys: HashSet, pub propagate_switch_keys: Option, + #[serde(default)] + pub device_allowlist: Vec, } #[derive(Deserialize, Clone, Copy, PartialEq, Eq, Hash)] @@ -1236,4 +1239,10 @@ mod test { let config = include_str!("../../example/server.toml"); toml::from_str::(config).unwrap(); } + + #[test] + fn example_with_allowlist_parses() { + let config = include_str!("../../example/server-with-allowlist.toml"); + toml::from_str::(config).unwrap(); + } } diff --git a/rkvm-server/src/main.rs b/rkvm-server/src/main.rs index eb3f402..0551377 100644 --- a/rkvm-server/src/main.rs +++ b/rkvm-server/src/main.rs @@ -71,7 +71,7 @@ async fn main() -> ExitCode { let propagate_switch_keys = config.propagate_switch_keys.unwrap_or(true); tokio::select! { - result = server::run(config.listen, acceptor, &config.password, &switch_keys, propagate_switch_keys) => { + result = server::run(config.listen, acceptor, &config.password, &switch_keys, propagate_switch_keys, config.device_allowlist) => { if let Err(err) = result { tracing::error!("Error: {}", err); return ExitCode::FAILURE; diff --git a/rkvm-server/src/server.rs b/rkvm-server/src/server.rs index b783181..eb0f7d1 100644 --- a/rkvm-server/src/server.rs +++ b/rkvm-server/src/server.rs @@ -1,4 +1,5 @@ use rkvm_input::abs::{AbsAxis, AbsInfo}; +use rkvm_input::device::DeviceSpec; use rkvm_input::event::Event; use rkvm_input::key::{Key, KeyEvent}; use rkvm_input::monitor::Monitor; @@ -39,11 +40,12 @@ pub async fn run( password: &str, switch_keys: &HashSet, propagate_switch_keys: bool, + device_allowlist: Vec, ) -> Result<(), Error> { let listener = TcpListener::bind(&listen).await.map_err(Error::Network)?; tracing::info!("Listening on {}", listen); - let mut monitor = Monitor::new(); + let mut monitor = Monitor::new(device_allowlist); let mut devices = Slab::::new(); let mut clients = Slab::<(Sender<_>, SocketAddr)>::new(); let mut current = 0;