Skip to main content

haematite_cli/client/
network.rs

1//! HAEMNET-003 R1: a store reached at a HAEMNET-001 network endpoint as well
2//! as at its Unix socket.
3//!
4//! A network store is reached only over TLS, its certificate verified for
5//! the `server_name` the configuration names against this machine's trust
6//! roots ([`Trust::system`]); nothing turns that verification off, and no
7//! peer is trusted for its address. Once the handshake is done the bytes
8//! are the same haem/1 frames, hello and `session.pass` as on the socket.
9
10use std::fmt;
11use std::io::{self, Read, Write};
12use std::net::{Shutdown, SocketAddr, TcpStream, ToSocketAddrs};
13use std::os::unix::net::UnixStream;
14use std::path::{Path, PathBuf};
15use std::sync::{Arc, mpsc};
16use std::time::Duration;
17
18use rustls::pki_types::{CertificateDer, ServerName, pem::PemObject};
19use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned};
20
21/// The environment variable naming a PEM file of further trust roots, the
22/// name every TLS stack on the machine reads for the system's bundle.
23pub const CERT_FILE: &str = "SSL_CERT_FILE";
24
25/// Where a store is reached: its socket's absolute path, or a network
26/// endpoint over verified TLS.
27#[derive(Clone, Debug)]
28pub enum Address {
29    Socket(PathBuf),
30    Network(Remote),
31}
32
33impl Address {
34    /// The words an operator reads for this address: the socket's path, or
35    /// the endpoint.
36    #[must_use]
37    pub fn named(&self) -> String {
38        match self {
39            Self::Socket(path) => path.display().to_string(),
40            Self::Network(remote) => remote.endpoint.clone(),
41        }
42    }
43}
44
45/// A store's HAEMNET-001 network endpoint, the name its certificate must
46/// carry, and the roots it is verified against.
47#[derive(Clone)]
48pub struct Remote {
49    endpoint: String,
50    server_name: ServerName<'static>,
51    trust: Trust,
52}
53
54impl fmt::Debug for Remote {
55    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
56        formatter
57            .debug_struct("Remote")
58            .field("endpoint", &self.endpoint)
59            .field("server_name", &self.server_name)
60            .finish_non_exhaustive()
61    }
62}
63
64impl Remote {
65    /// The endpoint `endpoint` (`<host>:<port>`) verified as `server_name`
66    /// against `trust`.
67    ///
68    /// # Errors
69    ///
70    /// Words naming which of the two does not parse.
71    pub fn new(endpoint: &str, server_name: &str, trust: &Trust) -> Result<Self, String> {
72        if !endpoint_parses(endpoint) {
73            return Err(format!(
74                "endpoint {endpoint:?} is not <host>:<port> with a port from 1 to 65535"
75            ));
76        }
77        Ok(Self {
78            endpoint: endpoint.to_owned(),
79            server_name: name(server_name)?,
80            trust: trust.clone(),
81        })
82    }
83
84    /// The endpoint, as configured.
85    #[must_use]
86    pub fn endpoint(&self) -> &str {
87        &self.endpoint
88    }
89}
90
91/// The TLS server name `text` names: a DNS name or an IP address.
92///
93/// # Errors
94///
95/// Words saying it is neither.
96pub fn name(text: &str) -> Result<ServerName<'static>, String> {
97    ServerName::try_from(text.to_owned())
98        .map_err(|_| format!("server_name {text:?} is not a DNS name or an IP address"))
99}
100
101/// Whether `text` is `<host>:<port>`: a DNS name, an IPv4 address or a
102/// bracketed IPv6 address, and a port from 1 to 65535.
103#[must_use]
104pub fn endpoint_parses(text: &str) -> bool {
105    let Some((host, port)) = text.rsplit_once(':') else {
106        return false;
107    };
108    let port_ok = port.parse::<u16>().is_ok_and(|port| port != 0);
109    let host_ok = host.strip_prefix('[').map_or_else(
110        || {
111            !host.is_empty()
112                && host.len() <= 253
113                && host
114                    .bytes()
115                    .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'.')
116        },
117        |inner| {
118            inner
119                .strip_suffix(']')
120                .is_some_and(|inner| inner.parse::<std::net::Ipv6Addr>().is_ok())
121        },
122    );
123    port_ok && host_ok
124}
125
126/// The roots a network store's certificate is verified against.
127#[derive(Clone)]
128pub struct Trust(Arc<ClientConfig>);
129
130impl Trust {
131    /// This machine's trust roots: the Mozilla bundle every HTTPS client in
132    /// haematite already verifies against (webpki-roots, as ureq and reqwest
133    /// build it), and every certificate in the PEM file `SSL_CERT_FILE`
134    /// names, when it is set, for a site's own authority.
135    ///
136    /// # Errors
137    ///
138    /// `door_trust_roots_invalid`, naming the file, when `SSL_CERT_FILE` is
139    /// set and cannot be read or holds no certificate.
140    pub fn system() -> Result<Self, String> {
141        let mut roots = mozilla();
142        if let Some(file) = std::env::var_os(CERT_FILE) {
143            add_file(&mut roots, Path::new(&file))?;
144        }
145        Self::from_roots(roots)
146    }
147
148    /// The Mozilla bundle alone, without reading `SSL_CERT_FILE`: for a
149    /// door with no network store, which verifies nothing with it, so a
150    /// fault in that file never stops a door of sockets.
151    ///
152    /// # Errors
153    ///
154    /// `door_trust_roots_invalid` when no safe protocol version is offered.
155    pub fn mozilla() -> Result<Self, String> {
156        Self::from_roots(mozilla())
157    }
158
159    /// Only the certificates in `file`, for a test's own authority.
160    ///
161    /// # Errors
162    ///
163    /// `door_trust_roots_invalid` naming the file.
164    #[cfg(test)]
165    pub(crate) fn only(file: &Path) -> Result<Self, String> {
166        let mut roots = RootCertStore::empty();
167        add_file(&mut roots, file)?;
168        Self::from_roots(roots)
169    }
170
171    /// The rustls client configuration these roots verify with, for a
172    /// connection that is not a store's (the assistant seat's door link).
173    #[must_use]
174    pub(crate) fn client_config(&self) -> Arc<ClientConfig> {
175        Arc::clone(&self.0)
176    }
177
178    fn from_roots(roots: RootCertStore) -> Result<Self, String> {
179        ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
180            .with_safe_default_protocol_versions()
181            .map(|builder| {
182                Self(Arc::new(
183                    builder.with_root_certificates(roots).with_no_client_auth(),
184                ))
185            })
186            .map_err(|error| format!("door_trust_roots_invalid: protocol versions: {error}"))
187    }
188}
189
190fn mozilla() -> RootCertStore {
191    let mut roots = RootCertStore::empty();
192    roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
193    roots
194}
195
196fn add_file(roots: &mut RootCertStore, file: &Path) -> Result<(), String> {
197    let invalid =
198        |why: &dyn fmt::Display| format!("door_trust_roots_invalid: {}: {why}", file.display());
199    let mut added = 0_usize;
200    for certificate in CertificateDer::pem_file_iter(file).map_err(|error| invalid(&error))? {
201        roots
202            .add(certificate.map_err(|error| invalid(&error))?)
203            .map_err(|error| invalid(&error))?;
204        added += 1;
205    }
206    if added == 0 {
207        return Err(invalid(&"holds no certificate"));
208    }
209    Ok(())
210}
211
212/// Why a network store was not reached over verified TLS.
213#[derive(Debug)]
214pub enum Refused {
215    /// No connection: nothing answered at the endpoint within the bound.
216    Unreached(String),
217    /// Something answered and did not complete a TLS handshake: a
218    /// plain-TCP endpoint, or another protocol.
219    NotTls(String),
220    /// The endpoint's certificate is not trusted for its server name.
221    Untrusted(String),
222    /// A TLS endpoint refused the handshake with an alert.
223    TlsRefused(String),
224}
225
226impl Refused {
227    /// The name an operator reads for it.
228    #[must_use]
229    pub const fn code(&self) -> &'static str {
230        match self {
231            Self::Unreached(_) => "door_store_unreached",
232            Self::NotTls(_) => "door_store_not_tls",
233            Self::Untrusted(_) => "door_store_tls_untrusted",
234            Self::TlsRefused(_) => "door_store_tls_refused",
235        }
236    }
237
238    /// The cause in words.
239    #[must_use]
240    pub fn why(&self) -> &str {
241        match self {
242            Self::Unreached(why)
243            | Self::NotTls(why)
244            | Self::Untrusted(why)
245            | Self::TlsRefused(why) => why,
246        }
247    }
248}
249
250impl fmt::Display for Refused {
251    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
252        write!(formatter, "{}: {}", self.code(), self.why())
253    }
254}
255
256/// One connection to a store, on its socket or over TLS.
257pub enum Stream {
258    Unix(UnixStream),
259    Tls(Box<StreamOwned<ClientConnection, TcpStream>>),
260}
261
262impl Stream {
263    /// Bound each read by `timeout`, or by nothing with `None`.
264    ///
265    /// # Errors
266    ///
267    /// The system's refusal of the bound.
268    pub fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
269        match self {
270            Self::Unix(stream) => stream.set_read_timeout(timeout),
271            Self::Tls(stream) => stream.sock.set_read_timeout(timeout),
272        }
273    }
274
275    /// Bound each write by `timeout`.
276    ///
277    /// # Errors
278    ///
279    /// The system's refusal of the bound.
280    pub fn set_write_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
281        match self {
282            Self::Unix(stream) => stream.set_write_timeout(timeout),
283            Self::Tls(stream) => stream.sock.set_write_timeout(timeout),
284        }
285    }
286
287    /// A handle that hangs this connection up from another thread.
288    ///
289    /// # Errors
290    ///
291    /// The system's refusal to duplicate the connection's descriptor.
292    pub fn hangup(&self) -> io::Result<Hangup> {
293        Ok(match self {
294            Self::Unix(stream) => Hangup::Unix(stream.try_clone()?),
295            Self::Tls(stream) => Hangup::Tcp(stream.sock.try_clone()?),
296        })
297    }
298}
299
300impl Read for Stream {
301    fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
302        match self {
303            Self::Unix(stream) => stream.read(bytes),
304            // A store that closes its socket without a TLS close_notify has
305            // still closed: between frames that is the end, and inside one
306            // the frame reader names the cut.
307            Self::Tls(stream) => match stream.read(bytes) {
308                Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => Ok(0),
309                read => read,
310            },
311        }
312    }
313}
314
315impl Write for Stream {
316    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
317        match self {
318            Self::Unix(stream) => stream.write(bytes),
319            Self::Tls(stream) => stream.write(bytes),
320        }
321    }
322
323    fn flush(&mut self) -> io::Result<()> {
324        match self {
325            Self::Unix(stream) => stream.flush(),
326            Self::Tls(stream) => stream.flush(),
327        }
328    }
329}
330
331/// Hangs a store connection up from another thread.
332pub enum Hangup {
333    Unix(UnixStream),
334    Tcp(TcpStream),
335}
336
337impl Hangup {
338    /// Shut the connection down in `how`.
339    ///
340    /// # Errors
341    ///
342    /// The system's refusal.
343    pub fn shutdown(&self, how: Shutdown) -> io::Result<()> {
344        match self {
345            Self::Unix(stream) => stream.shutdown(how),
346            Self::Tcp(stream) => stream.shutdown(how),
347        }
348    }
349}
350
351/// Connect to `remote` and complete its TLS handshake.
352///
353/// Each step is bounded by `timeout`. The name resolution and the connect
354/// run on a thread of their own, waited for at most `timeout`, as the
355/// socket's connect is.
356///
357/// # Errors
358///
359/// [`Refused`] naming where it stopped.
360pub fn open(remote: &Remote, timeout: Duration) -> Result<Stream, Refused> {
361    let endpoint = remote.endpoint.clone();
362    let (sender, receiver) = mpsc::channel();
363    let thread_endpoint = endpoint.clone();
364    std::thread::Builder::new()
365        .name("haem-network-connect".to_owned())
366        .spawn(move || {
367            // The receiver is gone only when the connect outlived the timeout.
368            let _unwaited = sender.send(connect(&thread_endpoint, timeout));
369        })
370        .map_err(|error| Refused::Unreached(format!("the connect could not start: {error}")))?;
371    let mut tcp = match receiver.recv_timeout(timeout) {
372        Ok(Ok(tcp)) => tcp,
373        Ok(Err(error)) => {
374            return Err(Refused::Unreached(format!(
375                "could not connect to {endpoint}: {error}"
376            )));
377        }
378        Err(_) => {
379            return Err(Refused::Unreached(format!(
380                "could not connect to {endpoint} within {} ms",
381                timeout.as_millis()
382            )));
383        }
384    };
385    let unbounded = |error: io::Error| {
386        Refused::Unreached(format!(
387            "could not bound the connection to {endpoint}: {error}"
388        ))
389    };
390    tcp.set_read_timeout(Some(timeout)).map_err(unbounded)?;
391    tcp.set_write_timeout(Some(timeout)).map_err(unbounded)?;
392    let mut connection =
393        ClientConnection::new(Arc::clone(&remote.trust.0), remote.server_name.clone()).map_err(
394            |error| Refused::TlsRefused(format!("{endpoint}: TLS could not start: {error}")),
395        )?;
396    while connection.is_handshaking() {
397        if let Err(error) = connection.complete_io(&mut tcp) {
398            return Err(handshake_failed(&endpoint, &error));
399        }
400    }
401    Ok(Stream::Tls(Box::new(StreamOwned::new(connection, tcp))))
402}
403
404fn connect(endpoint: &str, timeout: Duration) -> io::Result<TcpStream> {
405    let addresses: Vec<SocketAddr> = endpoint.to_socket_addrs()?.collect();
406    let mut last = io::Error::new(
407        io::ErrorKind::NotFound,
408        "the endpoint's name resolved to no address",
409    );
410    for address in addresses {
411        match TcpStream::connect_timeout(&address, timeout) {
412            Ok(tcp) => return Ok(tcp),
413            Err(error) => last = error,
414        }
415    }
416    Err(last)
417}
418
419/// What a failed handshake was: a certificate not trusted, an alert from a
420/// TLS endpoint, a silent endpoint, or something that does not speak TLS.
421fn handshake_failed(endpoint: &str, error: &io::Error) -> Refused {
422    let tls = error
423        .get_ref()
424        .and_then(|inner| inner.downcast_ref::<rustls::Error>());
425    match tls {
426        Some(rustls::Error::InvalidCertificate(why)) => Refused::Untrusted(format!(
427            "{endpoint}: the store's certificate is not trusted for its server_name: {why:?}"
428        )),
429        Some(rustls::Error::AlertReceived(alert)) => Refused::TlsRefused(format!(
430            "{endpoint}: the store refused the TLS handshake: {alert:?}"
431        )),
432        _ if matches!(
433            error.kind(),
434            io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
435        ) =>
436        {
437            Refused::Unreached(format!(
438                "{endpoint}: no TLS handshake answer within the bound: {error}"
439            ))
440        }
441        _ => Refused::NotTls(format!(
442            "{endpoint}: the endpoint did not complete a TLS handshake; a network store is reached only over TLS, never plain TCP: {error}"
443        )),
444    }
445}