use serde::{Deserialize, Serialize};
use serde_json::Value;
use chio_kernel::{Guard, GuardContext, KernelError, Verdict};
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RemoteDesktopSideChannelConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_true")]
pub clipboard_enabled: bool,
#[serde(default = "default_true")]
pub file_transfer_enabled: bool,
#[serde(default = "default_true")]
pub session_share_enabled: bool,
#[serde(default = "default_true")]
pub audio_enabled: bool,
#[serde(default = "default_true")]
pub drive_mapping_enabled: bool,
#[serde(default = "default_true")]
pub printing_enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_transfer_size_bytes: Option<u64>,
}
fn default_true() -> bool {
true
}
impl Default for RemoteDesktopSideChannelConfig {
fn default() -> Self {
Self {
enabled: true,
clipboard_enabled: true,
file_transfer_enabled: true,
session_share_enabled: true,
audio_enabled: true,
drive_mapping_enabled: true,
printing_enabled: true,
max_transfer_size_bytes: None,
}
}
}
pub struct RemoteDesktopSideChannelGuard {
config: RemoteDesktopSideChannelConfig,
}
impl RemoteDesktopSideChannelGuard {
pub fn new() -> Self {
Self::with_config(RemoteDesktopSideChannelConfig::default())
}
pub fn with_config(config: RemoteDesktopSideChannelConfig) -> Self {
Self { config }
}
fn channel_action_type(tool_name: &str, arguments: &Value) -> Option<String> {
if is_side_channel(tool_name) {
return Some(tool_name.to_string());
}
for key in ["action_type", "actionType", "custom_type", "customType"] {
if let Some(v) = arguments.get(key).and_then(|v| v.as_str()) {
if is_side_channel(v) {
return Some(v.to_string());
}
}
}
None
}
#[allow(clippy::result_unit_err)]
fn read_transfer_size(arguments: &Value) -> Result<Option<u64>, ()> {
let value = match arguments
.get("transfer_size")
.or_else(|| arguments.get("transferSize"))
{
Some(v) => v,
None => return Ok(None),
};
match value.as_u64() {
Some(n) => Ok(Some(n)),
None => Err(()),
}
}
}
impl Default for RemoteDesktopSideChannelGuard {
fn default() -> Self {
Self::new()
}
}
impl Guard for RemoteDesktopSideChannelGuard {
fn name(&self) -> &str {
"remote-desktop-side-channel"
}
fn evaluate(&self, ctx: &GuardContext) -> Result<Verdict, KernelError> {
if !self.config.enabled {
return Ok(Verdict::Allow);
}
let channel =
match Self::channel_action_type(&ctx.request.tool_name, &ctx.request.arguments) {
Some(c) => c,
None => return Ok(Verdict::Allow),
};
match channel.as_str() {
"remote.clipboard" => Ok(if self.config.clipboard_enabled {
Verdict::Allow
} else {
Verdict::Deny
}),
"remote.file_transfer" => {
if !self.config.file_transfer_enabled {
return Ok(Verdict::Deny);
}
if let Some(max) = self.config.max_transfer_size_bytes {
match Self::read_transfer_size(&ctx.request.arguments) {
Ok(Some(n)) => {
if n > max {
return Ok(Verdict::Deny);
}
}
Ok(None) | Err(()) => return Ok(Verdict::Deny),
}
}
Ok(Verdict::Allow)
}
"remote.session_share" => Ok(if self.config.session_share_enabled {
Verdict::Allow
} else {
Verdict::Deny
}),
"remote.audio" => Ok(if self.config.audio_enabled {
Verdict::Allow
} else {
Verdict::Deny
}),
"remote.drive_mapping" => Ok(if self.config.drive_mapping_enabled {
Verdict::Allow
} else {
Verdict::Deny
}),
"remote.printing" => Ok(if self.config.printing_enabled {
Verdict::Allow
} else {
Verdict::Deny
}),
_ => Ok(Verdict::Deny),
}
}
}
fn is_side_channel(s: &str) -> bool {
if !s.starts_with("remote.") {
return false;
}
!matches!(
s,
"remote.session.connect" | "remote.session.disconnect" | "remote.session.reconnect"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_side_channel_classifies_correctly() {
assert!(is_side_channel("remote.clipboard"));
assert!(is_side_channel("remote.file_transfer"));
assert!(is_side_channel("remote.webrtc")); assert!(!is_side_channel("remote.session.connect"));
assert!(!is_side_channel("input.inject"));
assert!(!is_side_channel("filesystem"));
}
#[test]
fn read_transfer_size_variants() {
let ok = serde_json::json!({"transfer_size": 1024});
assert_eq!(
RemoteDesktopSideChannelGuard::read_transfer_size(&ok),
Ok(Some(1024))
);
let camel = serde_json::json!({"transferSize": 2048});
assert_eq!(
RemoteDesktopSideChannelGuard::read_transfer_size(&camel),
Ok(Some(2048))
);
let missing = serde_json::json!({});
assert_eq!(
RemoteDesktopSideChannelGuard::read_transfer_size(&missing),
Ok(None)
);
let bad = serde_json::json!({"transfer_size": "1024"});
assert_eq!(
RemoteDesktopSideChannelGuard::read_transfer_size(&bad),
Err(())
);
}
}