Skip to main content

wire/server/
listener.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! The port a city takes before it opens, and serving on it
7//! (`crates/wire/spec/Server/Listener.lean` ยง8-46).
8//!
9//! Two steps, because the assembly layer needs a gap between them: a
10//! city takes its port first, then opens its one writer, and only then
11//! has the sinks a [`ServeConfig`] is made of. A single call that bound
12//! and served together left the writer open, and writing, before the
13//! operating system had said whether the port was free.
14
15use std::net::SocketAddr;
16
17use kernel::{AxCode, AxError, B3Hash};
18
19use crate::reception::{BindFace, BindVerdict, decide_bind};
20
21use super::config::{ServeConfig, router};
22
23/// A listener already bound, the address it holds, and the face its
24/// judgement gave it.
25///
26/// The face travels with the listener because it is the verdict about
27/// this very address: a face carried apart from its listener could be
28/// served on a socket it was never judged for. The address is read once,
29/// at bind, because a city asked to serve on `:0` holds a port only the
30/// listener knows (wire D16).
31#[must_use = "a bound port answers nobody until it is served"]
32pub struct Bound {
33    listener: tokio::net::TcpListener,
34    local: SocketAddr,
35    face: BindFace,
36}
37
38impl Bound {
39    /// The address the listener holds: the port the operating system
40    /// gave when `bind` was asked for port 0, the one asked for
41    /// otherwise. Every reader that hands the city's address on reads
42    /// this one, never the address `bind` was given.
43    #[must_use]
44    pub fn local_addr(&self) -> SocketAddr {
45        self.local
46    }
47}
48
49/// Judges the bind face, then binds the listener.
50///
51/// # Errors
52/// `E_CONFIG_INVALID` from [`decide_bind`], without touching the
53/// network, when an exposed address has no pairing token; the same code
54/// when the operating system refuses the address, most often because
55/// another process holds the port, or cannot say which address it gave.
56pub async fn bind(addr: SocketAddr, token_digest: Option<B3Hash>) -> Result<Bound, AxError> {
57    // The face that comes back is the whole of what this listener
58    // presents and what it demands; it goes into the shell, where every
59    // door reads it rather than reading the configuration again.
60    let face = match decide_bind(&addr, token_digest) {
61        BindVerdict::Serve(face) => face,
62        BindVerdict::Refuse(err) => return Err(err),
63    };
64    let unbound = |source: std::io::Error| {
65        AxError::failure(
66            AxCode::ConfigInvalid,
67            "bind the control surface",
68            format!("{addr}: {source}"),
69        )
70        .with_recovery("choose a free port, or stop the process already holding it")
71    };
72    let listener = tokio::net::TcpListener::bind(addr).await.map_err(unbound)?;
73    let local = listener.local_addr().map_err(unbound)?;
74    Ok(Bound {
75        listener,
76        local,
77        face,
78    })
79}
80
81/// Serves on a bound listener until the future is dropped.
82///
83/// The peer address is read on both faces, because enrolment is refused
84/// off this machine even when the listener never left it.
85///
86/// # Errors
87/// Propagates the accept failures the operating system reports.
88pub async fn serve(bound: Bound, config: ServeConfig) -> Result<(), AxError> {
89    let Bound { listener, face, .. } = bound;
90    let app = router(&config, face).into_make_service_with_connect_info::<SocketAddr>();
91    axum::serve(listener, app).await.map_err(|source| {
92        AxError::failure(
93            AxCode::StorageFatal,
94            "serve the control surface",
95            source.to_string(),
96        )
97        .with_recovery("restart the process; the listener is gone")
98    })
99}