Skip to main content

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}