sentinelpass_protocol/transport/
unix.rs1use super::{TransportError, TransportResult, MAX_MESSAGE_SIZE};
5use std::path::{Path, PathBuf};
6use tokio::io::{AsyncReadExt, AsyncWriteExt};
7
8pub fn ensure_private_socket_dir(socket_path: &Path, create: bool) -> TransportResult<()> {
17 use std::os::unix::fs::PermissionsExt;
18
19 let Some(dir) = socket_path.parent() else {
20 return Err(TransportError::Other(format!(
21 "socket path has no parent directory: {}",
22 socket_path.display()
23 )));
24 };
25
26 match std::fs::symlink_metadata(dir) {
27 Ok(meta) => {
28 if meta.file_type().is_symlink() {
29 return Err(TransportError::Other(format!(
30 "refusing IPC socket directory (symlink): {}",
31 dir.display()
32 )));
33 }
34 use std::os::unix::fs::MetadataExt;
35 if meta.uid() != unsafe { libc::geteuid() } {
36 return Err(TransportError::Other(format!(
37 "refusing IPC socket directory (not owned by the current user): {}",
38 dir.display()
39 )));
40 }
41 let mode = meta.permissions().mode();
42 if mode & 0o077 != 0 {
43 return Err(TransportError::Other(format!(
44 "refusing IPC socket directory (not owner-only, mode {:o}): {} \
45 — the daemon and clients only accept sockets inside a private \
46 runtime directory (0700)",
47 mode & 0o777,
48 dir.display()
49 )));
50 }
51 Ok(())
52 }
53 Err(e) if e.kind() == std::io::ErrorKind::NotFound && create => {
54 std::fs::create_dir_all(dir).map_err(TransportError::Io)?;
55 std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))
56 .map_err(TransportError::Io)?;
57 Ok(())
58 }
59 Err(e) => Err(TransportError::Io(e)),
60 }
61}
62
63pub struct UnixSocketConnection {
65 stream: tokio::net::UnixStream,
66}
67
68impl UnixSocketConnection {
69 pub fn from_stream(stream: tokio::net::UnixStream) -> Self {
71 Self { stream }
72 }
73
74 pub async fn connect(path: PathBuf) -> TransportResult<Self> {
76 ensure_private_socket_dir(&path, false)?;
78
79 let stream = tokio::net::UnixStream::connect(&path).await.map_err(|e| {
80 TransportError::ConnectionFailed(format!(
81 "Failed to connect to {}: {}",
82 path.display(),
83 e
84 ))
85 })?;
86
87 Ok(Self { stream })
88 }
89
90 pub async fn read_message(&mut self) -> TransportResult<Vec<u8>> {
92 let mut length_buf = [0u8; 4];
94 self.stream.read_exact(&mut length_buf).await?;
95
96 let length = u32::from_be_bytes(length_buf) as usize;
97
98 if length == 0 || length > MAX_MESSAGE_SIZE {
99 return Err(TransportError::MessageTooLarge {
100 size: length,
101 max: MAX_MESSAGE_SIZE,
102 });
103 }
104
105 let mut buffer = vec![0u8; length];
107 self.stream.read_exact(&mut buffer).await?;
108
109 Ok(buffer)
110 }
111
112 pub async fn write_message(&mut self, data: &[u8]) -> TransportResult<()> {
114 let length = data.len() as u32;
115
116 if length as usize > MAX_MESSAGE_SIZE {
118 return Err(TransportError::MessageTooLarge {
119 size: data.len(),
120 max: MAX_MESSAGE_SIZE,
121 });
122 }
123
124 self.stream.write_all(&length.to_be_bytes()).await?;
126
127 self.stream.write_all(data).await?;
129
130 self.stream.flush().await?;
131
132 Ok(())
133 }
134
135 pub async fn close(&mut self) -> TransportResult<()> {
137 self.stream.shutdown().await?;
138 Ok(())
139 }
140
141 pub fn is_open(&self) -> bool {
143 self.stream.peer_addr().is_ok()
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use std::time::Duration;
152
153 fn private_dir() -> PathBuf {
154 let dir = tempfile::TempDir::new().unwrap().keep();
157 {
158 use std::os::unix::fs::PermissionsExt;
159 std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).unwrap();
160 }
161 dir
162 }
163
164 #[tokio::test]
165 async fn test_unix_socket_connection_roundtrip() {
166 let dir = private_dir();
167 let socket_path = dir.join(format!("test_ipc_{}.sock", uuid_v4()));
168
169 let listener = tokio::net::UnixListener::bind(&socket_path).unwrap();
171 let server_handle = tokio::spawn(async move {
172 let (stream, _) = listener.accept().await.unwrap();
173 let mut conn = UnixSocketConnection::from_stream(stream);
174 let msg = conn.read_message().await.unwrap();
175 conn.write_message(&msg).await.unwrap();
176 conn.close().await.unwrap();
177 });
178
179 tokio::time::sleep(Duration::from_millis(100)).await;
181 let mut client = UnixSocketConnection::connect(socket_path).await.unwrap();
182
183 let test_data = b"Hello, IPC!";
185 client.write_message(test_data).await.unwrap();
186 let received = client.read_message().await.unwrap();
187
188 assert_eq!(received, test_data);
189
190 server_handle.await.unwrap();
191 }
192
193 #[tokio::test]
196 async fn client_refuses_socket_in_loose_directory() {
197 let outer = tempfile::TempDir::new().unwrap();
198 let loose = outer.path().join("loose");
199 std::fs::create_dir_all(&loose).unwrap();
200 {
201 use std::os::unix::fs::PermissionsExt;
202 std::fs::set_permissions(&loose, std::fs::Permissions::from_mode(0o755)).unwrap();
203 }
204 let socket_path = loose.join("sock");
205
206 let err = match UnixSocketConnection::connect(socket_path).await {
207 Err(err) => err,
208 Ok(_) => panic!("loose socket dir must be refused"),
209 };
210 assert!(
211 err.to_string().contains("owner-only"),
212 "refusal must name the policy: {err}"
213 );
214 }
215
216 #[tokio::test]
218 async fn client_accepts_socket_in_private_directory() {
219 let dir = private_dir();
220 let socket_path = dir.join("sock");
221 let listener = tokio::net::UnixListener::bind(&socket_path).unwrap();
222 tokio::spawn(async move {
223 let listener = listener;
224 let _ = listener.accept().await;
226 });
227 tokio::time::sleep(Duration::from_millis(50)).await;
228 let client = UnixSocketConnection::connect(socket_path).await;
229 assert!(client.is_ok(), "private-dir socket must be accepted");
230 }
231
232 fn uuid_v4() -> String {
233 let nanos = std::time::SystemTime::now()
235 .duration_since(std::time::UNIX_EPOCH)
236 .unwrap()
237 .as_nanos();
238 format!("proto{n}", n = nanos)
239 }
240}