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//! 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::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                    abandonment: DEFAULT_ABANDONMENT_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 lifetime of automatic `UNANSWERED` replies for the current and
71    /// future sessions. Defaults to [`DEFAULT_ABANDONMENT_TIMEOUT`]. Applies even
72    /// before `accept()`. Replies already queued keep their deadlines.
73    ///
74    /// See [`Session::set_abandonment_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_abandonment_timeout(self, timeout: Duration) -> Self {
78        self.inner.set_abandonment_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        abandonment: 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_abandonment_timeout(&self, timeout: Duration) {
270        let mut state = self.state.lock().expect("server state not poisoned");
271        if let State::Open {
272            abandonment,
273            session,
274            ..
275        } = &mut *state
276        {
277            *abandonment = timeout;
278            if let Some(session) = session.upgrade() {
279                session.set_abandonment_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                    abandonment,
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_abandonment_timeout(*abandonment);
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                abandonment: DEFAULT_ABANDONMENT_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}