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}