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