hyperactor_mesh/
mesh_admin_client.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Shared TLS-aware `reqwest` client construction for mesh-admin
10//! HTTP clients.
11//!
12//! The mesh admin server requires mutual TLS. Building a correct
13//! `reqwest::Client` against it is subtle because fbcode Buck builds
14//! compile both `native-tls` and `rustls` features into reqwest, and
15//! the two backends use incompatible `Identity` constructors.
16//!
17//! This module centralizes that logic so that every mesh-admin client
18//! (admin TUI, integration tests, future tooling) shares the same
19//! correct code path.
20
21/// Configure TLS on a `reqwest::ClientBuilder` by adding a root CA,
22/// and optionally a client identity (cert + key) for mutual TLS.
23///
24/// - `ca_bytes` must be a PEM-encoded CA certificate; if it cannot be
25///   parsed, this returns `(builder, false)` and leaves the builder
26///   unchanged.
27/// - If both `cert_bytes` and `key_bytes` are provided, they are
28///   concatenated and parsed as a PEM identity. Identity parse
29///   failures are non-fatal: the root CA remains installed and the
30///   function still returns `true`.
31///
32/// Returns `(updated_builder, ca_installed)`.
33pub fn add_tls(
34    builder: reqwest::ClientBuilder,
35    ca_bytes: &[u8],
36    cert_bytes: Option<Vec<u8>>,
37    key_bytes: Option<Vec<u8>>,
38) -> (reqwest::ClientBuilder, bool) {
39    let root_cert = match reqwest::Certificate::from_pem(ca_bytes) {
40        Ok(c) => c,
41        Err(e) => {
42            eprintln!("TLS: invalid CA PEM: {}", e);
43            return (builder, false);
44        }
45    };
46    let mut builder = builder.add_root_certificate(root_cert);
47
48    if let (Some(cert), Some(key)) = (cert_bytes, key_bytes) {
49        // reqwest's Identity type is backend-specific: from_pkcs8_pem
50        // creates a native-tls identity, from_pem creates a rustls
51        // identity. When both features are compiled (fbcode Buck builds),
52        // using the wrong variant silently fails at connect time with
53        // "incompatible TLS identity type".
54        //
55        // Meta's server.pem bundles certs + key in one file.
56        // from_pkcs8_pem requires the key as a separate buffer, so we
57        // split it out by finding the private key marker.
58        let combined = if cert == key {
59            cert
60        } else {
61            let mut c = cert;
62            c.extend_from_slice(&key);
63            c
64        };
65        let identity_result = {
66            // Split PEM into cert-only and key-only buffers for native-tls.
67            // reqwest 0.11 with both native-tls and rustls features compiled
68            // (fbcode Buck builds) defaults to the native-tls connector.
69            // Identity::from_pem creates a rustls-flavored identity that is
70            // silently rejected by native-tls at connect time. We must use
71            // from_pkcs8_pem (native-tls) in fbcode, and from_pem (rustls)
72            // in OSS where native-tls is excluded (D93626607).
73            let combined_str = String::from_utf8_lossy(&combined);
74            let key_markers = [
75                // @lint-ignore PRIVATEKEY
76                "-----BEGIN PRIVATE KEY-----",
77                // @lint-ignore PRIVATEKEY
78                "-----BEGIN RSA PRIVATE KEY-----",
79                // @lint-ignore PRIVATEKEY
80                "-----BEGIN EC PRIVATE KEY-----",
81            ];
82            let key_pos = key_markers
83                .iter()
84                .filter_map(|m| combined_str.find(m))
85                .min();
86            #[cfg(fbcode_build)]
87            {
88                if let Some(key_start) = key_pos {
89                    let cert_pem = combined_str[..key_start].trim().as_bytes();
90                    let key_pem = combined_str[key_start..].trim().as_bytes();
91                    reqwest::Identity::from_pkcs8_pem(cert_pem, key_pem)
92                } else {
93                    reqwest::Identity::from_pem(&combined)
94                }
95            }
96            #[cfg(not(fbcode_build))]
97            {
98                let _ = key_pos; // suppress unused warning
99                reqwest::Identity::from_pem(&combined)
100            }
101        };
102        match identity_result {
103            Ok(identity) => {
104                builder = builder.identity(identity);
105            }
106            Err(e) => eprintln!(
107                "WARNING: TLS: failed to parse client identity PEM: {}. \
108                 The mesh admin server requires mTLS — connection will fail \
109                 without a valid client certificate.",
110                e
111            ),
112        }
113    }
114
115    (builder, true)
116}