beck_rt/
outbound.rs

1//! The outbound HTTP client: `beck_core::net::Outbound`, over hyper.
2//!
3//! The seam is in `beck-core` because the evaluator needs it and the evaluator must not know what
4//! a socket is. The implementation is here because this is the crate that already has an HTTP
5//! stack — [`docs/07`](../../../../../docs/07-dependencies.md) chose hyper, and the server half of it
6//! has been in [`crate::http`] since Phase 1. Nothing new was taken to make a request.
7//!
8//! # TLS, when the request asked for it
9//!
10//! [`beck_core::net::Request::tls`] decides, and the certificate is verified against
11//! [`beck_core::net::Request::host`] — the string the *call site* wrote, which is also the atom the
12//! call performs and therefore the peer in the cluster's egress rule
13//! ([`adr/0013`](../../../../../docs/adr/0013-the-host-of-an-outbound-call-is-written-at-the-call-site.md)).
14//! There is deliberately no way to reach a peer under a name the deployment was not told about:
15//! no SNI override, no `danger_accept_invalid_certs`, no pinning.
16//!
17//! The trust anchors are Mozilla's, compiled in as data
18//! ([`adr/0023`](../../../../../docs/adr/0023-tls-and-the-signature-it-brings.md)) rather than read
19//! from the container's filesystem — §6.2's images execute nothing at build time, so a
20//! `ca-certificates` package would be one more thing whose version the SBOM cannot state.
21//!
22//! # Its own runtime
23//!
24//! [`beck_core::net::Outbound::fetch`] is synchronous, because the evaluator is: a tree-walker
25//! cannot await. Rather than block on whatever runtime happens to be current — which panics on a
26//! current-thread runtime and steals a worker on a multi-thread one — this owns a small one of its
27//! own, on its own thread. An outbound call therefore cannot stall the runtime serving the page.
28
29use std::sync::Arc;
30use std::sync::OnceLock;
31use std::time::Duration;
32
33use beck_core::net::{Failure, Outbound, Reply, Request, Stop};
34use http_body_util::{BodyExt, Full, Limited};
35use hyper::body::Bytes;
36use tokio_rustls::rustls::pki_types::ServerName;
37use tokio_rustls::rustls::ClientConfig;
38use tokio_rustls::TlsConnector;
39
40/// How long an exchange may take before it is a [`Failure::TimedOut`].
41///
42/// A default rather than a policy: a per-call deadline is a language question (§3.6 would have to
43/// give it a place in a signature) and this is the number that keeps a wedged peer from wedging a
44/// fold. It is elapsed time, which [`beck_core::clock`] deliberately does not cover — a deadline
45/// does not enter the log and cannot change what a replay produces.
46pub const DEFAULT_TIMEOUT_MS: i64 = 10_000;
47
48/// How often a watched exchange asks whether the caller still wants the reply.
49///
50/// Only a request made by a child of a `parallel:` is watched at all, so this is not a cost every
51/// outbound call pays. The number is chosen against what cancellation is *for*: a sibling that
52/// failed should not leave a scope waiting, and the difference between 5 ms and 50 ms of extra
53/// waiting is invisible beside the ten-second timeout it replaces.
54const POLL_MS: Duration = Duration::from_millis(5);
55
56/// The most of a reply that will be read.
57///
58/// A peer that streams for ever is the cheapest denial of service there is, and a runtime that
59/// reads until EOF is the one that falls for it. 8 MiB is generous for an API response and
60/// bounded, which is the property that matters.
61pub const MAX_BODY_BYTES: usize = 8 * 1024 * 1024;
62
63/// The client half of a TLS session, built once.
64///
65/// Mozilla's trust anchors and nothing else: no filesystem store, no environment variable naming
66/// one, and no way for a program to add to it. What a Beck program may reach is decided by the
67/// hosts it writes (§6.5's egress rule); *who* may answer to one of those names is decided here,
68/// and neither is a runtime knob.
69fn client_config() -> &'static Arc<ClientConfig> {
70    static CONFIG: OnceLock<Arc<ClientConfig>> = OnceLock::new();
71    CONFIG.get_or_init(|| {
72        let mut roots = tokio_rustls::rustls::RootCertStore::empty();
73        roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
74        Arc::new(config_for(roots))
75    })
76}
77
78/// The TLS versions this project offers, named rather than defaulted.
79///
80/// `docs/12`'s row is "TLS 1.3 (RFC 8446), no legacy downgrade" and rustls's safe defaults are 1.2
81/// *and* 1.3, so a client built with them accepts exactly the downgrade the row says there is none
82/// of — which is what it did until this constant existed.
83///
84/// Public, and read by `beck-cli`'s `fetch.rs`, because there are **two** clients — a program's
85/// outbound call and the compiler's own fetch — and a version list written twice is a version list
86/// that can differ. One decision, and `a_peer_that_speaks_only_tls_1_2_is_refused` below is the
87/// handshake that holds it.
88pub const TLS_VERSIONS: &[&tokio_rustls::rustls::SupportedProtocolVersion] =
89    &[&tokio_rustls::rustls::version::TLS13];
90
91/// The provider is named rather than defaulted: rustls picks one for you only when exactly one is
92/// compiled in, and "exactly one" is a property of somebody else's feature unification.
93fn config_for(roots: tokio_rustls::rustls::RootCertStore) -> ClientConfig {
94    let mut config = ClientConfig::builder_with_provider(Arc::new(
95        tokio_rustls::rustls::crypto::aws_lc_rs::default_provider(),
96    ))
97    .with_protocol_versions(TLS_VERSIONS)
98    .expect("TLS 1.3 is supported by the provider")
99    .with_root_certificates(roots)
100    .with_no_client_auth();
101    // The exchange is one request over HTTP/1.1 (`beck_core::net::Outbound`), so say so: a peer
102    // that negotiates h2 against a client that cannot speak it is a hang rather than an error.
103    config.alpn_protocols = vec![b"http/1.1".to_vec()];
104    config
105}
106
107#[derive(Debug)]
108pub struct HttpOutbound {
109    runtime: tokio::runtime::Runtime,
110    timeout: Duration,
111    /// Whose certificates are believed. `None` is Mozilla's set, which is every deployment;
112    /// `Some` is a test that made its own certificate authority a moment ago.
113    roots: Option<Arc<ClientConfig>>,
114}
115
116impl HttpOutbound {
117    pub fn new() -> std::io::Result<HttpOutbound> {
118        HttpOutbound::with_timeout(DEFAULT_TIMEOUT_MS)
119    }
120
121    pub fn with_timeout(millis: i64) -> std::io::Result<HttpOutbound> {
122        Ok(HttpOutbound {
123            runtime: tokio::runtime::Builder::new_multi_thread()
124                .worker_threads(1)
125                .enable_all()
126                .thread_name("beck-outbound")
127                .build()?,
128            timeout: Duration::from_millis(millis.max(1) as u64),
129            roots: None,
130        })
131    }
132
133    /// A client that believes one certificate authority instead of Mozilla's.
134    ///
135    /// `#[cfg(test)]`, and that is the point: a real TLS handshake is the only way to check that
136    /// this module speaks TLS rather than that it compiles against a TLS library, and it needs a
137    /// certificate somebody trusts. Shipping the knob would put "trust this instead" one call away
138    /// from every deployment, so it is not shipped.
139    #[cfg(test)]
140    fn trusting(roots: tokio_rustls::rustls::RootCertStore) -> std::io::Result<HttpOutbound> {
141        Ok(HttpOutbound {
142            roots: Some(Arc::new(config_for(roots))),
143            ..HttpOutbound::new()?
144        })
145    }
146
147    fn tls(&self) -> Arc<ClientConfig> {
148        self.roots
149            .clone()
150            .unwrap_or_else(|| Arc::clone(client_config()))
151    }
152
153    /// Install this as the process's client, if nothing has installed one.
154    ///
155    /// Returns whether it was installed. A `beck test` process deliberately does not call this:
156    /// `net.out` is auto-stubbed there (§21.3), and a test that reached a real socket would be a
157    /// test that depends on somebody else's uptime.
158    pub fn install() -> bool {
159        match HttpOutbound::new() {
160            Ok(client) => beck_core::net::set_process_outbound(Arc::new(client)),
161            Err(e) => {
162                tracing::warn!(error = %e, "no outbound HTTP client: requests will be refused");
163                false
164            }
165        }
166    }
167}
168
169impl Outbound for HttpOutbound {
170    fn fetch(&self, request: &Request, stop: &Stop) -> Result<Reply, Failure> {
171        let millis = self.timeout.as_millis() as i64;
172        let tls = self.tls();
173        self.runtime.block_on(async {
174            // The ordinary case has no watcher, and pays for none: only a child of a `parallel:`
175            // can be cancelled, so a request from anywhere else takes the path this always had.
176            if !stop.watched() {
177                return match tokio::time::timeout(self.timeout, exchange(request, tls)).await {
178                    Ok(result) => result,
179                    Err(_) => Err(Failure::TimedOut(millis)),
180                };
181            }
182            tokio::select! {
183                // Biased so that a reply already in hand wins a stop that arrived in the same
184                // tick: answering is never worse than not answering, and an arbitrary choice here
185                // would make a cancelled scope's *timing* decide whether a peer was called for
186                // nothing.
187                biased;
188                result = tokio::time::timeout(self.timeout, exchange(request, tls)) => match result {
189                    Ok(result) => result,
190                    Err(_) => Err(Failure::TimedOut(millis)),
191                },
192                () = watch(stop) => Err(Failure::Stopped),
193            }
194        })
195    }
196}
197
198/// Resolve when the caller stops wanting the reply.
199///
200/// A poll rather than a notification, because [`Stop`] is a predicate over state the *caller*
201/// already keeps — the chain of enclosing scopes and their first-failed indices — and a
202/// notification would be a second copy of it. [`POLL_MS`] is what that costs.
203async fn watch(stop: &Stop) {
204    loop {
205        if stop.asked() {
206            return;
207        }
208        tokio::time::sleep(POLL_MS).await;
209    }
210}
211
212async fn exchange(request: &Request, tls: Arc<ClientConfig>) -> Result<Reply, Failure> {
213    let authority = format!("{}:{}", request.host, request.port);
214    let stream = tokio::net::TcpStream::connect(&authority)
215        .await
216        .map_err(|e| Failure::Unreachable(e.to_string()))?;
217    if !request.tls {
218        return speak_http(stream, request, &authority).await;
219    }
220    // The name checked is `request.host` — the literal the call site wrote, which is the atom the
221    // call performs. There is no other string here it *could* be checked against, which is what
222    // makes "every host a program can reach is one the program named" hold on the inbound half too.
223    let name = ServerName::try_from(request.host.to_string()).map_err(|_| {
224        Failure::Unreachable(format!(
225            "`{}` is not a name a certificate can be checked against",
226            request.host
227        ))
228    })?;
229    let stream = TlsConnector::from(tls)
230        .connect(name, stream)
231        .await
232        .map_err(|e| {
233            Failure::Unreachable(format!(
234                "the TLS handshake with `{}` failed: {e}",
235                request.host
236            ))
237        })?;
238    speak_http(stream, request, &authority).await
239}
240
241/// The HTTP/1.1 half, over whichever stream the caller established.
242///
243/// Generic so that the plaintext and TLS paths are the *same* exchange rather than two written
244/// out: a difference between them would be a difference nothing in the suite could see, since the
245/// tests that assert what a request looks like run over the plaintext one.
246async fn speak_http<S>(stream: S, request: &Request, authority: &str) -> Result<Reply, Failure>
247where
248    S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static,
249{
250    let (mut sender, conn) =
251        hyper::client::conn::http1::handshake(hyper_util::rt::TokioIo::new(stream))
252            .await
253            .map_err(|e| Failure::BadResponse(e.to_string()))?;
254    // The connection is a future that has to be polled for the exchange to progress. It ends when
255    // the response is done; nothing here reuses it, because a pool is a policy and this is a seam.
256    let pump = tokio::spawn(async move {
257        let _ = conn.await;
258    });
259
260    let mut builder = hyper::Request::builder()
261        .method(request.method.as_ref())
262        .uri(request.path.as_ref())
263        // HTTP/1.1 requires it, and it is the name the peer is asked about rather than the address
264        // it was reached at.
265        .header(hyper::header::HOST, authority);
266    for (name, value) in &request.headers {
267        builder = builder.header(name.as_ref(), value.as_ref());
268    }
269    let outgoing = builder
270        .body(Full::new(Bytes::from(request.body.as_bytes().to_vec())))
271        .map_err(|e| Failure::BadResponse(e.to_string()))?;
272
273    let response = sender
274        .send_request(outgoing)
275        .await
276        .map_err(|e| Failure::Unreachable(e.to_string()))?;
277    let status = response.status().as_u16() as i64;
278    let headers: Vec<(Arc<str>, Arc<str>)> = response
279        .headers()
280        .iter()
281        .filter_map(|(k, v)| {
282            v.to_str()
283                .ok()
284                .map(|v| (Arc::from(k.as_str()), Arc::from(v)))
285        })
286        .collect();
287    let body = Limited::new(response.into_body(), MAX_BODY_BYTES)
288        .collect()
289        .await
290        .map_err(|_| {
291            Failure::BadResponse(format!("the reply is longer than {MAX_BODY_BYTES} bytes"))
292        })?
293        .to_bytes();
294    pump.abort();
295    Ok(Reply {
296        status,
297        headers,
298        body: Arc::from(String::from_utf8_lossy(&body).as_ref()),
299    })
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305    use std::net::SocketAddr;
306
307    /// A server that answers once, and says what it was asked.
308    ///
309    /// Loopback, on a port the OS picks: this test makes a real HTTP request over a real socket
310    /// and reaches nothing outside the process, which is the only way to test a client without
311    /// making the suite depend on somebody else's uptime.
312    async fn echo_once(reply: &'static str) -> SocketAddr {
313        let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
314            .await
315            .expect("a loopback port");
316        let addr = listener.local_addr().expect("an address");
317        tokio::spawn(async move {
318            let (stream, _) = listener.accept().await.expect("a connection");
319            let service = hyper::service::service_fn(
320                move |req: hyper::Request<hyper::body::Incoming>| async move {
321                    let method = req.method().to_string();
322                    let path = req.uri().path().to_string();
323                    let hdr = req
324                        .headers()
325                        .get("x-beck")
326                        .and_then(|v| v.to_str().ok())
327                        .unwrap_or("")
328                        .to_string();
329                    let body = req.into_body().collect().await.expect("a body").to_bytes();
330                    let seen = format!(
331                        "{method} {path} x-beck={hdr} body={}",
332                        String::from_utf8_lossy(&body)
333                    );
334                    Ok::<_, std::convert::Infallible>(
335                        hyper::Response::builder()
336                            .status(if reply == "seen" { 200 } else { 503 })
337                            .header("x-reply", "yes")
338                            .body(Full::new(Bytes::from(seen)))
339                            .expect("a response"),
340                    )
341                },
342            );
343            let _ = hyper::server::conn::http1::Builder::new()
344                .serve_connection(hyper_util::rt::TokioIo::new(stream), service)
345                .await;
346        });
347        addr
348    }
349
350    fn request(port: u16, path: &str) -> Request {
351        Request {
352            host: Arc::from("127.0.0.1"),
353            port,
354            tls: false,
355            method: Arc::from("POST"),
356            path: Arc::from(path),
357            headers: vec![(Arc::from("x-beck"), Arc::from("1"))],
358            body: Arc::from("hello"),
359        }
360    }
361
362    #[test]
363    fn a_request_reaches_a_real_server_and_the_reply_comes_back() {
364        let client = HttpOutbound::new().expect("a client");
365        let addr = client.runtime.block_on(echo_once("seen"));
366        let reply = client
367            .fetch(&request(addr.port(), "/v1/things?x=1"), &Stop::never())
368            .expect("a reply");
369        assert_eq!(reply.status, 200);
370        assert_eq!(
371            reply.body.as_ref(),
372            "POST /v1/things x-beck=1 body=hello",
373            "the server saw the method, the path, the header and the body"
374        );
375        assert!(
376            reply
377                .headers
378                .iter()
379                .any(|(k, v)| k.as_ref() == "x-reply" && v.as_ref() == "yes"),
380            "{:?}",
381            reply.headers
382        );
383    }
384
385    #[test]
386    fn a_status_is_a_reply_and_not_a_failure() {
387        let client = HttpOutbound::new().expect("a client");
388        let addr = client.runtime.block_on(echo_once("no"));
389        let reply = client
390            .fetch(&request(addr.port(), "/gone"), &Stop::never())
391            .expect("a reply");
392        assert_eq!(reply.status, 503);
393        assert!(reply.body.contains("/gone"), "the body survives a 503");
394    }
395
396    /// The client lets go of a peer that never answers, when the caller stops wanting the reply.
397    ///
398    /// A real socket that accepts and then says nothing, which is the shape a hung peer has: the
399    /// exchange is genuinely in flight, so this is `Stop` reaching *into* the await rather than a
400    /// check made before or after it. The alternative is the ten-second timeout, and a `parallel:`
401    /// whose first child failed spending all of it.
402    #[test]
403    fn a_watched_request_is_given_up_when_the_caller_stops_wanting_it() {
404        let client = HttpOutbound::new().expect("a client");
405        let addr = client.runtime.block_on(async {
406            let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
407                .await
408                .expect("a port");
409            let addr = listener.local_addr().expect("an address");
410            tokio::spawn(async move {
411                // Accept and hold: no bytes, no close. Dropped when the runtime is.
412                let held = listener.accept().await;
413                std::future::pending::<()>().await;
414                drop(held);
415            });
416            addr
417        });
418
419        let asked = Arc::new(std::sync::atomic::AtomicBool::new(false));
420        let stop = {
421            let asked = Arc::clone(&asked);
422            Stop::when(move || asked.load(std::sync::atomic::Ordering::SeqCst))
423        };
424        // Set from another thread while the call is in flight, which is what a sibling failing in
425        // a `parallel:` looks like from here.
426        let flip = {
427            let asked = Arc::clone(&asked);
428            std::thread::spawn(move || {
429                std::thread::sleep(Duration::from_millis(50));
430                asked.store(true, std::sync::atomic::Ordering::SeqCst);
431            })
432        };
433
434        let out = client.fetch(&request(addr.port(), "/hangs"), &stop);
435        flip.join().expect("the flipping thread");
436        assert!(
437            matches!(out, Err(Failure::Stopped)),
438            "the client should give up on a stopped request rather than wait out its timeout, and \
439             it answered {out:?}"
440        );
441    }
442
443    #[test]
444    fn nothing_listening_is_unreachable_rather_than_a_panic() {
445        let client = HttpOutbound::new().expect("a client");
446        // Port 1 on loopback: privileged, and nothing in this process is bound to it.
447        match client.fetch(&request(1, "/"), &Stop::never()) {
448            Err(Failure::Unreachable(_)) => {}
449            other => panic!("expected unreachable, got {other:?}"),
450        }
451    }
452
453    // ------------------------------------------------------------------------------------ TLS
454    //
455    // A real handshake against a real server, because the thing worth checking is that this module
456    // speaks TLS rather than that it compiles against a TLS library. The certificate is made here,
457    // a moment before it is used, and trusted by *this client only* — `HttpOutbound::trusting` is
458    // `#[cfg(test)]` so the knob does not exist in a deployment.
459
460    /// A certificate authority, and the trust store that believes exactly it.
461    fn certificate_authority() -> (
462        rcgen::Issuer<'static, rcgen::KeyPair>,
463        tokio_rustls::rustls::RootCertStore,
464    ) {
465        let mut params = rcgen::CertificateParams::new(Vec::new()).expect("no names on a CA");
466        params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
467        let key = rcgen::KeyPair::generate().expect("a key pair");
468        let ca = params.self_signed(&key).expect("a self-signed CA");
469        let mut roots = tokio_rustls::rustls::RootCertStore::empty();
470        roots
471            .add(ca.der().clone())
472            .expect("the CA is a certificate");
473        (rcgen::Issuer::new(params, key), roots)
474    }
475
476    /// A TLS server on loopback that answers one request, presenting a certificate for whatever
477    /// name it is told to claim.
478    async fn tls_echo_once(
479        issuer: &rcgen::Issuer<'static, rcgen::KeyPair>,
480        claims: &str,
481    ) -> SocketAddr {
482        tls_echo_once_speaking(issuer, claims, &[&tokio_rustls::rustls::version::TLS13]).await
483    }
484
485    /// The same, offering exactly the protocol versions it is given.
486    ///
487    /// A parameter rather than a second server, because the pair only means something if the two
488    /// halves differ in *one* way: the same certificate, the same trust anchor, the same request,
489    /// and a peer that speaks 1.2 where the other speaks 1.3.
490    async fn tls_echo_once_speaking(
491        issuer: &rcgen::Issuer<'static, rcgen::KeyPair>,
492        claims: &str,
493        versions: &[&'static tokio_rustls::rustls::SupportedProtocolVersion],
494    ) -> SocketAddr {
495        let params =
496            rcgen::CertificateParams::new(vec![claims.to_string()]).expect("a subject alt name");
497        let key = rcgen::KeyPair::generate().expect("a key pair");
498        let leaf = params.signed_by(&key, issuer).expect("issued by the CA");
499
500        let config = tokio_rustls::rustls::ServerConfig::builder_with_provider(Arc::new(
501            tokio_rustls::rustls::crypto::aws_lc_rs::default_provider(),
502        ))
503        .with_protocol_versions(versions)
504        .expect("the protocol versions are supported by the provider")
505        .with_no_client_auth()
506        .with_single_cert(
507            vec![leaf.der().clone()],
508            tokio_rustls::rustls::pki_types::PrivateKeyDer::Pkcs8(key.serialize_der().into()),
509        )
510        .expect("a server configuration");
511
512        let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
513            .await
514            .expect("a loopback port");
515        let addr = listener.local_addr().expect("an address");
516        tokio::spawn(async move {
517            let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(config));
518            let (stream, _) = listener.accept().await.expect("a connection");
519            let Ok(stream) = acceptor.accept(stream).await else {
520                return;
521            };
522            let service = hyper::service::service_fn(
523                |_req: hyper::Request<hyper::body::Incoming>| async move {
524                    Ok::<_, std::convert::Infallible>(hyper::Response::new(Full::new(
525                        Bytes::from_static(b"ok, privately"),
526                    )))
527                },
528            );
529            let _ = hyper::server::conn::http1::Builder::new()
530                .serve_connection(hyper_util::rt::TokioIo::new(stream), service)
531                .await;
532        });
533        addr
534    }
535
536    fn secure_request(host: &str, port: u16) -> Request {
537        Request {
538            host: Arc::from(host),
539            port,
540            tls: true,
541            method: Arc::from("GET"),
542            path: Arc::from("/"),
543            headers: Vec::new(),
544            body: Arc::from(""),
545        }
546    }
547
548    /// **A peer that speaks only TLS 1.2 is refused**, which is `docs/12`'s "no legacy downgrade".
549    ///
550    /// The row said that half had no gate. It had no *implementation* either: rustls's safe
551    /// defaults are 1.2 and 1.3, so a client built with them negotiates 1.2 happily, and the
552    /// difference between the claim and the configuration was one unnamed argument. Both clients
553    /// name the version now — this one and `beck-cli`'s `fetch.rs` — and this is the test that
554    /// says so.
555    ///
556    /// The control is the point. A refusal on its own proves nothing: the certificate could be
557    /// wrong, the port could be closed, the name could mismatch. So the same certificate, the same
558    /// trust anchor and the same request go to a 1.3 server and come back with the body, and the
559    /// only difference between the two halves is which version the *server* offers.
560    #[test]
561    fn a_peer_that_speaks_only_tls_1_2_is_refused() {
562        let (issuer, roots) = certificate_authority();
563        let client = HttpOutbound::trusting(roots).expect("a client");
564
565        let legacy = client.runtime.block_on(tls_echo_once_speaking(
566            &issuer,
567            "127.0.0.1",
568            &[&tokio_rustls::rustls::version::TLS12],
569        ));
570        let refused = client.fetch(&secure_request("127.0.0.1", legacy.port()), &Stop::never());
571        assert!(
572            refused.is_err(),
573            "a 1.2-only peer was accepted: {refused:?}"
574        );
575
576        let modern = client.runtime.block_on(tls_echo_once(&issuer, "127.0.0.1"));
577        let reply = client
578            .fetch(&secure_request("127.0.0.1", modern.port()), &Stop::never())
579            .expect("the same certificate over 1.3 is fine");
580        assert_eq!(reply.body.as_ref(), "ok, privately");
581    }
582
583    /// A real handshake, with the name verified.
584    ///
585    /// The host a request carries is both the address it is reached at and the name the
586    /// certificate must answer for, so the certificate is issued for `127.0.0.1` — an IP
587    /// subject-alt-name, which is a name a certificate can carry. That the verification is real
588    /// and not a formality is the second half: the same trust anchor, a certificate for another
589    /// name, refused.
590    #[test]
591    fn tls_verifies_the_name_the_call_site_wrote() {
592        let (issuer, roots) = certificate_authority();
593        let client = HttpOutbound::trusting(roots).expect("a client");
594
595        let addr = client.runtime.block_on(tls_echo_once(&issuer, "127.0.0.1"));
596        let reply = client
597            .fetch(&secure_request("127.0.0.1", addr.port()), &Stop::never())
598            .expect("the handshake completes and the reply comes back");
599        assert_eq!(reply.status, 200);
600        assert_eq!(reply.body.as_ref(), "ok, privately");
601
602        // Same trust anchor, a certificate for somebody else: refused.
603        let (issuer, roots) = certificate_authority();
604        let client = HttpOutbound::trusting(roots).expect("a client");
605        let addr = client
606            .runtime
607            .block_on(tls_echo_once(&issuer, "elsewhere.test"));
608        match client.fetch(&secure_request("127.0.0.1", addr.port()), &Stop::never()) {
609            Err(Failure::Unreachable(why)) => assert!(why.contains("handshake"), "{why}"),
610            other => panic!("a certificate for another name was accepted: {other:?}"),
611        }
612    }
613
614    /// And a peer that is not speaking TLS at all is a failure rather than a hang or a plaintext
615    /// request sent in the clear — which is the mode confusion this field exists to prevent.
616    #[test]
617    fn a_plaintext_peer_does_not_answer_a_request_that_asked_for_tls() {
618        let client = HttpOutbound::new().expect("a client");
619        let addr = client.runtime.block_on(echo_once("seen"));
620        match client.fetch(&secure_request("127.0.0.1", addr.port()), &Stop::never()) {
621            Err(Failure::Unreachable(why)) => assert!(why.contains("handshake"), "{why}"),
622            other => panic!("a plaintext peer answered a TLS request: {other:?}"),
623        }
624    }
625}