Skip to main content

mq_bridge/endpoints/memory/
ipc_unix.rs

1//  mq-bridge
2//  © Copyright 2025, by Marco Mengelkoch
3//  Licensed under MIT License, see License file for more details
4//  git clone https://github.com/marcomq/mq-bridge
5
6#![cfg(unix)]
7
8use super::transport::TransportChannel;
9use crate::CanonicalMessage;
10use anyhow::{anyhow, Result};
11use async_trait::async_trait;
12use std::path::Path;
13use std::sync::Arc;
14use tokio::io::{AsyncReadExt, AsyncWriteExt};
15use tokio::net::{UnixListener, UnixStream};
16use tokio::sync::Mutex;
17use tracing::{debug, info, warn};
18
19/// Unix Domain Socket transport for local IPC
20#[derive(Clone)]
21pub struct UnixIpcTransport {
22    inner: Arc<UnixIpcTransportInner>,
23}
24
25struct UnixIpcTransportInner {
26    socket_path: String,
27    capacity: usize,
28    // For server mode (consumer)
29    listener: Mutex<Option<UnixListener>>,
30    // For client mode (publisher)
31    stream: Mutex<Option<UnixStream>>,
32    closed: Mutex<bool>,
33}
34
35impl UnixIpcTransport {
36    /// Create a new Unix IPC transport as a server (consumer side)
37    pub async fn new_server(socket_path: impl AsRef<Path>, capacity: usize) -> Result<Self> {
38        let socket_path = socket_path.as_ref();
39
40        // Remove existing socket if it exists
41        if socket_path.exists() {
42            std::fs::remove_file(socket_path)?;
43        }
44
45        // Create parent directory if needed
46        if let Some(parent) = socket_path.parent() {
47            std::fs::create_dir_all(parent)?;
48            // Set restrictive permissions on directory (0700)
49            #[cfg(unix)]
50            {
51                use std::os::unix::fs::PermissionsExt;
52                let mut perms = std::fs::metadata(parent)?.permissions();
53                perms.set_mode(0o700);
54                std::fs::set_permissions(parent, perms)?;
55            }
56        }
57
58        let listener = UnixListener::bind(socket_path)?;
59
60        // Set restrictive permissions on socket (0600)
61        #[cfg(unix)]
62        {
63            use std::os::unix::fs::PermissionsExt;
64            let mut perms = std::fs::metadata(socket_path)?.permissions();
65            perms.set_mode(0o600);
66            std::fs::set_permissions(socket_path, perms)?;
67        }
68
69        info!(path = %socket_path.display(), "Unix IPC server listening");
70
71        Ok(Self {
72            inner: Arc::new(UnixIpcTransportInner {
73                socket_path: socket_path.to_string_lossy().to_string(),
74                capacity,
75                listener: Mutex::new(Some(listener)),
76                stream: Mutex::new(None),
77                closed: Mutex::new(false),
78            }),
79        })
80    }
81
82    /// Create a new Unix IPC transport as a client (publisher side)
83    pub async fn new_client(socket_path: impl AsRef<Path>, capacity: usize) -> Result<Self> {
84        let socket_path = socket_path.as_ref();
85
86        let stream = UnixStream::connect(socket_path).await?;
87
88        info!(path = %socket_path.display(), "Unix IPC client connected");
89
90        Ok(Self {
91            inner: Arc::new(UnixIpcTransportInner {
92                socket_path: socket_path.to_string_lossy().to_string(),
93                capacity,
94                listener: Mutex::new(None),
95                stream: Mutex::new(Some(stream)),
96                closed: Mutex::new(false),
97            }),
98        })
99    }
100
101    /// Send a length-prefixed frame
102    async fn send_frame(stream: &mut UnixStream, data: &[u8]) -> Result<()> {
103        let len = data.len() as u32;
104        stream.write_all(&len.to_be_bytes()).await?;
105        stream.write_all(data).await?;
106        stream.flush().await?;
107        Ok(())
108    }
109
110    /// Receive a length-prefixed frame
111    async fn recv_frame(stream: &mut UnixStream) -> Result<Vec<u8>> {
112        let mut len_bytes = [0u8; 4];
113        stream.read_exact(&mut len_bytes).await?;
114        let len = u32::from_be_bytes(len_bytes) as usize;
115
116        // Sanity check: limit frame size to 100MB
117        if len > 100 * 1024 * 1024 {
118            return Err(anyhow!("Frame too large: {} bytes", len));
119        }
120
121        let mut data = vec![0u8; len];
122        stream.read_exact(&mut data).await?;
123        Ok(data)
124    }
125
126    /// Accept a connection (server mode)
127    async fn accept_connection(&self) -> Result<UnixStream> {
128        let listener_guard = self.inner.listener.lock().await;
129        if let Some(listener) = listener_guard.as_ref() {
130            let (stream, _addr) = listener.accept().await?;
131            debug!(path = %self.inner.socket_path, "Accepted Unix IPC connection");
132            Ok(stream)
133        } else {
134            Err(anyhow!("Unix IPC transport not in server mode"))
135        }
136    }
137
138    fn is_disconnected(error: &anyhow::Error) -> bool {
139        error
140            .downcast_ref::<std::io::Error>()
141            .is_some_and(|io_error| {
142                matches!(
143                    io_error.kind(),
144                    std::io::ErrorKind::UnexpectedEof
145                        | std::io::ErrorKind::BrokenPipe
146                        | std::io::ErrorKind::ConnectionReset
147                        | std::io::ErrorKind::ConnectionAborted
148                        | std::io::ErrorKind::NotConnected
149                )
150            })
151    }
152}
153
154#[async_trait]
155impl TransportChannel for UnixIpcTransport {
156    async fn send_batch(&self, messages: Vec<CanonicalMessage>) -> Result<()> {
157        if *self.inner.closed.lock().await {
158            return Err(anyhow!("Unix IPC transport is closed"));
159        }
160
161        // Serialize the batch using MessagePack
162        let data = rmp_serde::to_vec(&messages)?;
163
164        let mut stream_guard = self.inner.stream.lock().await;
165        if let Some(stream) = stream_guard.as_mut() {
166            Self::send_frame(stream, &data).await?;
167            debug!(
168                path = %self.inner.socket_path,
169                count = messages.len(),
170                bytes = data.len(),
171                "Sent batch via Unix IPC"
172            );
173            Ok(())
174        } else {
175            Err(anyhow!("Unix IPC transport not in client mode"))
176        }
177    }
178
179    async fn recv_batch(&self) -> Result<Vec<CanonicalMessage>> {
180        loop {
181            if *self.inner.closed.lock().await {
182                return Err(anyhow!("Unix IPC transport is closed"));
183            }
184
185            let mut stream_guard = self.inner.stream.lock().await;
186            if stream_guard.is_none() {
187                drop(stream_guard);
188                let stream = self.accept_connection().await?;
189                stream_guard = self.inner.stream.lock().await;
190                *stream_guard = Some(stream);
191            }
192
193            let read_result = if let Some(stream) = stream_guard.as_mut() {
194                Self::recv_frame(stream).await
195            } else {
196                Err(anyhow!("Unix IPC transport has no active connection"))
197            };
198
199            match read_result {
200                Ok(data) => {
201                    let messages: Vec<CanonicalMessage> = rmp_serde::from_slice(&data)?;
202                    debug!(
203                        path = %self.inner.socket_path,
204                        count = messages.len(),
205                        bytes = data.len(),
206                        "Received batch via Unix IPC"
207                    );
208                    return Ok(messages);
209                }
210                Err(error) if Self::is_disconnected(&error) => {
211                    warn!(path = %self.inner.socket_path, error = %error, "Unix IPC peer disconnected; waiting for a new connection");
212                    *stream_guard = None;
213                }
214                Err(error) => return Err(error),
215            }
216        }
217    }
218
219    fn try_recv_batch(&self) -> Result<Option<Vec<CanonicalMessage>>> {
220        // Unix sockets don't support non-blocking try_recv in the same way
221        // This would require a more complex implementation with polling
222        // For now, return None to indicate no immediate data available
223        Ok(None)
224    }
225
226    fn len(&self) -> usize {
227        // Unix sockets don't have a concept of queued messages
228        // Return 0 as we don't buffer
229        0
230    }
231
232    fn capacity(&self) -> Option<usize> {
233        Some(self.inner.capacity)
234    }
235
236    fn is_closed(&self) -> bool {
237        // This is a blocking check, but should be fast
238        if let Ok(closed) = self.inner.closed.try_lock() {
239            *closed
240        } else {
241            false
242        }
243    }
244
245    fn close(&self) {
246        if let Ok(mut closed) = self.inner.closed.try_lock() {
247            *closed = true;
248            info!(path = %self.inner.socket_path, "Closing Unix IPC transport");
249        }
250    }
251}
252
253impl Drop for UnixIpcTransport {
254    fn drop(&mut self) {
255        // Clean up socket file if we're the server
256        if let Ok(listener_guard) = self.inner.listener.try_lock() {
257            if listener_guard.is_some() {
258                let socket_path = &self.inner.socket_path;
259                if let Err(e) = std::fs::remove_file(socket_path) {
260                    if e.kind() != std::io::ErrorKind::NotFound {
261                        warn!(path = %socket_path, error = %e, "Failed to remove Unix socket file");
262                    }
263                }
264            }
265        }
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use tempfile::TempDir;
273
274    #[tokio::test]
275    async fn test_unix_ipc_roundtrip() {
276        let temp_dir = TempDir::new().unwrap();
277        let socket_path = temp_dir.path().join("test.sock");
278
279        // Create server
280        let server = UnixIpcTransport::new_server(&socket_path, 10)
281            .await
282            .unwrap();
283
284        // Create client
285        let client = UnixIpcTransport::new_client(&socket_path, 10)
286            .await
287            .unwrap();
288
289        // Send from client
290        let msg = CanonicalMessage::from_vec(b"test");
291        client.send_batch(vec![msg.clone()]).await.unwrap();
292
293        // Receive on server
294        let received = server.recv_batch().await.unwrap();
295        assert_eq!(received.len(), 1);
296    }
297
298    #[tokio::test]
299    async fn test_unix_ipc_close() {
300        let temp_dir = TempDir::new().unwrap();
301        let socket_path = temp_dir.path().join("test.sock");
302
303        let server = UnixIpcTransport::new_server(&socket_path, 10)
304            .await
305            .unwrap();
306
307        assert!(!server.is_closed());
308        server.close();
309        assert!(server.is_closed());
310    }
311}
312
313// Made with Bob