1use crate::message::MessageContext;
11use core_types::{DaemonId, EventKind, SecurityLevel};
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15use std::time::{Duration, Instant};
16use tokio::net::UnixStream;
17use tokio::sync::{Mutex, mpsc, oneshot};
18use uuid::Uuid;
19
20use crate::framing::{decode_frame, encode_frame};
21use crate::message::Message;
22use crate::transport::{extract_ucred, local_credentials};
23
24pub struct RetryConfig {
26 pub max_attempts: u32,
28 pub backoff: Duration,
30}
31
32#[derive(Clone)]
34struct RotationConfig {
35 daemon_name: String,
36 daemon_id: DaemonId,
37 socket_path: PathBuf,
38 server_pub: [u8; 32],
39 capabilities: Vec<String>,
40 version: String,
41}
42
43pub struct BusClient {
45 daemon_id: DaemonId,
46 msg_ctx: MessageContext,
47 outbound_tx: mpsc::Sender<Vec<u8>>,
49 inbound_rx: mpsc::Receiver<Vec<u8>>,
51 pending: Arc<Mutex<HashMap<Uuid, oneshot::Sender<Message<EventKind>>>>>,
53 epoch: Instant,
54 io_handle: Option<tokio::task::JoinHandle<()>>,
57}
58
59impl BusClient {
60 #[must_use]
62 pub fn new(
63 daemon_id: DaemonId,
64 outbound_tx: mpsc::Sender<Vec<u8>>,
65 inbound_rx: mpsc::Receiver<Vec<u8>>,
66 ) -> Self {
67 Self {
68 daemon_id,
69 msg_ctx: MessageContext::new(daemon_id),
70 outbound_tx,
71 inbound_rx,
72 pending: Arc::new(Mutex::new(HashMap::new())),
73 epoch: Instant::now(),
74 io_handle: None,
75 }
76 }
77
78 pub async fn connect_encrypted(
88 daemon_id: DaemonId,
89 path: &Path,
90 server_public_key: &[u8; 32],
91 client_keypair: &snow::Keypair,
92 ) -> core_types::Result<Self> {
93 let stream = connect_with_retry(path, 3, Duration::from_millis(100)).await?;
94 let server_creds = extract_ucred(&stream)?;
95 let local_creds = local_credentials();
96
97 let (reader, writer) = stream.into_split();
98 let mut reader = tokio::io::BufReader::new(reader);
99 let mut writer = tokio::io::BufWriter::new(writer);
100
101 let transport = crate::noise::client_handshake(
102 &mut reader,
103 &mut writer,
104 server_public_key,
105 client_keypair,
106 &local_creds,
107 &server_creds,
108 )
109 .await?;
110
111 let (outbound_tx, mut outbound_rx) = mpsc::channel::<Vec<u8>>(256);
112 let (inbound_tx, inbound_rx) = mpsc::channel::<Vec<u8>>(1024);
113 let pending: Arc<Mutex<HashMap<Uuid, oneshot::Sender<Message<EventKind>>>>> =
114 Arc::new(Mutex::new(HashMap::new()));
115
116 let pending_clone = Arc::clone(&pending);
117 let io_handle = tokio::spawn(async move {
118 let mut transport = transport;
119 let mut reader = reader;
120 let mut writer = writer;
121 loop {
122 tokio::select! {
123 result = transport.read_encrypted_frame(&mut reader) => {
124 if let Ok(payload) = result {
125 route_inbound(payload, &pending_clone, &inbound_tx).await;
126 } else {
127 tracing::info!("server disconnected (encrypted)");
128 break;
129 }
130 }
131 msg = outbound_rx.recv() => {
132 if let Some(mut payload) = msg {
133 let result = transport.write_encrypted_frame(&mut writer, &payload).await;
134 zeroize::Zeroize::zeroize(&mut payload);
135 if let Err(e) = result {
136 tracing::debug!(error = %e, "encrypted write failed, closing client");
137 break;
138 }
139 } else {
140 tracing::debug!("outbound channel closed, I/O task exiting");
141 break;
142 }
143 }
144 }
145 }
146 });
147
148 Ok(Self {
149 daemon_id,
150 msg_ctx: MessageContext::new(daemon_id),
151 outbound_tx,
152 inbound_rx,
153 pending,
154 epoch: Instant::now(),
155 io_handle: Some(io_handle),
156 })
157 }
158
159 async fn connect_as_daemon(
174 config: RotationConfig,
175 client_keypair: &snow::Keypair,
176 ) -> core_types::Result<Self> {
177 let stream = connect_with_retry(&config.socket_path, 3, Duration::from_millis(100)).await?;
178 let server_creds = extract_ucred(&stream)?;
179 let local_creds = local_credentials();
180
181 let (reader, writer) = stream.into_split();
182 let mut reader = tokio::io::BufReader::new(reader);
183 let mut writer = tokio::io::BufWriter::new(writer);
184
185 let transport = crate::noise::client_handshake(
186 &mut reader,
187 &mut writer,
188 &config.server_pub,
189 client_keypair,
190 &local_creds,
191 &server_creds,
192 )
193 .await?;
194
195 let daemon_id = config.daemon_id;
196 let (outbound_tx, mut outbound_rx) = mpsc::channel::<Vec<u8>>(256);
197 let (inbound_tx, inbound_rx) = mpsc::channel::<Vec<u8>>(1024);
198 let pending: Arc<Mutex<HashMap<Uuid, oneshot::Sender<Message<EventKind>>>>> =
199 Arc::new(Mutex::new(HashMap::new()));
200
201 let pending_clone = Arc::clone(&pending);
202 let io_handle = tokio::spawn(async move {
203 let mut transport = transport;
204 let mut reader = reader;
205 let mut writer = writer;
206 loop {
207 tokio::select! {
208 result = transport.read_encrypted_frame(&mut reader) => {
209 if let Ok(payload) = result {
210 if let RotationResult::NotRotation = try_handle_rotation(
211 &payload, &config, &mut transport, &mut reader, &mut writer,
212 ).await {
213 route_inbound(payload, &pending_clone, &inbound_tx).await;
214 }
215 } else {
216 tracing::error!("server disconnected (encrypted)");
217 break;
218 }
219 }
220 msg = outbound_rx.recv() => {
221 if let Some(mut payload) = msg {
222 let result = transport.write_encrypted_frame(&mut writer, &payload).await;
223 zeroize::Zeroize::zeroize(&mut payload);
224 if let Err(e) = result {
225 tracing::debug!(error = %e, "encrypted write failed, closing client");
226 break;
227 }
228 } else {
229 tracing::debug!("outbound channel closed, I/O task exiting");
230 break;
231 }
232 }
233 }
234 }
235 });
236
237 Ok(Self {
238 daemon_id,
239 msg_ctx: MessageContext::new(daemon_id),
240 outbound_tx,
241 inbound_rx,
242 pending,
243 epoch: Instant::now(),
244 io_handle: Some(io_handle),
245 })
246 }
247
248 pub async fn send(&self, msg: &Message<EventKind>) -> core_types::Result<()> {
254 let payload = encode_frame(msg)?;
255 self.outbound_tx
256 .send(payload)
257 .await
258 .map_err(|_| core_types::Error::Ipc("outbound channel closed".into()))?;
259 Ok(())
260 }
261
262 pub async fn publish(
268 &self,
269 event: EventKind,
270 security_level: SecurityLevel,
271 ) -> core_types::Result<()> {
272 let msg = Message::new(&self.msg_ctx, event, security_level, self.epoch);
273 self.send(&msg).await
274 }
275
276 pub async fn request(
282 &self,
283 event: EventKind,
284 security_level: SecurityLevel,
285 timeout: Duration,
286 ) -> core_types::Result<Message<EventKind>> {
287 let msg = Message::new(&self.msg_ctx, event, security_level, self.epoch);
288 let msg_id = msg.msg_id;
289
290 let (tx, rx) = oneshot::channel();
291 self.pending.lock().await.insert(msg_id, tx);
292
293 self.send(&msg).await?;
294
295 match tokio::time::timeout(timeout, rx).await {
296 Ok(Ok(response)) => Ok(response),
297 Ok(Err(_)) => {
298 self.pending.lock().await.remove(&msg_id);
299 Err(core_types::Error::Ipc("response channel dropped".into()))
300 }
301 Err(_) => {
302 self.pending.lock().await.remove(&msg_id);
303 Err(core_types::Error::Ipc(format!(
304 "request timed out after {}ms",
305 timeout.as_millis()
306 )))
307 }
308 }
309 }
310
311 pub async fn recv(&mut self) -> Option<Message<EventKind>> {
315 let mut payload = self.inbound_rx.recv().await?;
316 let result = decode_frame(&payload);
317 zeroize::Zeroize::zeroize(&mut payload);
319 match result {
320 Ok(msg) => Some(msg),
321 Err(e) => {
322 tracing::warn!(error = %e, "failed to decode inbound frame");
323 None
324 }
325 }
326 }
327
328 #[must_use]
330 pub fn daemon_id(&self) -> DaemonId {
331 self.daemon_id
332 }
333
334 #[must_use]
336 pub fn epoch(&self) -> Instant {
337 self.epoch
338 }
339
340 pub fn set_installation(&mut self, installation: core_types::InstallationId) {
342 self.msg_ctx.installation = Some(installation);
343 }
344
345 pub async fn shutdown(self) {
347 drop(self.outbound_tx);
348 if let Some(handle) = self.io_handle {
349 let _ = handle.await;
350 }
351 }
352
353 pub async fn connect_with_keypair_retry(
363 daemon_name: &str,
364 daemon_id: DaemonId,
365 socket_path: &Path,
366 server_pub: &[u8; 32],
367 max_attempts: u32,
368 backoff: Duration,
369 ) -> core_types::Result<(Self, crate::noise::ZeroizingKeypair)> {
370 let mut last_err = None;
371 for attempt in 1..=max_attempts {
372 let (private_key, public_key) =
373 match crate::noise::read_daemon_keypair(daemon_name).await {
374 Ok(kp) => kp,
375 Err(e) => {
376 tracing::warn!(attempt, error = %e, "keypair read failed, retrying");
377 last_err = Some(e);
378 if attempt < max_attempts {
379 tokio::time::sleep(backoff * attempt).await;
380 }
381 continue;
382 }
383 };
384 let client_keypair = crate::noise::ZeroizingKeypair::new(snow::Keypair {
385 private: private_key.to_vec(),
386 public: public_key.to_vec(),
387 });
388
389 match Self::connect_encrypted(
390 daemon_id,
391 socket_path,
392 server_pub,
393 client_keypair.as_inner(),
394 )
395 .await
396 {
397 Ok(client) => {
398 return Ok((client, client_keypair));
399 }
400 Err(e) => {
401 tracing::warn!(attempt, error = %e, "IPC connect failed, retrying");
402 last_err = Some(e);
403 if attempt < max_attempts {
404 tokio::time::sleep(backoff * attempt).await;
405 }
406 }
407 }
408 }
409 Err(last_err.unwrap_or_else(|| {
410 core_types::Error::Ipc(format!("connect failed after {max_attempts} attempts"))
411 }))
412 }
413
414 pub async fn connect_daemon_with_keypair_retry(
427 daemon_name: &str,
428 daemon_id: DaemonId,
429 socket_path: &Path,
430 server_pub: &[u8; 32],
431 capabilities: Vec<String>,
432 version: &str,
433 retry: RetryConfig,
434 ) -> core_types::Result<Self> {
435 let config = RotationConfig {
436 daemon_name: daemon_name.to_owned(),
437 daemon_id,
438 socket_path: socket_path.to_owned(),
439 server_pub: *server_pub,
440 capabilities,
441 version: version.to_owned(),
442 };
443
444 let mut last_err = None;
445 for attempt in 1..=retry.max_attempts {
446 let (private_key, public_key) =
447 match crate::noise::read_daemon_keypair(daemon_name).await {
448 Ok(kp) => kp,
449 Err(e) => {
450 tracing::warn!(attempt, error = %e, "keypair read failed, retrying");
451 last_err = Some(e);
452 if attempt < retry.max_attempts {
453 tokio::time::sleep(retry.backoff * attempt).await;
454 }
455 continue;
456 }
457 };
458 let client_keypair = crate::noise::ZeroizingKeypair::new(snow::Keypair {
459 private: private_key.to_vec(),
460 public: public_key.to_vec(),
461 });
462
463 match Self::connect_as_daemon(config.clone(), client_keypair.as_inner()).await {
464 Ok(client) => return Ok(client),
465 Err(e) => {
466 tracing::warn!(attempt, error = %e, "IPC connect failed, retrying");
467 last_err = Some(e);
468 if attempt < retry.max_attempts {
469 tokio::time::sleep(retry.backoff * attempt).await;
470 }
471 }
472 }
473 }
474 Err(last_err.unwrap_or_else(|| {
475 core_types::Error::Ipc(format!(
476 "connect failed after {} attempts",
477 retry.max_attempts
478 ))
479 }))
480 }
481}
482
483enum RotationResult {
485 Handled,
488 NotRotation,
490}
491
492#[allow(clippy::similar_names)] async fn try_handle_rotation(
498 payload: &[u8],
499 config: &RotationConfig,
500 transport: &mut crate::noise::NoiseTransport,
501 reader: &mut tokio::io::BufReader<tokio::net::unix::OwnedReadHalf>,
502 writer: &mut tokio::io::BufWriter<tokio::net::unix::OwnedWriteHalf>,
503) -> RotationResult {
504 let msg: Message<EventKind> = match decode_frame(payload) {
506 Ok(m) => m,
507 Err(_) => return RotationResult::NotRotation,
508 };
509
510 let (announced_name, announced_pubkey) = match &msg.payload {
511 EventKind::KeyRotationPending {
512 daemon_name,
513 new_pubkey,
514 ..
515 } => (daemon_name.as_str(), new_pubkey),
516 _ => return RotationResult::NotRotation,
517 };
518
519 if announced_name != config.daemon_name {
520 return RotationResult::NotRotation;
521 }
522
523 tracing::info!(
524 daemon = %config.daemon_name,
525 "key rotation: reading new keypair and reconnecting"
526 );
527
528 match perform_rotation(config, announced_pubkey, transport, reader, writer).await {
531 Ok(()) => RotationResult::Handled,
532 Err(e) => {
533 tracing::error!(
534 daemon = %config.daemon_name,
535 error = %e,
536 "key rotation failed, continuing on current connection"
537 );
538 RotationResult::NotRotation
539 }
540 }
541}
542
543async fn perform_rotation(
546 config: &RotationConfig,
547 announced_pubkey: &[u8; 32],
548 transport: &mut crate::noise::NoiseTransport,
549 reader: &mut tokio::io::BufReader<tokio::net::unix::OwnedReadHalf>,
550 writer: &mut tokio::io::BufWriter<tokio::net::unix::OwnedWriteHalf>,
551) -> core_types::Result<()> {
552 let (new_private, new_public) = crate::noise::read_daemon_keypair(&config.daemon_name).await?;
553
554 if new_public != *announced_pubkey {
555 return Err(core_types::Error::Ipc(
556 "rotated pubkey mismatch: disk vs announced — possible tampering".into(),
557 ));
558 }
559
560 let kp = crate::noise::ZeroizingKeypair::new(snow::Keypair {
561 private: new_private.to_vec(),
562 public: new_public.to_vec(),
563 });
564
565 let new_stream = connect_with_retry(&config.socket_path, 3, Duration::from_millis(100)).await?;
566 let server_creds = extract_ucred(&new_stream)?;
567 let local_creds = local_credentials();
568
569 let (new_read_half, new_write_half) = new_stream.into_split();
570 let mut new_reader = tokio::io::BufReader::new(new_read_half);
571 let mut new_writer = tokio::io::BufWriter::new(new_write_half);
572
573 let mut new_transport = crate::noise::client_handshake(
574 &mut new_reader,
575 &mut new_writer,
576 &config.server_pub,
577 kp.as_inner(),
578 &local_creds,
579 &server_creds,
580 )
581 .await?;
582 let msg_ctx = MessageContext::new(config.daemon_id);
588 let announce = Message::new(
589 &msg_ctx,
590 EventKind::DaemonStarted {
591 daemon_id: config.daemon_id,
592 version: config.version.clone(),
593 capabilities: config.capabilities.clone(),
594 },
595 SecurityLevel::Internal,
596 Instant::now(),
597 );
598 let mut announce_bytes = encode_frame(&announce)?;
599 new_transport
600 .write_encrypted_frame(&mut new_writer, &announce_bytes)
601 .await?;
602 zeroize::Zeroize::zeroize(&mut announce_bytes);
603
604 *transport = new_transport;
607 *reader = new_reader;
608 *writer = new_writer;
609
610 tracing::info!(
611 daemon = %config.daemon_name,
612 "key rotation: transport swapped to new connection"
613 );
614
615 Ok(())
616}
617
618async fn route_inbound(
620 payload: Vec<u8>,
621 pending: &Mutex<HashMap<Uuid, oneshot::Sender<Message<EventKind>>>>,
622 inbound_tx: &mpsc::Sender<Vec<u8>>,
623) {
624 match decode_frame::<Message<EventKind>>(&payload) {
625 Ok(msg) => {
626 if let Some(corr_id) = msg.correlation_id {
627 let waiter = pending.lock().await.remove(&corr_id);
628 if let Some(tx) = waiter {
629 let _ = tx.send(msg);
630 return;
631 }
632 }
633 if inbound_tx.try_send(payload).is_err() {
634 tracing::warn!("inbound channel full, frame dropped");
635 }
636 }
637 Err(e) => {
638 tracing::warn!(error = %e, "failed to decode inbound frame");
639 }
640 }
641}
642
643async fn connect_with_retry(
645 path: &Path,
646 max_attempts: u32,
647 backoff: Duration,
648) -> core_types::Result<UnixStream> {
649 let mut last_err = None;
650 for attempt in 1..=max_attempts {
651 match UnixStream::connect(path).await {
652 Ok(stream) => return Ok(stream),
653 Err(e) => {
654 tracing::debug!(
655 attempt,
656 max_attempts,
657 path = %path.display(),
658 error = %e,
659 "connection attempt failed"
660 );
661 last_err = Some(e);
662 if attempt < max_attempts {
663 tokio::time::sleep(backoff).await;
664 }
665 }
666 }
667 }
668 Err(core_types::Error::Ipc(format!(
669 "failed to connect to {} after {max_attempts} attempts: {}",
670 path.display(),
671 last_err.map_or_else(|| "unknown error".to_string(), |e| e.to_string())
672 )))
673}