1use std::collections::HashMap;
9use std::io::{self, Read, Write};
10use std::sync::Arc;
11use std::thread;
12
13use interprocess::local_socket::traits::Listener;
14use prost::Message;
15
16use crate::broker::protocol::{
17 hello_reply::Result as HelloReplyResult, read_frame_with_cap, write_frame, ErrorCode, Frame,
18 FrameKind, FramingError, HelloReply, PayloadEncoding, Refused, CONTROL_PAYLOAD_PROTOCOL,
19 MAX_HELLO_BYTES, PROTOCOL_VERSION,
20};
21use crate::broker::server::deadline_stream::{hello_read_deadline, with_nonblocking_deadline};
22use crate::broker::server::{HelloHandler, HelloRouter, PeerIdentity};
23
24#[derive(Clone, Debug, PartialEq, Eq)]
26pub enum PeerCredentialPolicy {
27 AllowAny,
29 OwnerOnly {
31 uid_or_sid: String,
33 },
34}
35
36impl PeerCredentialPolicy {
37 pub fn allow_any() -> Self {
39 Self::AllowAny
40 }
41
42 pub fn owner_only(uid_or_sid: impl Into<String>) -> Self {
44 Self::OwnerOnly {
45 uid_or_sid: uid_or_sid.into(),
46 }
47 }
48
49 pub fn current_user() -> Option<Self> {
51 #[cfg(unix)]
52 {
53 Some(Self::owner_only(unsafe { libc::geteuid() }.to_string()))
54 }
55
56 #[cfg(windows)]
57 {
58 current_process_user_sid().ok().map(Self::owner_only)
59 }
60 }
61
62 pub fn allows(&self, peer: &PeerIdentity) -> bool {
64 match self {
65 Self::AllowAny => true,
66 Self::OwnerOnly { uid_or_sid } => {
67 !uid_or_sid.is_empty() && peer.uid_or_sid == *uid_or_sid
68 }
69 }
70 }
71}
72
73pub trait HelloResponder {
79 fn handle_frame(&self, frame: Frame, peer: PeerIdentity) -> HelloReply;
81}
82
83impl HelloResponder for HelloHandler {
84 fn handle_frame(&self, frame: Frame, peer: PeerIdentity) -> HelloReply {
85 Self::handle_frame(self, frame, peer)
86 }
87}
88
89impl HelloResponder for HelloRouter<'_> {
90 fn handle_frame(&self, frame: Frame, peer: PeerIdentity) -> HelloReply {
91 Self::handle_frame(self, frame, peer)
92 }
93}
94
95pub fn handle_hello_connection<S: Read + Write>(
101 stream: &mut S,
102 handler: &HelloHandler,
103 peer: PeerIdentity,
104) -> Result<HelloReply, BrokerConnectionError> {
105 handle_hello_connection_with(stream, handler, peer)
106}
107
108pub fn handle_hello_connection_with<S, R>(
113 stream: &mut S,
114 responder: &R,
115 peer: PeerIdentity,
116) -> Result<HelloReply, BrokerConnectionError>
117where
118 S: Read + Write,
119 R: HelloResponder + ?Sized,
120{
121 handle_hello_connection_with_peer_policy(
122 stream,
123 responder,
124 peer,
125 &PeerCredentialPolicy::allow_any(),
126 )
127 .map(|reply| reply.expect("allow-any policy must not drop peers"))
128}
129
130pub fn handle_hello_connection_with_peer_policy<S, R>(
136 stream: &mut S,
137 responder: &R,
138 peer: PeerIdentity,
139 peer_policy: &PeerCredentialPolicy,
140) -> Result<Option<HelloReply>, BrokerConnectionError>
141where
142 S: Read + Write,
143 R: HelloResponder + ?Sized,
144{
145 if !peer_policy.allows(&peer) {
146 return Ok(None);
147 }
148
149 let request_bytes = match read_frame_with_cap(stream, MAX_HELLO_BYTES) {
150 Ok(bytes) => bytes,
151 Err(err) => {
152 let reply = reply_for_framing_error(&err);
153 write_response_frame(stream, None, &reply)?;
154 return Ok(Some(reply));
155 }
156 };
157
158 let request_frame = match Frame::decode(request_bytes.as_slice()) {
159 Ok(frame) => frame,
160 Err(_) => {
161 let reply = refused_reply(ErrorCode::ErrorPeerRejected, "malformed broker Frame", 0);
162 write_response_frame(stream, None, &reply)?;
163 return Ok(Some(reply));
164 }
165 };
166
167 let reply = responder.handle_frame(request_frame.clone(), peer);
168 write_response_frame(stream, Some(&request_frame), &reply)?;
169 Ok(Some(reply))
170}
171
172pub fn serve_one_local_socket(
179 socket_path: &str,
180 handler: &HelloHandler,
181) -> Result<HelloReply, BrokerConnectionError> {
182 serve_one_local_socket_with(socket_path, handler)
183}
184
185pub fn serve_one_local_socket_with<R>(
188 socket_path: &str,
189 responder: &R,
190) -> Result<HelloReply, BrokerConnectionError>
191where
192 R: HelloResponder + ?Sized,
193{
194 serve_one_local_socket_with_peer_policy(
195 socket_path,
196 responder,
197 &PeerCredentialPolicy::allow_any(),
198 )
199 .map(|reply| reply.expect("allow-any policy must not drop peers"))
200}
201
202pub fn serve_one_local_socket_with_peer_policy<R>(
204 socket_path: &str,
205 responder: &R,
206 peer_policy: &PeerCredentialPolicy,
207) -> Result<Option<HelloReply>, BrokerConnectionError>
208where
209 R: HelloResponder + ?Sized,
210{
211 let listener = bind_local_socket(socket_path)?;
212 let cleanup = LocalSocketCleanup(socket_path);
213 let result = (|| {
214 let mut stream = listener.accept()?;
215 let peer = peer_identity_from_stream(&stream)?;
216 with_nonblocking_deadline(&mut stream, hello_read_deadline(), |stream| {
217 handle_hello_connection_with_peer_policy(stream, responder, peer, peer_policy)
218 })
219 })();
220 drop(listener);
221 drop(cleanup);
222 result
223}
224
225pub fn serve_local_socket_connections(
231 socket_path: &str,
232 handler: Arc<HelloHandler>,
233 connection_count: usize,
234) -> Result<(), BrokerConnectionError> {
235 serve_local_socket_connections_with_peer_policy(
236 socket_path,
237 handler,
238 connection_count,
239 &PeerCredentialPolicy::allow_any(),
240 )
241}
242
243pub fn serve_local_socket_connections_with_peer_policy(
245 socket_path: &str,
246 handler: Arc<HelloHandler>,
247 connection_count: usize,
248 peer_policy: &PeerCredentialPolicy,
249) -> Result<(), BrokerConnectionError> {
250 if connection_count == 0 {
251 return Ok(());
252 }
253
254 let listener = bind_local_socket(socket_path)?;
255 let cleanup = LocalSocketCleanup(socket_path);
256 let result = (|| {
257 let mut workers = Vec::with_capacity(connection_count);
258 let peer_policy = Arc::new(peer_policy.clone());
259
260 for _ in 0..connection_count {
261 let mut stream = listener.accept()?;
262 let peer = peer_identity_from_stream(&stream)?;
263 let handler = Arc::clone(&handler);
264 let peer_policy = Arc::clone(&peer_policy);
265 workers.push(thread::spawn(move || {
266 with_nonblocking_deadline(&mut stream, hello_read_deadline(), |stream| {
267 handle_hello_connection_with_peer_policy(
268 stream,
269 handler.as_ref(),
270 peer,
271 peer_policy.as_ref(),
272 )
273 .map(|_| ())
274 })
275 }));
276 }
277
278 for worker in workers {
279 match worker.join() {
280 Ok(Ok(())) => {}
281 Ok(Err(err)) => return Err(err),
282 Err(_) => return Err(BrokerConnectionError::WorkerPanic),
283 }
284 }
285 Ok(())
286 })();
287 drop(listener);
288 drop(cleanup);
289 result
290}
291
292pub fn serve_local_socket_connections_with<R>(
298 socket_path: &str,
299 responder: &R,
300 connection_count: usize,
301) -> Result<(), BrokerConnectionError>
302where
303 R: HelloResponder + ?Sized,
304{
305 serve_local_socket_connections_with_policy(
306 socket_path,
307 responder,
308 connection_count,
309 &PeerCredentialPolicy::allow_any(),
310 )
311}
312
313pub fn serve_local_socket_connections_with_policy<R>(
315 socket_path: &str,
316 responder: &R,
317 connection_count: usize,
318 peer_policy: &PeerCredentialPolicy,
319) -> Result<(), BrokerConnectionError>
320where
321 R: HelloResponder + ?Sized,
322{
323 if connection_count == 0 {
324 return Ok(());
325 }
326
327 let listener = bind_local_socket(socket_path)?;
328 let cleanup = LocalSocketCleanup(socket_path);
329 let result = (|| {
330 for _ in 0..connection_count {
331 let mut stream = listener.accept()?;
332 let peer = peer_identity_from_stream(&stream)?;
333 let _ = with_nonblocking_deadline(&mut stream, hello_read_deadline(), |stream| {
334 handle_hello_connection_with_peer_policy(stream, responder, peer, peer_policy)
335 })?;
336 }
337 Ok(())
338 })();
339 drop(listener);
340 drop(cleanup);
341 result
342}
343
344pub fn local_socket_name(socket_path: &str) -> io::Result<interprocess::local_socket::Name<'_>> {
347 #[cfg(unix)]
348 {
349 use interprocess::local_socket::{GenericFilePath, ToFsName};
350 socket_path.to_fs_name::<GenericFilePath>()
351 }
352
353 #[cfg(windows)]
354 {
355 use interprocess::local_socket::{GenericNamespaced, ToNsName};
356 socket_path.to_ns_name::<GenericNamespaced>()
357 }
358}
359
360#[derive(Debug, thiserror::Error)]
362pub enum BrokerConnectionError {
363 #[error(transparent)]
365 Framing(#[from] FramingError),
366 #[error("failed to encode broker response Frame: {0}")]
368 EncodeFrame(prost::EncodeError),
369 #[error(transparent)]
371 Io(#[from] io::Error),
372 #[error("broker connection worker panicked")]
374 WorkerPanic,
375}
376
377pub(super) fn bind_local_socket(
378 socket_path: &str,
379) -> Result<interprocess::local_socket::Listener, BrokerConnectionError> {
380 use interprocess::local_socket::ListenerOptions;
381
382 prepare_local_socket_path(socket_path)?;
383 let name = local_socket_name(socket_path)?;
384 let listener = ListenerOptions::new().name(name).create_sync()?;
385 secure_local_socket_path(socket_path)?;
386 Ok(listener)
387}
388
389pub(super) struct LocalSocketCleanup<'a>(pub(super) &'a str);
390
391impl Drop for LocalSocketCleanup<'_> {
392 fn drop(&mut self) {
393 cleanup_local_socket_path(self.0);
394 }
395}
396
397pub(super) fn write_response_frame<W: Write>(
398 writer: &mut W,
399 request_frame: Option<&Frame>,
400 reply: &HelloReply,
401) -> Result<(), BrokerConnectionError> {
402 let response_frame = Frame {
403 envelope_version: PROTOCOL_VERSION,
404 kind: FrameKind::Response as i32,
405 payload_protocol: CONTROL_PAYLOAD_PROTOCOL,
406 payload: reply.encode_to_vec(),
407 request_id: request_frame.map_or(0, |frame| frame.request_id),
408 payload_encoding: PayloadEncoding::None as i32,
409 deadline_unix_ms: 0,
410 traceparent: request_frame
411 .map(|frame| frame.traceparent.clone())
412 .unwrap_or_default(),
413 tracestate: request_frame
414 .map(|frame| frame.tracestate.clone())
415 .unwrap_or_default(),
416 };
417 let mut response_bytes = Vec::new();
418 response_frame
419 .encode(&mut response_bytes)
420 .map_err(BrokerConnectionError::EncodeFrame)?;
421 write_frame(writer, &response_bytes)?;
422 Ok(())
423}
424
425pub(super) fn reply_for_framing_error(error: &FramingError) -> HelloReply {
426 match error {
427 FramingError::UnsupportedFramingVersion { .. } => refused_reply(
428 ErrorCode::ErrorVersionUnsupported,
429 "unsupported framing version",
430 0,
431 ),
432 FramingError::FrameTooLarge { .. } => refused_reply(
433 ErrorCode::ErrorPeerRejected,
434 "initial Hello frame exceeds 64 KiB",
435 0,
436 ),
437 FramingError::UnexpectedEof { .. } | FramingError::Io(_) => {
438 refused_reply(ErrorCode::ErrorPeerRejected, "incomplete Hello frame", 0)
439 }
440 FramingError::Decode(_) => {
443 refused_reply(ErrorCode::ErrorPeerRejected, "malformed Hello frame", 0)
444 }
445 }
446}
447
448pub(super) fn refused_reply(
449 code: ErrorCode,
450 reason: impl Into<String>,
451 retry_after_ms: u64,
452) -> HelloReply {
453 HelloReply {
454 result: Some(HelloReplyResult::Refused(Refused {
455 reason: reason.into(),
456 daemon_min_protocol: PROTOCOL_VERSION,
457 daemon_max_protocol: PROTOCOL_VERSION,
458 code: code as i32,
459 details: HashMap::new(),
460 retry_after_ms,
461 })),
462 }
463}
464
465pub(super) fn peer_identity_from_stream(
466 stream: &interprocess::local_socket::Stream,
467) -> Result<PeerIdentity, BrokerConnectionError> {
468 use interprocess::local_socket::traits::StreamCommon;
469
470 let creds = stream.peer_creds()?;
471 #[cfg(unix)]
472 let pid = creds
473 .pid()
474 .and_then(|pid| u32::try_from(pid).ok())
475 .unwrap_or(0);
476
477 #[cfg(windows)]
478 let pid = creds.pid().unwrap_or(0);
479
480 #[cfg(unix)]
481 let uid_or_sid = creds.euid().map(|uid| uid.to_string()).unwrap_or_default();
482
483 #[cfg(windows)]
484 let uid_or_sid = if pid == 0 {
485 String::new()
486 } else {
487 process_user_sid(pid).unwrap_or_default()
488 };
489
490 Ok(PeerIdentity { pid, uid_or_sid })
491}
492
493#[cfg(windows)]
494fn current_process_user_sid() -> io::Result<String> {
495 process_user_sid(std::process::id())
496}
497
498#[cfg(windows)]
499fn process_user_sid(pid: u32) -> io::Result<String> {
500 use std::ptr;
501 use winapi::um::processthreadsapi::{OpenProcess, OpenProcessToken};
502 use winapi::um::winnt::{
503 TokenUser, HANDLE, PROCESS_QUERY_LIMITED_INFORMATION, TOKEN_QUERY, TOKEN_USER,
504 };
505
506 unsafe {
507 let process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
508 if process.is_null() {
509 return Err(io::Error::last_os_error());
510 }
511 let _process_guard = WindowsHandle(process);
512
513 let mut token: HANDLE = ptr::null_mut();
514 if OpenProcessToken(process, TOKEN_QUERY, &mut token) == 0 {
515 return Err(io::Error::last_os_error());
516 }
517 let _token_guard = WindowsHandle(token);
518
519 let mut required_size = 0_u32;
520 let _ = winapi::um::securitybaseapi::GetTokenInformation(
521 token,
522 TokenUser,
523 ptr::null_mut(),
524 0,
525 &mut required_size,
526 );
527 if required_size == 0 {
528 return Err(io::Error::last_os_error());
529 }
530
531 let mut buffer = vec![0_u8; required_size as usize];
532 if winapi::um::securitybaseapi::GetTokenInformation(
533 token,
534 TokenUser,
535 buffer.as_mut_ptr().cast(),
536 required_size,
537 &mut required_size,
538 ) == 0
539 {
540 return Err(io::Error::last_os_error());
541 }
542
543 let token_user: *const TOKEN_USER = buffer.as_ptr().cast();
544 let sid = (*token_user).User.Sid;
545 sid_to_stable_string(sid)
546 }
547}
548
549#[cfg(windows)]
550struct WindowsHandle(winapi::um::winnt::HANDLE);
551
552#[cfg(windows)]
553impl Drop for WindowsHandle {
554 fn drop(&mut self) {
555 unsafe {
556 winapi::um::handleapi::CloseHandle(self.0);
557 }
558 }
559}
560
561#[cfg(windows)]
562unsafe fn sid_to_stable_string(sid: winapi::um::winnt::PSID) -> io::Result<String> {
563 use winapi::um::securitybaseapi::{GetLengthSid, IsValidSid};
564
565 if sid.is_null() || IsValidSid(sid) == 0 {
566 return Err(io::Error::other("invalid Windows SID"));
567 }
568 let len = GetLengthSid(sid) as usize;
569 if len == 0 || len > 1024 {
570 return Err(io::Error::other(format!(
571 "implausible Windows SID length {len}"
572 )));
573 }
574 let bytes = std::slice::from_raw_parts(sid.cast::<u8>(), len);
575 let mut out = String::with_capacity("windows-sid:".len() + len * 2);
576 out.push_str("windows-sid:");
577 for byte in bytes {
578 out.push(nibble_to_hex(byte >> 4));
579 out.push(nibble_to_hex(byte & 0x0f));
580 }
581 Ok(out)
582}
583
584#[cfg(windows)]
585fn nibble_to_hex(nibble: u8) -> char {
586 match nibble {
587 0..=9 => (b'0' + nibble) as char,
588 10..=15 => (b'a' + (nibble - 10)) as char,
589 _ => unreachable!("nibble out of range"),
590 }
591}
592
593fn prepare_local_socket_path(socket_path: &str) -> io::Result<()> {
594 #[cfg(unix)]
595 {
596 let path = std::path::Path::new(socket_path);
597 if let Some(parent) = path.parent() {
598 std::fs::create_dir_all(parent)?;
599 }
600 match std::fs::symlink_metadata(path) {
601 Ok(_) => {
602 return Err(io::Error::new(
603 io::ErrorKind::AlreadyExists,
604 "broker local socket path already exists",
605 ));
606 }
607 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
608 Err(err) => return Err(err),
609 }
610 }
611
612 #[cfg(windows)]
613 let _ = socket_path;
614
615 Ok(())
616}
617
618fn secure_local_socket_path(socket_path: &str) -> io::Result<()> {
619 #[cfg(unix)]
620 {
621 use std::os::unix::fs::PermissionsExt;
622
623 let perms = std::fs::Permissions::from_mode(0o600);
624 std::fs::set_permissions(socket_path, perms)?;
625 }
626
627 #[cfg(windows)]
628 let _ = socket_path;
629
630 Ok(())
631}
632
633fn cleanup_local_socket_path(socket_path: &str) {
634 #[cfg(unix)]
635 {
636 let _ = std::fs::remove_file(socket_path);
637 }
638
639 #[cfg(windows)]
640 let _ = socket_path;
641}