feat(server): device allowlist

This commit is contained in:
Rasmus Karlsson 2024-12-28 14:23:41 +01:00
parent 754217adf4
commit 5bd5ab20e4
8 changed files with 113 additions and 8 deletions

View file

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

42
rkvm-input/src/device.rs Normal file
View file

@ -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<std::ffi::CString>,
pub vendor_id: Option<u16>,
pub product_id: Option<u16>,
}
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
}
}

View file

@ -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<Self, OpenError> {
#[tracing::instrument(skip(registry, device_allowlist))]
pub(crate) async fn open(
path: &Path,
registry: &Registry,
device_allowlist: &[DeviceSpec],
) -> Result<Self, OpenError> {
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),
}

View file

@ -1,4 +1,5 @@
pub mod abs;
pub mod device;
pub mod event;
pub mod interceptor;
pub mod key;

View file

@ -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<DeviceSpec>) -> 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<Result<Interceptor, Error>>) {
async fn monitor(sender: Sender<Result<Interceptor, Error>>, device_allowlist: Vec<DeviceSpec>) {
let run = async {
let registry = Registry::new();
@ -70,10 +71,11 @@ async fn monitor(sender: Sender<Result<Interceptor, Error>>) {
continue;
}
let interceptor = match Interceptor::open(&path, &registry).await {
let interceptor = match Interceptor::open(&path, &registry, &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() {

View file

@ -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<SwitchKey>,
pub propagate_switch_keys: Option<bool>,
#[serde(default)]
pub device_allowlist: Vec<DeviceSpec>,
}
#[derive(Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
@ -1236,4 +1239,10 @@ mod test {
let config = include_str!("../../example/server.toml");
toml::from_str::<Config>(config).unwrap();
}
#[test]
fn example_with_allowlist_parses() {
let config = include_str!("../../example/server-with-allowlist.toml");
toml::from_str::<Config>(config).unwrap();
}
}

View file

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

View file

@ -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<Key>,
propagate_switch_keys: bool,
device_allowlist: Vec<DeviceSpec>,
) -> 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::<Device>::new();
let mut clients = Slab::<(Sender<_>, SocketAddr)>::new();
let mut current = 0;