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}