1use std::collections::HashSet;
2use std::io::{IoSlice, IoSliceMut};
3use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
4use std::os::unix::net::{UnixListener, UnixStream};
5use std::path::Path;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Mutex};
8use std::time::Instant;
9
10use anyhow::{Context, Result};
11use nix::sys::signal::{SaFlags, SigAction, SigHandler, SigSet, Signal, sigaction};
12use nix::sys::socket::{ControlMessage, ControlMessageOwned, MsgFlags, recvmsg, sendmsg};
13
14static SHUTDOWN_REQUESTED: AtomicBool = AtomicBool::new(false);
16
17fn setup_signal_handler() {
19 extern "C" fn handle_signal(_: i32) {
20 SHUTDOWN_REQUESTED.store(true, Ordering::Relaxed);
21 }
22 let sig_action = SigAction::new(
23 SigHandler::Handler(handle_signal),
24 SaFlags::empty(),
25 SigSet::empty(),
26 );
27 unsafe {
29 let _ = sigaction(Signal::SIGTERM, &sig_action);
30 let _ = sigaction(Signal::SIGINT, &sig_action);
31 }
32}
33
34use crate::config::Config;
35
36const MAX_CONNECTIONS: usize = 128;
37
38struct FirewallState {
39 blocked_interfaces: HashSet<String>,
40}
41
42impl FirewallState {
43 fn new(blocked_interfaces: Vec<String>) -> Self {
44 Self {
45 blocked_interfaces: blocked_interfaces.into_iter().collect(),
46 }
47 }
48}
49
50pub fn run_compositor(config: &Config, name: &str) -> Result<()> {
57 let xdg_runtime = std::env::var("XDG_RUNTIME_DIR")
58 .or_else(|_| {
59 let uid = nix::unistd::getuid().as_raw();
60 Ok::<_, std::env::VarError>(format!("/run/user/{uid}"))
61 })
62 .context("XDG_RUNTIME_DIR not set")?;
63
64 let wayland_display = std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "wayland-0".into());
65 let host_socket = Path::new(&xdg_runtime).join(&wayland_display);
66
67 if !host_socket.exists() {
68 anyhow::bail!(
69 "Host Wayland socket not found at {} (WAYLAND_DISPLAY={})",
70 host_socket.display(),
71 wayland_display
72 );
73 }
74
75 let socket_path = Path::new(&xdg_runtime)
76 .join("podbox")
77 .join(format!("{name}-wayland.sock"));
78
79 let _ = std::fs::remove_file(&socket_path);
80 std::fs::create_dir_all(socket_path.parent().context("socket path has no parent")?)?;
81
82 setup_signal_handler();
83
84 let listener = UnixListener::bind(&socket_path).with_context(|| {
85 format!(
86 "Failed to bind Wayland proxy socket at {}",
87 socket_path.display()
88 )
89 })?;
90
91 let blocked = config.wayland.blocked_interfaces.clone();
92
93 let mut connections = 0;
94 loop {
95 if SHUTDOWN_REQUESTED.load(Ordering::Relaxed) || connections >= MAX_CONNECTIONS {
96 break;
97 }
98
99 let stream = match listener.accept() {
100 Ok((s, _)) => s,
101 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
102 Err(e) => {
103 tracing::error!("compositor: accept failed: {e}");
104 break;
105 }
106 };
107 connections += 1;
108
109 let host_conn = match UnixStream::connect(&host_socket) {
110 Ok(s) => s,
111 Err(e) => {
112 tracing::error!("compositor: failed to connect to host Wayland socket: {e}");
113 continue;
114 }
115 };
116
117 let state = Arc::new(Mutex::new(FirewallState::new(blocked.clone())));
118 let done = Arc::new(AtomicBool::new(false));
119
120 let client_clone = stream.try_clone()?;
121 let host_clone = host_conn.try_clone()?;
122 let state_c2h = Arc::clone(&state);
123 let done_c2h = Arc::clone(&done);
124
125 std::thread::spawn(move || {
126 if let Err(e) = bridge_loop(stream, host_clone, state_c2h, &done_c2h, true) {
127 tracing::error!("compositor: client→host bridge error: {e}");
128 }
129 done_c2h.store(true, Ordering::Relaxed);
130 });
131
132 let state_h2c = state;
133 let done_h2c = done;
134
135 std::thread::spawn(move || {
136 if let Err(e) = bridge_loop(host_conn, client_clone, state_h2c, &done_h2c, false) {
137 tracing::error!("compositor: host→client bridge error: {e}");
138 }
139 done_h2c.store(true, Ordering::Relaxed);
140 });
141 }
142
143 Ok(())
144}
145
146fn rate_allow(bucket: &mut f64, last_refill: &mut Instant) -> bool {
149 const RATE: f64 = 10_000.0;
150 let now = Instant::now();
151 let elapsed = now.duration_since(*last_refill).as_secs_f64();
152 *bucket = (*bucket + elapsed * RATE).min(RATE);
153 *last_refill = now;
154 if *bucket >= 1.0 {
155 *bucket -= 1.0;
156 true
157 } else {
158 false
159 }
160}
161
162fn bridge_loop(
171 in_socket: UnixStream,
172 out_socket: UnixStream,
173 state: Arc<Mutex<FirewallState>>,
174 done: &AtomicBool,
175 is_client_to_host: bool,
176) -> Result<()> {
177 let mut read_buf = [0u8; 16384];
178 let mut cmsg_buffer = vec![0u8; 4096];
179 let mut bytes_cache = Vec::with_capacity(32768);
180 let mut pending_fds: Vec<OwnedFd> = Vec::new();
181
182 let mut bucket: f64 = 10_000.0;
185 let mut last_refill = Instant::now();
186
187 loop {
188 if done.load(Ordering::Relaxed) {
189 break;
190 }
191
192 let msg_bytes = {
193 let mut iov = [IoSliceMut::new(&mut read_buf)];
194 let msg = match recvmsg::<()>(
195 in_socket.as_raw_fd(),
196 &mut iov,
197 Some(&mut cmsg_buffer),
198 MsgFlags::empty(),
199 ) {
200 Ok(m) => m,
201 Err(e) if e == nix::errno::Errno::EINTR => continue,
202 Err(e) => {
203 done.store(true, Ordering::Relaxed);
204 let _ = in_socket.shutdown(std::net::Shutdown::Both);
205 let _ = out_socket.shutdown(std::net::Shutdown::Both);
206 return Err(e.into());
207 }
208 };
209
210 let bytes = msg.bytes;
211 if bytes == 0 {
212 break;
213 }
214
215 if let Ok(cmsgs) = msg.cmsgs() {
216 for cmsg in cmsgs {
217 if let ControlMessageOwned::ScmRights(fds) = cmsg {
218 for fd in fds {
219 let owned = unsafe { OwnedFd::from_raw_fd(fd) };
221 pending_fds.push(owned);
222 }
223 }
224 }
225 }
226
227 bytes
228 };
229
230 bytes_cache.extend_from_slice(&read_buf[..msg_bytes]);
231
232 let mut consumed = 0;
234 while consumed + 8 <= bytes_cache.len() {
235 let header = &bytes_cache[consumed..consumed + 8];
236 let size_and_opcode = u32::from_ne_bytes(header[4..8].try_into().unwrap());
237 let msg_size = (size_and_opcode >> 16) as usize;
238 let opcode = (size_and_opcode & 0xFFFF) as u16;
239
240 if msg_size < 8 {
241 done.store(true, Ordering::Relaxed);
242 let _ = in_socket.shutdown(std::net::Shutdown::Both);
243 let _ = out_socket.shutdown(std::net::Shutdown::Both);
244 anyhow::bail!("Invalid Wayland message size: {}", msg_size);
245 }
246
247 if consumed + msg_size > bytes_cache.len() {
248 break;
249 }
250
251 let message_bytes = &bytes_cache[consumed..consumed + msg_size];
252 let should_drop =
253 !is_client_to_host && is_blocked_global(message_bytes, opcode, &state);
254
255 if should_drop {
256 pending_fds.clear();
257 } else if is_client_to_host || rate_allow(&mut bucket, &mut last_refill) {
258 forward_message(&out_socket, message_bytes, &mut pending_fds)?;
259 } else {
260 pending_fds.clear();
261 done.store(true, Ordering::Relaxed);
262 let _ = in_socket.shutdown(std::net::Shutdown::Both);
263 let _ = out_socket.shutdown(std::net::Shutdown::Both);
264 tracing::warn!("compositor: rate-limited, closing connection");
265 return Ok(());
266 }
267
268 consumed += msg_size;
269 }
270
271 bytes_cache.drain(..consumed);
272 }
273
274 done.store(true, Ordering::Relaxed);
276 let _ = in_socket.shutdown(std::net::Shutdown::Both);
277 let _ = out_socket.shutdown(std::net::Shutdown::Both);
278 Ok(())
279}
280
281fn is_blocked_global(message_bytes: &[u8], opcode: u16, state: &Mutex<FirewallState>) -> bool {
284 if opcode != 0 || message_bytes.len() < 16 {
291 return false;
292 }
293
294 let str_len = u32::from_ne_bytes(message_bytes[12..16].try_into().unwrap()) as usize;
295
296 if message_bytes
298 .len()
299 .checked_sub(16)
300 .is_none_or(|rem| rem < str_len)
301 {
302 return false;
303 }
304
305 if str_len < 2 {
306 return false;
307 }
308
309 let interface_bytes = &message_bytes[16..16 + str_len - 1];
311 let interface_name = match std::str::from_utf8(interface_bytes) {
312 Ok(s) => s,
313 Err(_) => return false,
314 };
315
316 let guard = state.lock().unwrap_or_else(|e| e.into_inner());
317 guard.blocked_interfaces.contains(interface_name)
318}
319
320fn forward_message(
323 out_socket: &UnixStream,
324 message_bytes: &[u8],
325 pending_fds: &mut Vec<OwnedFd>,
326) -> Result<()> {
327 let iov = [IoSlice::new(message_bytes)];
328
329 if pending_fds.is_empty() {
330 sendmsg::<()>(out_socket.as_raw_fd(), &iov, &[], MsgFlags::empty(), None)?;
331 } else {
332 let raw_fds: Vec<RawFd> = pending_fds.iter().map(|f| f.as_raw_fd()).collect();
333 let cmsg = ControlMessage::ScmRights(&raw_fds);
334 sendmsg::<()>(
335 out_socket.as_raw_fd(),
336 &iov,
337 &[cmsg],
338 MsgFlags::empty(),
339 None,
340 )?;
341 pending_fds.clear();
342 }
343
344 Ok(())
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 fn make_global(object_id: u32, name: u32, interface: &str, version: u32) -> Vec<u8> {
352 let raw = interface.as_bytes();
353 let str_len = raw.len().checked_add(1).unwrap();
354 let padded_len = str_len.next_multiple_of(4);
355 let msg_size = u32::try_from(8 + 4 + 4 + padded_len + 4).unwrap();
356
357 let mut buf = Vec::with_capacity(msg_size as usize);
358 buf.extend_from_slice(&object_id.to_ne_bytes());
359 buf.extend_from_slice(&(msg_size << 16).to_ne_bytes());
360 buf.extend_from_slice(&name.to_ne_bytes());
361 buf.extend_from_slice(&u32::try_from(str_len).unwrap().to_ne_bytes());
362 buf.extend_from_slice(raw);
363 buf.push(0);
364 while buf.len() < (8 + 4 + 4 + padded_len) {
365 buf.push(0);
366 }
367 buf.extend_from_slice(&version.to_ne_bytes());
368 buf
369 }
370
371 fn make_message(object_id: u32, size: u32, opcode: u16) -> Vec<u8> {
372 let mut buf = Vec::with_capacity(size as usize);
373 buf.extend_from_slice(&object_id.to_ne_bytes());
374 buf.extend_from_slice(&((size << 16) | u32::from(opcode)).to_ne_bytes());
375 while buf.len() < size as usize {
376 buf.push(0);
377 }
378 buf
379 }
380
381 fn blocked_state() -> Mutex<FirewallState> {
382 Mutex::new(FirewallState::new(vec![
383 "zwlr_screencopy_manager_v1".into(),
384 "ext_foreign_toplevel_list_v1".into(),
385 ]))
386 }
387
388 fn empty_state() -> Mutex<FirewallState> {
389 Mutex::new(FirewallState::new(vec![]))
390 }
391
392 #[test]
393 fn blocks_screencopy_interface() {
394 let data = make_global(2, 42, "zwlr_screencopy_manager_v1", 1);
395 assert!(is_blocked_global(&data, 0, &blocked_state()));
396 }
397
398 #[test]
399 fn blocks_foreign_toplevel() {
400 let data = make_global(2, 43, "ext_foreign_toplevel_list_v1", 1);
401 assert!(is_blocked_global(&data, 0, &blocked_state()));
402 }
403
404 #[test]
405 fn allows_safe_interface() {
406 let data = make_global(2, 44, "wl_compositor", 6);
407 assert!(!is_blocked_global(&data, 0, &blocked_state()));
408 }
409
410 #[test]
411 fn allows_wl_shm() {
412 let data = make_global(2, 1, "wl_shm", 1);
413 assert!(!is_blocked_global(&data, 0, &blocked_state()));
414 }
415
416 #[test]
417 fn blocks_nothing_when_empty_blocklist() {
418 let data = make_global(2, 42, "zwlr_screencopy_manager_v1", 1);
419 assert!(!is_blocked_global(&data, 0, &empty_state()));
420 }
421
422 #[test]
423 fn ignores_non_registry_opcode() {
424 let data = make_message(2, 16, 1);
425 assert!(!is_blocked_global(&data, 1, &blocked_state()));
426 }
427
428 #[test]
429 fn ignores_short_payload() {
430 let data = make_message(2, 12, 0);
431 assert!(!is_blocked_global(&data, 0, &blocked_state()));
432 }
433
434 #[test]
435 fn ignores_empty_interface_string() {
436 let mut data = make_message(2, 16, 0);
437 data[12..16].copy_from_slice(&0u32.to_ne_bytes());
438 assert!(!is_blocked_global(&data, 0, &blocked_state()));
439 }
440
441 #[test]
442 fn allows_partial_name_prefix_match() {
443 let data = make_global(2, 42, "zwlr_screencopy", 1);
444 assert!(!is_blocked_global(&data, 0, &blocked_state()));
445 }
446
447 #[test]
448 fn allows_similar_but_not_blocked() {
449 let data = make_global(2, 99, "zwlr_layer_shell_v1", 1);
450 assert!(!is_blocked_global(&data, 0, &blocked_state()));
451 }
452
453 #[test]
454 fn rate_allow_accepts_first_message() {
455 let mut bucket = 10_000.0;
456 let mut last = Instant::now();
457 assert!(rate_allow(&mut bucket, &mut last));
458 }
459
460 #[test]
461 fn rate_allow_drains_bucket() {
462 let mut bucket = 2.0;
463 let mut last = Instant::now();
464 assert!(rate_allow(&mut bucket, &mut last));
465 assert!(rate_allow(&mut bucket, &mut last));
466 assert!(!rate_allow(&mut bucket, &mut last));
467 }
468
469 #[test]
470 fn rate_allow_refills_over_time() {
471 let mut bucket = 0.0;
472 let mut last = Instant::now();
473 assert!(!rate_allow(&mut bucket, &mut last));
474 std::thread::sleep(std::time::Duration::from_millis(1));
476 let mut bucket = 0.0;
477 let mut last_refill = Instant::now();
478 std::thread::sleep(std::time::Duration::from_millis(2));
479 assert!(rate_allow(&mut bucket, &mut last_refill));
480 }
481}