darkbio_wire/protocol/server.rs
1// wire-rs: encrypted protocol between Ark and host
2// Copyright 2026 Dark Bio AG. All rights reserved.
3//
4// Use of this source code is governed by a BSD-style
5// license that can be found in the LICENSE file.
6
7//! Persistent server ownership and ordered attachment of successive sessions.
8
9use super::envelope::Side;
10use super::promise::Notifications;
11use super::session::SessionInner;
12use super::worker;
13use super::{
14 Closer, DEFAULT_AUTOREPLY_TIMEOUT, DEFAULT_MAX_INBOUND_BYTES, DEFAULT_MAX_INBOUND_REQUESTS,
15 Error, Session,
16};
17use crate::transport::{self, Attester, Read, Stream, Write};
18use darkbio_clock::Clock;
19use darkbio_clock::sync::{Condvar, Mutex};
20use darkbio_crypto::xdsa;
21use std::fmt;
22use std::sync::{Arc, Weak};
23use std::time::Duration;
24
25/// Owner of a persistent server stream, accepting successive sessions.
26///
27/// Closing or dropping the server ends its active session and shuts down the
28/// physical stream. Closing an individual [`Session`] keeps this owner and
29/// its stream available for another handshake.
30pub struct Server {
31 /// Server state retained independently of any accepted session owner.
32 pub(super) inner: Arc<ServerInner>,
33}
34
35impl Server {
36 /// Takes ownership of a stream and constructs its transport internally.
37 ///
38 /// The signer is the server's identity key, which must match the key embedded
39 /// in the attestation. The attester supplies the current device attestation
40 /// for each handshake. The persistent reader starts immediately. Failure to
41 /// start a required worker or an escaping worker panic aborts the process.
42 pub fn new<R, W, A>(stream: Stream<R, W>, signer: xdsa::SecretKey, attester: A) -> Self
43 where
44 R: Read + Send + 'static,
45 W: Write + Send + 'static,
46 A: Attester + Send + 'static,
47 {
48 // Retain the stream clock and shutdown handle for every accepted session
49 let stream_closer = stream.closer();
50 let server = Self {
51 inner: Arc::new(ServerInner {
52 clock: stream.clock(),
53 state: Mutex::new(State::Open {
54 max_inbound_requests: DEFAULT_MAX_INBOUND_REQUESTS,
55 max_inbound_bytes: DEFAULT_MAX_INBOUND_BYTES,
56 autoreply_timeout: DEFAULT_AUTOREPLY_TIMEOUT,
57 session: Weak::new(),
58 ready: None,
59 #[cfg(any(test, feature = "fuzz"))]
60 wait_hook: None,
61 }),
62 changed: Condvar::new(&stream.clock()),
63 stream_closer: Some(stream_closer),
64 #[cfg(any(test, feature = "fuzz"))]
65 workers: Arc::new(worker::Tracker::default()),
66 }),
67 };
68
69 // Keep accepting transport handshakes until this owner closes
70 let server_ref = Arc::downgrade(&server.inner);
71 worker::spawn(
72 "wire-server-reader",
73 #[cfg(any(test, feature = "fuzz"))]
74 &server.inner.workers,
75 move || {
76 let transport = transport::Server::new(stream, signer, attester);
77 run_reader(transport, server_ref);
78 },
79 );
80 server
81 }
82
83 /// Sets the timeout for automatic `UNANSWERED` and `UNKNOWN` replies in the
84 /// current and future sessions.
85 ///
86 /// Defaults to [`DEFAULT_AUTOREPLY_TIMEOUT`]. Applies even before
87 /// [`Self::accept`]. Replies already queued keep their deadlines.
88 ///
89 /// See [`Session::set_autoreply_timeout`] for when the timeout starts and
90 /// expires. Changing a session's timeout leaves the server's default unchanged.
91 /// This method also replaces a timeout set directly on the current session.
92 pub fn set_autoreply_timeout(self, timeout: Duration) -> Self {
93 self.inner.set_autoreply_timeout(timeout);
94 self
95 }
96
97 /// Sets both per-session inbound limits, initially
98 /// [`DEFAULT_MAX_INBOUND_REQUESTS`] and [`DEFAULT_MAX_INBOUND_BYTES`].
99 ///
100 /// Applies to the current session, even before [`Self::accept`], and future
101 /// sessions. Lowering either limit below usage closes that session. The
102 /// server stays open.
103 ///
104 /// See [`Session::set_inbound_limits`] for what each limit counts. Changing a
105 /// session's limits leaves the server's defaults unchanged. This method also
106 /// replaces limits set directly on the current session.
107 pub fn set_inbound_limits(self, requests: usize, bytes: usize) -> Self {
108 self.inner.set_inbound_limits(requests, bytes);
109 self
110 }
111
112 /// Blocks until a session is established or the server ends.
113 ///
114 /// Recoverable handshake failures leave the stream available for another
115 /// attempt. A replacement session closes the previous one, and old handles
116 /// still refer to it.
117 ///
118 /// The reader runs before acceptance. A returned session may already have
119 /// queued requests or be closed, including from exceeding an inbound limit.
120 /// If several sessions arrive before acceptance, only the newest is returned.
121 pub fn accept(&mut self) -> Result<Session, Error> {
122 let mut state = self.inner.state.lock().expect("server state not poisoned");
123 loop {
124 match &mut *state {
125 State::Closed { reason, .. } => return Err(reason.clone()),
126 State::Open {
127 ready,
128 #[cfg(any(test, feature = "fuzz"))]
129 wait_hook,
130 ..
131 } => {
132 if let Some(session) = ready.take() {
133 return Ok(session);
134 }
135 #[cfg(any(test, feature = "fuzz"))]
136 if let Some(wait_hook) = wait_hook.take() {
137 let _ = wait_hook.send(());
138 }
139 state = self
140 .inner
141 .changed
142 .wait(state)
143 .expect("server state not poisoned");
144 }
145 }
146 }
147 }
148
149 /// Returns a clonable handle for closing this server from another thread,
150 /// including while its owner is blocked in [`Self::accept`].
151 pub fn closer(&self) -> Closer {
152 Closer::server(Arc::downgrade(&self.inner))
153 }
154
155 /// Permanently closes this server and its active session.
156 ///
157 /// It wakes blocked acceptance and receive calls and fails unresolved
158 /// operations. It also closes the stream, waiting for adapter calls in
159 /// progress to return. Repeated calls have no further effect. It does not
160 /// join application jobs or guarantee the peer has observed closure.
161 pub fn close(&self) {
162 self.inner.close(Error::Closed);
163 }
164}
165
166/// Receives transport events across successive server sessions.
167///
168/// Weak references let closed sessions be freed while this reader waits for
169/// another handshake.
170fn run_reader<R: Read, W: Write + Send + 'static, A: Attester>(
171 mut transport: transport::Server<R, W, A>,
172 server_ref: Weak<ServerInner>,
173) {
174 let mut current: Weak<SessionInner> = Weak::new();
175 loop {
176 // Hold no server state across the blocking read. Dropping Server closes
177 // its stream and wakes this call.
178 let result = transport.recv();
179 let Some(server) = server_ref.upgrade() else {
180 break;
181 };
182 match result {
183 // A successful handshake gets its own session and workers
184 Ok(transport::Event::Connected(sender)) => {
185 let session = Session::start(
186 Side::Server,
187 server.clock.clone(),
188 sender,
189 None,
190 #[cfg(any(test, feature = "fuzz"))]
191 server.workers.clone(),
192 );
193 current = Arc::downgrade(&session.inner);
194 if server.attach(session).is_err() {
195 break;
196 }
197 }
198 // Disconnecting closes the session while keeping the server stream
199 Ok(transport::Event::Disconnected) => {
200 if let Some(session) = current.upgrade() {
201 session.close(transport::Error::SessionReset.into());
202 }
203 current = Weak::new();
204 }
205 // A message goes to the current session, which closes if handling fails
206 Ok(transport::Event::Message(bytes)) => {
207 if let Some(session) = current.upgrade()
208 && let Err(error) = session.handle_message(bytes)
209 {
210 session.close(error);
211 }
212 }
213 // A handshake that timed out or failed to write leaves the reader
214 // available for the next reset
215 Err(transport::Error::RecvFailed(error))
216 if error.kind() == std::io::ErrorKind::TimedOut =>
217 {
218 tracing::debug!("wire handshake timed out");
219 }
220 Err(transport::Error::SendFailed(error)) => {
221 tracing::debug!("wire handshake output failed: {}", error);
222 }
223 // Any other failure ends the server and its stream
224 Err(error) => {
225 server.close(error.into());
226 break;
227 }
228 }
229 }
230}
231
232impl Drop for Server {
233 /// Ends the server and its attached session even when handles remain.
234 fn drop(&mut self) {
235 self.close();
236 }
237}
238
239impl fmt::Debug for Server {
240 /// Shows whether the server still accepts sessions.
241 ///
242 /// A state lock held elsewhere leaves the state out.
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 let mut server = f.debug_struct("Server");
245 if let Ok(state) = self.inner.state.try_lock() {
246 server.field("open", &matches!(*state, State::Open { .. }));
247 }
248 server.finish_non_exhaustive()
249 }
250}
251
252/// Shared server state, holding at most one session waiting for acceptance.
253///
254/// The reader attaches replacements in transport order. Accepted sessions own
255/// themselves, and the server retains only a weak reference for server shutdown.
256pub(super) struct ServerInner {
257 /// Clock inherited by every session accepted on this stream.
258 clock: Clock,
259 /// Attached session, pending acceptance and server closure, under one lock.
260 state: Mutex<State>,
261 /// Signal waking [`Server::accept`] when a session is attached or the
262 /// server closes.
263 changed: Condvar,
264 /// Closer of the server's stream, empty in tests that supply sessions directly.
265 stream_closer: Option<transport::Closer>,
266 /// Tracker letting tests wait for the reader and all session workers to exit.
267 #[cfg(any(test, feature = "fuzz"))]
268 pub(super) workers: Arc<worker::Tracker>,
269}
270
271/// Server policy and the session waiting for acceptance, or the error that
272/// closed the server.
273enum State {
274 /// Open server, tracking the current session and keeping its owner until
275 /// [`Server::accept`] takes it.
276 Open {
277 /// Request ceiling applied to the attached session and future sessions.
278 max_inbound_requests: usize,
279 /// Encoded-byte ceiling applied independently to each session.
280 max_inbound_bytes: usize,
281 /// Automatic reply timeout applied to the current and future sessions.
282 autoreply_timeout: Duration,
283 /// Current session, which server closure closes even after
284 /// [`Server::accept`] returns it.
285 session: Weak<SessionInner>,
286 /// Session waiting for [`Server::accept`], replaced by a newer handshake.
287 ready: Option<Session>,
288 /// One-shot test notification sent under the server lock before waiting.
289 #[cfg(any(test, feature = "fuzz"))]
290 wait_hook: Option<std::sync::mpsc::Sender<()>>,
291 },
292 /// Closed server, with its closing error and the session attached at the time.
293 ///
294 /// Repeated [`ServerInner::close`] calls can finish closing that session if
295 /// the first closer is still doing so.
296 Closed {
297 /// First reason the server ended; later closes cannot replace it.
298 reason: Error,
299 /// Session that was attached when the server closed.
300 session: Weak<SessionInner>,
301 },
302}
303
304impl ServerInner {
305 /// Updates the current session's timeout and the default under the
306 /// attachment lock.
307 ///
308 /// Lock order is server then session, as with inbound limit updates.
309 fn set_autoreply_timeout(&self, timeout: Duration) {
310 let mut state = self.state.lock().expect("server state not poisoned");
311 if let State::Open {
312 autoreply_timeout,
313 session,
314 ..
315 } = &mut *state
316 {
317 *autoreply_timeout = timeout;
318 if let Some(session) = session.upgrade() {
319 session.set_autoreply_timeout(timeout);
320 }
321 }
322 }
323
324 /// Updates the current session's limits and the defaults under the
325 /// attachment lock.
326 ///
327 /// Lock order is server then session, and session methods never acquire
328 /// the server lock. Server sessions have no stream closer, so applying
329 /// their limits cannot wait for stream I/O.
330 fn set_inbound_limits(&self, requests: usize, bytes: usize) {
331 // Defer session callbacks until the server's policy lock is released too
332 let mut notifications = Notifications::default();
333 let mut state = self.state.lock().expect("server state not poisoned");
334 if let State::Open {
335 max_inbound_requests,
336 max_inbound_bytes,
337 session,
338 ..
339 } = &mut *state
340 {
341 *max_inbound_requests = requests;
342 *max_inbound_bytes = bytes;
343 if let Some(session) = session.upgrade() {
344 session.set_inbound_limits(requests, bytes, &mut notifications);
345 }
346 }
347 }
348
349 /// Closes the server, refusing attachment and acceptance before closing the
350 /// attached session.
351 ///
352 /// The server lock is released before closing or dropping a [`Session`],
353 /// since those operations take the session's own lock.
354 pub(super) fn close(&self, error: Error) {
355 // Stop `attach` and `accept` by switching to `Closed`. Save the attached
356 // session so repeated `close` calls can finish closing it too.
357 let (session, reason, ready) = {
358 let mut state = self.state.lock().expect("server state not poisoned");
359 match &mut *state {
360 State::Closed { reason, session } => (session.upgrade(), reason.clone(), None),
361 State::Open { session, ready, .. } => {
362 let session = session.clone();
363 let ready = ready.take();
364 *state = State::Closed {
365 reason: error.clone(),
366 session: session.clone(),
367 };
368 match &error {
369 Error::Closed => tracing::info!("wire server closed locally"),
370 _ if error.orderly() => {
371 tracing::info!("wire server closed: {}", error.reason());
372 }
373 _ => tracing::warn!("wire server failed: {}", error.reason()),
374 }
375 (session.upgrade(), error, ready)
376 }
377 }
378 };
379
380 // Release the server lock before taking the session's lock. Wake local
381 // callers before closing the stream, which waits for active I/O to return.
382 if let Some(session) = session {
383 session.close(reason);
384 }
385 self.changed.notify_all();
386 drop(ready);
387 if let Some(stream_closer) = &self.stream_closer {
388 stream_closer.close();
389 }
390 }
391
392 /// Closes the previous session and makes this one available to
393 /// [`Server::accept`].
394 ///
395 /// Only the reader, or the test fixture replacing it, calls this method.
396 fn attach(&self, session: Session) -> Result<(), Error> {
397 // Take an Arc to the previous session, then release the server lock
398 // before closing that session
399 let previous = {
400 let state = self.state.lock().expect("server state not poisoned");
401 match &*state {
402 State::Closed { reason, .. } => return Err(reason.clone()),
403 State::Open { session, .. } => session.upgrade(),
404 }
405 };
406 if let Some(previous) = previous {
407 tracing::info!(
408 "replacing wire session {} with session {}",
409 previous.log_id,
410 session.inner.log_id
411 );
412 previous.close(transport::Error::SessionReset.into());
413 }
414
415 // Another thread may have closed the server while we closed the old
416 // session. Check again under the lock before installing the new one.
417 let mut notifications = Notifications::default();
418 let previous = {
419 let mut state = self.state.lock().expect("server state not poisoned");
420 match &mut *state {
421 State::Closed { reason, .. } => return Err(reason.clone()),
422 State::Open {
423 session: attached,
424 max_inbound_requests,
425 max_inbound_bytes,
426 autoreply_timeout,
427 ready,
428 ..
429 } => {
430 // Apply the current policy before exposing this session or
431 // letting the reader deliver its first message
432 session.inner.set_inbound_limits(
433 *max_inbound_requests,
434 *max_inbound_bytes,
435 &mut notifications,
436 );
437 session.inner.set_autoreply_timeout(*autoreply_timeout);
438 *attached = Arc::downgrade(&session.inner);
439 ready.replace(session)
440 }
441 }
442 };
443
444 // Wake a blocked accept, then drop the owner of a previous session that
445 // accept never took
446 self.changed.notify_all();
447 drop(previous);
448 Ok(())
449 }
450}
451
452/// Fixture supplying sessions in tests in place of the server's transport reader.
453#[cfg(any(test, feature = "fuzz"))]
454pub(super) struct SessionSource {
455 /// Server that receives sessions created by [`SessionSource::open`].
456 server_ref: Weak<ServerInner>,
457}
458
459#[cfg(any(test, feature = "fuzz"))]
460impl Server {
461 /// Creates a server and a fixture that attaches sessions without a stream.
462 pub(super) fn fixture(clock: Clock) -> (Self, SessionSource) {
463 let inner = Arc::new(ServerInner {
464 clock: clock.clone(),
465 state: Mutex::new(State::Open {
466 max_inbound_requests: DEFAULT_MAX_INBOUND_REQUESTS,
467 max_inbound_bytes: DEFAULT_MAX_INBOUND_BYTES,
468 autoreply_timeout: DEFAULT_AUTOREPLY_TIMEOUT,
469 session: Weak::new(),
470 ready: None,
471 wait_hook: None,
472 }),
473 changed: Condvar::new(&clock),
474 stream_closer: None,
475 workers: Arc::new(worker::Tracker::default()),
476 });
477 let source = SessionSource {
478 server_ref: Arc::downgrade(&inner),
479 };
480 (Self { inner }, source)
481 }
482}
483
484#[cfg(any(test, feature = "fuzz"))]
485impl SessionSource {
486 /// Creates a session and passes it to [`ServerInner::attach`], just as the
487 /// reader does after a handshake.
488 ///
489 /// Returns its weak reference so tests can deliver messages to it even
490 /// after another session connects.
491 pub(super) fn open(&mut self) -> Result<Weak<SessionInner>, Error> {
492 let server = self.server_ref.upgrade().ok_or(Error::Closed)?;
493 let session = Session::fixture_with_clock(Side::Server, server.clock.clone());
494 let session_ref = Arc::downgrade(&session.inner);
495 server.attach(session)?;
496 Ok(session_ref)
497 }
498}
499
500#[cfg(any(test, feature = "fuzz"))]
501impl Drop for SessionSource {
502 /// Models loss of the transport reader by permanently ending its server.
503 fn drop(&mut self) {
504 if let Some(server) = self.server_ref.upgrade() {
505 server.close(crate::transport::Error::Terminated.into());
506 }
507 }
508}
509
510#[cfg(any(test, feature = "fuzz"))]
511impl ServerInner {
512 /// Arms a one-shot notification for [`Server::accept`] waiting without a
513 /// ready session.
514 ///
515 /// It is sent while holding `state`, just before acceptance waits on
516 /// `changed`. Tests can then attach a session or close the server without
517 /// using sleeps.
518 ///
519 /// # Panics
520 ///
521 /// Panics if the fixture is closed or already has a session waiting for
522 /// acceptance.
523 pub(super) fn watch_accept_wait(&self) -> std::sync::mpsc::Receiver<()> {
524 let (sender, receiver) = std::sync::mpsc::channel();
525 let mut state = self.state.lock().expect("server state not poisoned");
526 let State::Open {
527 ready, wait_hook, ..
528 } = &mut *state
529 else {
530 panic!("only watch an open server accept");
531 };
532 assert!(ready.is_none());
533 *wait_hook = Some(sender);
534 receiver
535 }
536}
537
538/// Checks server policy callbacks and ownership bounds, and compiles server
539/// construction and acceptance.
540#[cfg(test)]
541#[cfg_attr(coverage_nightly, coverage(off))]
542mod tests {
543 use super::Side;
544 use crate::protocol::{Error, Server, Session};
545 use crate::transport::{Attester, Read, Stream, Write};
546 use darkbio_crypto::xdsa;
547 use std::fmt::Debug;
548
549 /// Checks that a server policy change closing its session runs promise
550 /// callbacks outside the server lock.
551 #[test]
552 fn test_limit_callback_releases_server_lock() {
553 use darkbio_clock::TestClock;
554 use std::sync::mpsc;
555 use std::time::Duration;
556
557 // Attach a clock-controlled session and retain a peer request against its limit
558 let tester = TestClock::new();
559 let (server, _source) = Server::fixture(tester.clock());
560 let session = Session::fixture_with_clock(Side::Server, tester.clock());
561 let inner = session.inner.clone();
562 server.inner.attach(session).unwrap();
563 inner.inject_request(1, vec![1].into()).unwrap();
564
565 // Register a callback on another pending operation in that session
566 let mut promise = inner
567 .request(
568 vec![2].into(),
569 tester.clock().now() + Duration::from_secs(5),
570 )
571 .unwrap();
572 let state = server.inner.clone();
573 let notification = promise.notification_unlocked();
574 let (observed, receiver) = mpsc::channel();
575 promise.notify(move || {
576 let _ = observed.send((state.state.try_lock().is_ok(), notification()));
577 });
578
579 // Lower the server policy and require notification outside both owning locks
580 let server = server.set_inbound_limits(0, 1024);
581 assert_eq!(receiver.try_recv(), Ok((true, true)));
582 assert!(matches!(
583 promise.wait::<Vec<u8>>(),
584 Err(Error::InboundRequestLimitExceeded(0))
585 ));
586 drop(server);
587 }
588
589 /// Compiles server construction from a caller-owned stream, signer and attester.
590 #[allow(dead_code)]
591 fn server<R, W, A>(stream: Stream<R, W>, signer: xdsa::SecretKey, attester: A) -> Server
592 where
593 R: Read + Send + 'static,
594 W: Write + Send + 'static,
595 A: Attester + Send + 'static,
596 {
597 Server::new(stream, signer, attester)
598 }
599
600 /// Checks that server acceptance returns the common concrete session type.
601 #[allow(dead_code)]
602 fn accept(server: &mut Server) -> Result<Session, Error> {
603 server.accept()
604 }
605
606 /// Checks the bounds required to move the server to an application thread
607 /// and to print it.
608 #[test]
609 fn test_thread_capabilities() {
610 /// Requires an owned value to be printable and transferable to a
611 /// background thread.
612 fn movable<T: Debug + Send + 'static>() {}
613 movable::<Server>();
614 }
615}