From 0f58cdbde2ca7c27162206c5edd9bb99217c0b92 Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 26 Sep 2026 19:57:01 +0200 Subject: [PATCH] swarm-queue-client: install aws-lc-rs as the process rustls provider rustls is built with both `ring` (async-nats's `ring` feature) and `aws-lc-rs` (reqwest's `rustls` feature), so it cannot pick a process-level default by itself. Since the queue started requiring TLS (1d261b3f), async-nats builds its config with `ClientConfig::builder()`, which panics without an installed default. The panic kills the async-nats connector task, and every queue client (swarm-controller, hive-c0re, all hive-agents) has sat in `Pending` since the 2026-09-25 23:04Z deploy. Add `swarm_queue_client::install_crypto_provider()`, which installs aws-lc-rs and ignores the "already installed" error. It is called first in `main` of every binary that links async-nats: hive-agent, hive-c0re, swarm-controller, swarm-nats-auth. `connect()` also calls it, so a new binary that dials through this crate is covered without remembering to. aws-lc-rs because reqwest already falls back to it when no default is installed, so HTTPS in these processes keeps its current provider. The other rustls users in the tree reach it only through reqwest, which never panics here. Closes #4738 --- Cargo.lock | 1 + Cargo.toml | 7 +++++++ hive-agent/src/main.rs | 1 + hive-c0re/src/main.rs | 1 + swarm-controller/src/main.rs | 1 + swarm-nats-auth/src/main.rs | 1 + swarm-queue-client/Cargo.toml | 1 + swarm-queue-client/src/lib.rs | 21 +++++++++++++++++++++ 8 files changed, 34 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 21b3cd8b..35c9cc36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4987,6 +4987,7 @@ version = "0.1.0" dependencies = [ "async-nats", "reqwest", + "rustls", "serde", "serde_json", "strum", diff --git a/Cargo.toml b/Cargo.toml index d01321e2..2dd60d00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -254,6 +254,13 @@ async-nats = { version = "0.50", default-features = false, features = [ "nkeys", "ring", ] } +# Named only to install the process-wide provider (`swarm-queue-client`'s +# `install_crypto_provider`). `aws-lc-rs` is the provider reqwest already +# falls back to, so installing it leaves every HTTPS client unchanged. +rustls = { version = "0.23", default-features = false, features = [ + "aws-lc-rs", + "std", +] } data-encoding = "2" # The nkey *format* - ed25519 + base32 + CRC16. The primitives are already in # the tree; the format is not, and hand-rolling a key format on an auth path diff --git a/hive-agent/src/main.rs b/hive-agent/src/main.rs index 6756ff90..ac1e0ca2 100644 --- a/hive-agent/src/main.rs +++ b/hive-agent/src/main.rs @@ -104,6 +104,7 @@ struct Cli { #[tokio::main] async fn main() -> Result<()> { hive_log::init(); + swarm_queue_client::install_crypto_provider(); let cli = Cli::parse(); serve_main::(&cli.socket, cli.poll_ms).await diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 13c29b58..8855946a 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -144,6 +144,7 @@ enum Cmd { #[tokio::main] async fn main() -> Result<()> { hive_log::init(); + swarm_queue_client::install_crypto_provider(); let cli = Cli::parse(); match cli.cmd { diff --git a/swarm-controller/src/main.rs b/swarm-controller/src/main.rs index cadeee0f..b7edb801 100644 --- a/swarm-controller/src/main.rs +++ b/swarm-controller/src/main.rs @@ -2242,6 +2242,7 @@ fn keep_forge_for_state( #[tokio::main] async fn main() -> Result<()> { hive_log::init(); + swarm_queue_client::install_crypto_provider(); let path = socket_path(); diff --git a/swarm-nats-auth/src/main.rs b/swarm-nats-auth/src/main.rs index 58dc5c8b..bfee3d5e 100644 --- a/swarm-nats-auth/src/main.rs +++ b/swarm-nats-auth/src/main.rs @@ -147,6 +147,7 @@ fn read_secret(path: &std::path::Path) -> anyhow::Result { #[tokio::main] async fn main() -> anyhow::Result<()> { + swarm_queue_client::install_crypto_provider(); tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() diff --git a/swarm-queue-client/Cargo.toml b/swarm-queue-client/Cargo.toml index 6b973d1a..b4dd258a 100644 --- a/swarm-queue-client/Cargo.toml +++ b/swarm-queue-client/Cargo.toml @@ -45,6 +45,7 @@ async-nats.workspace = true # feature's comment above), and `cargo check -p swarm-queue-client` alone # must not depend on what else is in the build. reqwest = { workspace = true, features = ["blocking"] } +rustls.workspace = true serde.workspace = true serde_json.workspace = true strum.workspace = true diff --git a/swarm-queue-client/src/lib.rs b/swarm-queue-client/src/lib.rs index a144b634..077eca8b 100644 --- a/swarm-queue-client/src/lib.rs +++ b/swarm-queue-client/src/lib.rs @@ -656,6 +656,18 @@ pub fn max_payload(client: &async_nats::Client) -> usize { client.server_info().max_payload } +/// Install `aws-lc-rs` as the process-wide rustls crypto provider. Call it +/// first thing in `main` of every binary that dials the queue. +/// +/// rustls is built with both `ring` and `aws-lc-rs`; without an installed +/// default, the first TLS handshake panics (async-nats builds its config with +/// `ClientConfig::builder()`) and the client never connects. `aws-lc-rs` +/// because it is what reqwest uses when nothing is installed. +pub fn install_crypto_provider() { + // `Err` only means a provider is already installed, which is all this needs. + let _already_installed = rustls::crypto::aws_lc_rs::default_provider().install_default(); +} + /// Connect to the swarm queue, presenting a token on each connection attempt /// and minting a fresh one only when the cached one is near expiry. /// @@ -664,6 +676,7 @@ pub fn max_payload(client: &async_nats::Client) -> usize { /// token expires, the controller keeps serving, its status data quietly stops /// updating, and nothing says so until someone reads a dashboard. pub async fn connect(cfg: QueueConfig) -> Result { + install_crypto_provider(); // Trusts `cfg.ca_file` when set (the swarm's own CA, when the token // endpoint is signed by it) — see `build_http_client`. A timeout too, // because this client runs INSIDE the auth callback: a token endpoint @@ -771,6 +784,14 @@ pub async fn connect(cfg: QueueConfig) -> Result { mod tests { use super::*; + #[test] + fn a_tls_client_config_builds_once_the_provider_is_installed() { + install_crypto_provider(); + let _config = rustls::ClientConfig::builder() + .with_root_certificates(rustls::RootCertStore::empty()) + .with_no_client_auth(); + } + /// The all-unset case is the common one — most hosts do not run the queue. /// /// Uses a prefix no deployment sets, so it cannot pass vacuously by