proc_daemon/ipc.rs
1//! IPC primitives (feature-gated)
2//!
3//! Minimal scaffold for cross-platform IPC channels used for control and health.
4//! Currently implements a Tokio Unix socket server/client on Unix.
5
6#[cfg(unix)]
7/// Unix-specific IPC primitives implemented with Tokio Unix sockets.
8pub mod unix {
9 use std::io;
10 use std::os::unix::fs::FileTypeExt;
11 use std::path::Path;
12 use tokio::net::{UnixListener, UnixStream};
13
14 /// Bind a Unix domain socket at the given path.
15 ///
16 /// # Security
17 ///
18 /// Attempts to atomically remove stale sockets and bind. However, there's still
19 /// a small TOCTOU window. For maximum security, ensure the socket directory
20 /// is only writable by the daemon process.
21 ///
22 /// # Errors
23 ///
24 /// Returns an error if the socket file cannot be removed or if binding to the
25 /// provided path fails.
26 pub async fn bind<P: AsRef<Path>>(path: P) -> io::Result<UnixListener> {
27 let path_ref = path.as_ref();
28
29 // First attempt: try to bind directly
30 match UnixListener::bind(path_ref) {
31 Ok(listener) => return Ok(listener),
32 Err(e) if e.kind() == io::ErrorKind::AddrInUse => {
33 // Address in use - check if it's a stale socket
34 }
35 Err(e) => return Err(e),
36 }
37
38 // Validate the existing file is a socket (not symlink or other file type)
39 match tokio::fs::symlink_metadata(path_ref).await {
40 Ok(metadata) => {
41 let file_type = metadata.file_type();
42 if file_type.is_symlink() {
43 return Err(io::Error::new(
44 io::ErrorKind::AlreadyExists,
45 "IPC path exists and is a symlink (potential security risk)",
46 ));
47 }
48 if !file_type.is_socket() {
49 return Err(io::Error::new(
50 io::ErrorKind::AlreadyExists,
51 "IPC path exists and is not a Unix socket",
52 ));
53 }
54 // It's a socket - try to remove it
55 tokio::fs::remove_file(path_ref).await?;
56 }
57 Err(_) => {
58 // File doesn't exist or can't be accessed
59 return Err(io::Error::new(
60 io::ErrorKind::AddrInUse,
61 "Socket address in use but cannot verify file type",
62 ));
63 }
64 }
65
66 // Try binding again after removal
67 UnixListener::bind(path_ref)
68 }
69
70 /// Connect to a Unix domain socket at the given path.
71 ///
72 /// # Errors
73 ///
74 /// Returns an error if the connection to the provided socket path fails.
75 pub async fn connect<P: AsRef<Path>>(path: P) -> io::Result<UnixStream> {
76 UnixStream::connect(path).await
77 }
78}
79
80#[cfg(windows)]
81/// Windows-specific IPC primitives implemented with Tokio named pipes.
82pub mod windows {
83 //! Tokio-based Windows named pipe IPC.
84 use tokio::io::{AsyncReadExt, AsyncWriteExt};
85 use tokio::net::windows::named_pipe::{
86 ClientOptions, NamedPipeClient, NamedPipeServer, ServerOptions,
87 };
88
89 /// Create a new named pipe server at the given pipe name (e.g., \\?\pipe\proc-daemon).
90 ///
91 /// Returns a server handle that can `connect().await` to wait for a client.
92 pub fn create_server<S: AsRef<str>>(name: S) -> std::io::Result<NamedPipeServer> {
93 ServerOptions::new()
94 .first_pipe_instance(true)
95 .create(name.as_ref())
96 }
97
98 /// Wait asynchronously for a client to connect to the given server instance.
99 pub async fn server_connect(server: &NamedPipeServer) -> std::io::Result<()> {
100 server.connect().await
101 }
102
103 /// Create a new named pipe client and connect to the given pipe name.
104 pub async fn connect<S: AsRef<str>>(name: S) -> std::io::Result<NamedPipeClient> {
105 ClientOptions::new().open(name.as_ref())
106 }
107
108 /// Simple echo handler demonstrating async read/write on a server connection.
109 pub async fn echo_once(mut server: NamedPipeServer) -> std::io::Result<()> {
110 let mut buf = [0u8; 1024];
111 let n = server.read(&mut buf).await?;
112 if n > 0 {
113 server.write_all(&buf[..n]).await?;
114 }
115 server.flush().await?;
116 Ok(())
117 }
118}