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