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