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