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