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