chio_guards/
remote_desktop.rs1use serde::{Deserialize, Serialize};
33use serde_json::Value;
34
35use chio_kernel::{Guard, GuardContext, KernelError, Verdict};
36
37#[derive(Clone, Debug, Deserialize, Serialize)]
39#[serde(deny_unknown_fields)]
40pub struct RemoteDesktopSideChannelConfig {
41 #[serde(default = "default_true")]
43 pub enabled: bool,
44 #[serde(default = "default_true")]
46 pub clipboard_enabled: bool,
47 #[serde(default = "default_true")]
49 pub file_transfer_enabled: bool,
50 #[serde(default = "default_true")]
52 pub session_share_enabled: bool,
53 #[serde(default = "default_true")]
55 pub audio_enabled: bool,
56 #[serde(default = "default_true")]
58 pub drive_mapping_enabled: bool,
59 #[serde(default = "default_true")]
61 pub printing_enabled: bool,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub max_transfer_size_bytes: Option<u64>,
65}
66
67fn default_true() -> bool {
68 true
69}
70
71impl Default for RemoteDesktopSideChannelConfig {
72 fn default() -> Self {
73 Self {
74 enabled: true,
75 clipboard_enabled: true,
76 file_transfer_enabled: true,
77 session_share_enabled: true,
78 audio_enabled: true,
79 drive_mapping_enabled: true,
80 printing_enabled: true,
81 max_transfer_size_bytes: None,
82 }
83 }
84}
85
86pub struct RemoteDesktopSideChannelGuard {
89 config: RemoteDesktopSideChannelConfig,
90}
91
92impl RemoteDesktopSideChannelGuard {
93 pub fn new() -> Self {
96 Self::with_config(RemoteDesktopSideChannelConfig::default())
97 }
98
99 pub fn with_config(config: RemoteDesktopSideChannelConfig) -> Self {
101 Self { config }
102 }
103
104 fn channel_action_type(tool_name: &str, arguments: &Value) -> Option<String> {
108 if is_side_channel(tool_name) {
109 return Some(tool_name.to_string());
110 }
111 for key in ["action_type", "actionType", "custom_type", "customType"] {
112 if let Some(v) = arguments.get(key).and_then(|v| v.as_str()) {
113 if is_side_channel(v) {
114 return Some(v.to_string());
115 }
116 }
117 }
118 None
119 }
120
121 #[allow(clippy::result_unit_err)]
128 fn read_transfer_size(arguments: &Value) -> Result<Option<u64>, ()> {
129 let value = match arguments
130 .get("transfer_size")
131 .or_else(|| arguments.get("transferSize"))
132 {
133 Some(v) => v,
134 None => return Ok(None),
135 };
136 match value.as_u64() {
137 Some(n) => Ok(Some(n)),
138 None => Err(()),
139 }
140 }
141}
142
143impl Default for RemoteDesktopSideChannelGuard {
144 fn default() -> Self {
145 Self::new()
146 }
147}
148
149impl Guard for RemoteDesktopSideChannelGuard {
150 fn name(&self) -> &str {
151 "remote-desktop-side-channel"
152 }
153
154 fn evaluate(&self, ctx: &GuardContext) -> Result<Verdict, KernelError> {
155 if !self.config.enabled {
156 return Ok(Verdict::Allow);
157 }
158
159 let channel =
160 match Self::channel_action_type(&ctx.request.tool_name, &ctx.request.arguments) {
161 Some(c) => c,
162 None => return Ok(Verdict::Allow),
163 };
164
165 match channel.as_str() {
166 "remote.clipboard" => Ok(if self.config.clipboard_enabled {
167 Verdict::Allow
168 } else {
169 Verdict::Deny
170 }),
171 "remote.file_transfer" => {
172 if !self.config.file_transfer_enabled {
173 return Ok(Verdict::Deny);
174 }
175 if let Some(max) = self.config.max_transfer_size_bytes {
176 match Self::read_transfer_size(&ctx.request.arguments) {
177 Ok(Some(n)) => {
178 if n > max {
179 return Ok(Verdict::Deny);
180 }
181 }
182 Ok(None) | Err(()) => return Ok(Verdict::Deny),
185 }
186 }
187 Ok(Verdict::Allow)
188 }
189 "remote.session_share" => Ok(if self.config.session_share_enabled {
190 Verdict::Allow
191 } else {
192 Verdict::Deny
193 }),
194 "remote.audio" => Ok(if self.config.audio_enabled {
195 Verdict::Allow
196 } else {
197 Verdict::Deny
198 }),
199 "remote.drive_mapping" => Ok(if self.config.drive_mapping_enabled {
200 Verdict::Allow
201 } else {
202 Verdict::Deny
203 }),
204 "remote.printing" => Ok(if self.config.printing_enabled {
205 Verdict::Allow
206 } else {
207 Verdict::Deny
208 }),
209 _ => Ok(Verdict::Deny),
211 }
212 }
213}
214
215fn is_side_channel(s: &str) -> bool {
218 if !s.starts_with("remote.") {
219 return false;
220 }
221 !matches!(
222 s,
223 "remote.session.connect" | "remote.session.disconnect" | "remote.session.reconnect"
224 )
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn is_side_channel_classifies_correctly() {
233 assert!(is_side_channel("remote.clipboard"));
234 assert!(is_side_channel("remote.file_transfer"));
235 assert!(is_side_channel("remote.webrtc")); assert!(!is_side_channel("remote.session.connect"));
237 assert!(!is_side_channel("input.inject"));
238 assert!(!is_side_channel("filesystem"));
239 }
240
241 #[test]
242 fn read_transfer_size_variants() {
243 let ok = serde_json::json!({"transfer_size": 1024});
244 assert_eq!(
245 RemoteDesktopSideChannelGuard::read_transfer_size(&ok),
246 Ok(Some(1024))
247 );
248 let camel = serde_json::json!({"transferSize": 2048});
249 assert_eq!(
250 RemoteDesktopSideChannelGuard::read_transfer_size(&camel),
251 Ok(Some(2048))
252 );
253 let missing = serde_json::json!({});
254 assert_eq!(
255 RemoteDesktopSideChannelGuard::read_transfer_size(&missing),
256 Ok(None)
257 );
258 let bad = serde_json::json!({"transfer_size": "1024"});
259 assert_eq!(
260 RemoteDesktopSideChannelGuard::read_transfer_size(&bad),
261 Err(())
262 );
263 }
264}