Skip to main content

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}