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::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}