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, ListenerOrigins, 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    origins: ListenerOrigins,
37}
38
39impl Bound {
40    /// The address the listener holds: the port the operating system
41    /// gave when `bind` was asked for port 0, the one asked for
42    /// otherwise. Every reader that hands the city's address on reads
43    /// this one, never the address `bind` was given.
44    #[must_use]
45    pub fn local_addr(&self) -> SocketAddr {
46        self.local
47    }
48
49    /// The names and origins this listener answers to, computed once from
50    /// the address it holds (`crates/wire/spec/Reception/Entry.lean`
51    /// §8-94): the URL a person opens, and what the residents' browser
52    /// guard keeps them away from.
53    #[must_use]
54    pub fn origins(&self) -> &ListenerOrigins {
55        &self.origins
56    }
57}
58
59/// Judges the bind face, then binds the listener.
60///
61/// # Errors
62/// `E_CONFIG_INVALID` from [`decide_bind`], without touching the
63/// network, when no key is given; the same code
64/// when the operating system refuses the address, most often because
65/// another process holds the port, or cannot say which address it gave.
66pub async fn bind(addr: SocketAddr, key: Option<B3Hash>) -> Result<Bound, AxError> {
67    // The face that comes back is the whole of what this listener
68    // presents and what it demands; it goes into the shell, where every
69    // door reads it rather than reading the configuration again.
70    let face = match decide_bind(&addr, key) {
71        BindVerdict::Serve(face) => face,
72        BindVerdict::Refuse(err) => return Err(err),
73    };
74    let unbound = |source: std::io::Error| {
75        AxError::failure(
76            AxCode::ConfigInvalid,
77            "bind the control surface",
78            format!("{addr}: {source}"),
79        )
80        .with_recovery("choose a free port, or stop the process already holding it")
81    };
82    let listener = tokio::net::TcpListener::bind(addr).await.map_err(unbound)?;
83    let local = listener.local_addr().map_err(unbound)?;
84    Ok(Bound {
85        listener,
86        local,
87        face,
88        origins: ListenerOrigins::of(local),
89    })
90}
91
92/// Serves on a bound listener until the future is dropped.
93///
94/// The peer address is read on both faces, because enrolment is refused
95/// off this machine even when the listener never left it.
96///
97/// # Errors
98/// Propagates the accept failures the operating system reports.
99pub async fn serve(bound: Bound, config: ServeConfig) -> Result<(), AxError> {
100    let Bound {
101        listener,
102        face,
103        origins,
104        ..
105    } = bound;
106    let app = router(&config, face, &origins)?.into_make_service_with_connect_info::<SocketAddr>();
107    axum::serve(listener, app).await.map_err(|source| {
108        AxError::failure(
109            AxCode::StorageFatal,
110            "serve the control surface",
111            source.to_string(),
112        )
113        .with_recovery("restart the process; the listener is gone")
114    })
115}