Add mutual-TLS to the gossip link

Optional [gossip.tls] wraps the peer link in TLS: each node presents a
certificate and requires the peer to present one signed by the configured
CA. scripts/gen-certs.sh builds a CA and node certs.
This commit is contained in:
Jean Chevronnet 2026-07-12 07:31:16 +00:00
parent cc0265548f
commit 0af7f10c3d
No known key found for this signature in database
5 changed files with 434 additions and 19 deletions

267
Cargo.lock generated
View file

@ -29,6 +29,29 @@ dependencies = [
"password-hash",
]
[[package]]
name = "aws-lc-rs"
version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad"
dependencies = [
"aws-lc-sys",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444"
dependencies = [
"cc",
"cmake",
"dunce",
"fs_extra",
"pkg-config",
]
[[package]]
name = "base64"
version = "0.22.1"
@ -65,12 +88,33 @@ version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cc"
version = "1.2.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cmake"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
"cc",
]
[[package]]
name = "cpufeatures"
version = "0.2.17"
@ -101,6 +145,12 @@ dependencies = [
"subtle",
]
[[package]]
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "equivalent"
version = "1.0.2"
@ -116,16 +166,30 @@ dependencies = [
"base64",
"hmac",
"pbkdf2",
"rustls-pemfile",
"serde",
"serde_json",
"sha2",
"subtle",
"tokio",
"tokio-rustls",
"toml",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "generic-array"
version = "0.14.7"
@ -147,6 +211,17 @@ dependencies = [
"wasi",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
@ -178,6 +253,16 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jobserver"
version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
dependencies = [
"getrandom 0.4.3",
"libc",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@ -219,7 +304,7 @@ checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
dependencies = [
"libc",
"wasi",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@ -228,7 +313,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@ -264,6 +349,12 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkg-config"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "proc-macro2"
version = "1.0.106"
@ -282,13 +373,19 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
"getrandom 0.2.17",
]
[[package]]
@ -308,6 +405,65 @@ version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rustls"
version = "0.23.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
dependencies = [
"aws-lc-rs",
"log",
"once_cell",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
dependencies = [
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "serde"
version = "1.0.228"
@ -380,6 +536,12 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "smallvec"
version = "1.15.2"
@ -393,7 +555,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
dependencies = [
"libc",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@ -434,7 +596,7 @@ dependencies = [
"pin-project-lite",
"socket2",
"tokio-macros",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@ -448,6 +610,16 @@ dependencies = [
"syn",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls",
"tokio",
]
[[package]]
name = "toml"
version = "0.8.23"
@ -562,6 +734,12 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "valuable"
version = "0.1.1"
@ -586,6 +764,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@ -595,6 +782,70 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.15"
@ -604,6 +855,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "zeroize"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
[[package]]
name = "zmij"
version = "1.0.21"

View file

@ -18,3 +18,5 @@ subtle = "2"
anyhow = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tokio-rustls = "0.26.4"
rustls-pemfile = "2.2.0"

23
scripts/gen-certs.sh Executable file
View file

@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Generate a CA and one node certificate for the gossip mutual-TLS link.
# Point every node's [gossip.tls] at ca.crt, and give each its own node cert/key.
# Usage: scripts/gen-certs.sh [out-dir] [node-name]
set -euo pipefail
DIR="${1:-certs}"
NAME="${2:-fedserv}"
mkdir -p "$DIR"
cd "$DIR"
if [ ! -f ca.crt ]; then
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
-keyout ca.key -out ca.crt -days 3650 -subj "/CN=fedserv-ca"
fi
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
-keyout "$NAME.key" -out "$NAME.csr" -subj "/CN=$NAME"
openssl x509 -req -in "$NAME.csr" -CA ca.crt -CAkey ca.key -CAcreateserial \
-out "$NAME.crt" -days 3650 \
-extfile <(printf "subjectAltName=DNS:%s" "$NAME")
rm -f "$NAME.csr" ca.srl
echo "wrote $DIR/{ca.crt, $NAME.crt, $NAME.key}"

View file

@ -17,11 +17,28 @@ pub struct Gossip {
pub bind: Option<String>,
// Shared secret both nodes must present.
pub secret: String,
// Mutual-TLS for the peer link. Absent = plaintext.
#[serde(default)]
pub tls: Option<Tls>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct Tls {
pub cert: String, // our certificate chain (PEM)
pub key: String, // our private key (PEM)
pub ca: String, // CA bundle that must have signed a peer's certificate (PEM)
}
#[derive(Debug, Deserialize, Clone)]
pub struct Peer {
pub addr: String,
// TLS server name to expect from this peer; must match its certificate.
#[serde(default = "default_peer_name")]
pub name: String,
}
fn default_peer_name() -> String {
"fedserv".to_string()
}
#[derive(Debug, Deserialize)]

View file

@ -9,8 +9,11 @@ use serde::{Deserialize, Serialize};
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{broadcast, mpsc, Mutex};
use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
use tokio_rustls::rustls::{self, ClientConfig, RootCertStore, ServerConfig};
use tokio_rustls::{TlsAcceptor, TlsConnector};
use crate::config::{Gossip, Peer};
use crate::config::{Gossip, Peer, Tls};
use crate::engine::db::LogEntry;
use crate::engine::Engine;
@ -29,28 +32,43 @@ enum Msg {
Entry { entry: LogEntry },
}
// Start the listener (if bound) and a dialer per configured peer.
// Start the listener (if bound) and a dialer per configured peer. When TLS is
// configured, every peer link is mutually authenticated against the CA.
pub async fn run(engine: Shared, cfg: Gossip, peers: Vec<Peer>, origin: String, outbound: Outbound) {
let tls = match cfg.tls.as_ref().map(build_tls).transpose() {
Ok(t) => t,
Err(e) => return tracing::error!(%e, "gossip TLS setup failed"),
};
let acceptor = tls.as_ref().map(|(a, _)| a.clone());
let connector = tls.as_ref().map(|(_, c)| c.clone());
if let Some(bind) = cfg.bind.clone() {
tokio::spawn(listen(bind, engine.clone(), cfg.secret.clone(), origin.clone(), outbound.clone()));
tokio::spawn(listen(bind, engine.clone(), cfg.secret.clone(), origin.clone(), outbound.clone(), acceptor));
}
for peer in peers {
tokio::spawn(dial(peer.addr, engine.clone(), cfg.secret.clone(), origin.clone(), outbound.clone()));
tokio::spawn(dial(peer, engine.clone(), cfg.secret.clone(), origin.clone(), outbound.clone(), connector.clone()));
}
}
async fn listen(bind: String, engine: Shared, secret: String, origin: String, outbound: Outbound) {
async fn listen(bind: String, engine: Shared, secret: String, origin: String, outbound: Outbound, acceptor: Option<TlsAcceptor>) {
let listener = match TcpListener::bind(&bind).await {
Ok(l) => l,
Err(e) => return tracing::error!(%e, %bind, "gossip bind failed"),
};
tracing::info!(%bind, "gossip listening");
tracing::info!(%bind, tls = acceptor.is_some(), "gossip listening");
loop {
if let Ok((stream, addr)) = listener.accept().await {
tracing::info!(%addr, "gossip peer accepted");
let (engine, secret, origin, outbound) = (engine.clone(), secret.clone(), origin.clone(), outbound.clone());
let (engine, secret, origin, outbound, acceptor) =
(engine.clone(), secret.clone(), origin.clone(), outbound.clone(), acceptor.clone());
tokio::spawn(async move {
if let Err(e) = session(stream, engine, secret, origin, outbound).await {
let served = match acceptor {
Some(acc) => match acc.accept(stream).await {
Ok(tls) => session(tls, engine, secret, origin, outbound).await,
Err(e) => return tracing::debug!(%e, %addr, "gossip tls accept failed"),
},
None => session(stream, engine, secret, origin, outbound).await,
};
if let Err(e) = served {
tracing::debug!(%e, "gossip session ended");
}
});
@ -58,21 +76,68 @@ async fn listen(bind: String, engine: Shared, secret: String, origin: String, ou
}
}
async fn dial(addr: String, engine: Shared, secret: String, origin: String, outbound: Outbound) {
async fn dial(peer: Peer, engine: Shared, secret: String, origin: String, outbound: Outbound, connector: Option<TlsConnector>) {
loop {
match TcpStream::connect(&addr).await {
match TcpStream::connect(&peer.addr).await {
Ok(stream) => {
tracing::info!(%addr, "gossip dialed peer");
if let Err(e) = session(stream, engine.clone(), secret.clone(), origin.clone(), outbound.clone()).await {
tracing::debug!(%e, %addr, "gossip session ended");
tracing::info!(addr = %peer.addr, "gossip dialed peer");
let served = match &connector {
Some(conn) => match ServerName::try_from(peer.name.clone()) {
Ok(name) => match conn.connect(name, stream).await {
Ok(tls) => session(tls, engine.clone(), secret.clone(), origin.clone(), outbound.clone()).await,
Err(e) => Err(anyhow::anyhow!("tls connect: {e}")),
},
Err(e) => Err(anyhow::anyhow!("bad peer TLS name {:?}: {e}", peer.name)),
},
None => session(stream, engine.clone(), secret.clone(), origin.clone(), outbound.clone()).await,
};
if let Err(e) = served {
tracing::debug!(%e, addr = %peer.addr, "gossip session ended");
}
}
Err(e) => tracing::debug!(%e, %addr, "gossip dial failed"),
Err(e) => tracing::debug!(%e, addr = %peer.addr, "gossip dial failed"),
}
tokio::time::sleep(REDIAL).await;
}
}
// Build a mutual-TLS acceptor + connector from PEM files: we present our cert and
// require the peer to present one signed by the configured CA.
fn build_tls(tls: &Tls) -> anyhow::Result<(TlsAcceptor, TlsConnector)> {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let certs = load_certs(&tls.cert)?;
let key = load_key(&tls.key)?;
let roots = load_roots(&tls.ca)?;
let verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(roots.clone())).build()?;
let server = ServerConfig::builder()
.with_client_cert_verifier(verifier)
.with_single_cert(certs.clone(), key.clone_key())?;
let client = ClientConfig::builder()
.with_root_certificates(roots)
.with_client_auth_cert(certs, key)?;
Ok((TlsAcceptor::from(Arc::new(server)), TlsConnector::from(Arc::new(client))))
}
fn load_certs(path: &str) -> anyhow::Result<Vec<CertificateDer<'static>>> {
let data = std::fs::read(path)?;
Ok(rustls_pemfile::certs(&mut &data[..]).collect::<Result<Vec<_>, _>>()?)
}
fn load_key(path: &str) -> anyhow::Result<PrivateKeyDer<'static>> {
let data = std::fs::read(path)?;
rustls_pemfile::private_key(&mut &data[..])?.ok_or_else(|| anyhow::anyhow!("no private key in {path}"))
}
fn load_roots(path: &str) -> anyhow::Result<RootCertStore> {
let mut roots = RootCertStore::empty();
for cert in load_certs(path)? {
roots.add(cert)?;
}
Ok(roots)
}
// One peer connection: authenticate, then run anti-entropy until it drops.
async fn session<S>(stream: S, engine: Shared, secret: String, origin: String, outbound: Outbound) -> anyhow::Result<()>
where
@ -262,4 +327,55 @@ mod tests {
.await;
assert!(!b.lock().await.test_has_account("alice"), "no state should cross a bad handshake");
}
// Generate a CA + node cert with openssl into a temp dir; None if unavailable.
fn gen_certs() -> Option<String> {
use std::process::Command;
let dir = std::env::temp_dir().join(format!("fedserv-tls-{}", std::process::id()));
std::fs::create_dir_all(&dir).ok()?;
let d = dir.to_str()?.to_string();
let run = |args: &[&str]| Command::new("openssl").args(args).status().map(|s| s.success()).unwrap_or(false);
let ok = run(&["req", "-x509", "-newkey", "ec", "-pkeyopt", "ec_paramgen_curve:P-256", "-nodes",
"-keyout", &format!("{d}/ca.key"), "-out", &format!("{d}/ca.crt"), "-days", "1", "-subj", "/CN=fedserv-ca"])
&& run(&["req", "-newkey", "ec", "-pkeyopt", "ec_paramgen_curve:P-256", "-nodes",
"-keyout", &format!("{d}/node.key"), "-out", &format!("{d}/node.csr"), "-subj", "/CN=fedserv"]);
if !ok {
return None;
}
std::fs::write(format!("{d}/ext"), "subjectAltName=DNS:fedserv,IP:127.0.0.1\n").ok()?;
run(&["x509", "-req", "-in", &format!("{d}/node.csr"), "-CA", &format!("{d}/ca.crt"),
"-CAkey", &format!("{d}/ca.key"), "-CAcreateserial", "-out", &format!("{d}/node.crt"),
"-days", "1", "-extfile", &format!("{d}/ext")])
.then_some(d)
}
// Convergence over a mutually authenticated TLS link. Skips if openssl is absent.
#[tokio::test]
async fn tls_link_converges() {
let Some(d) = gen_certs() else { return };
let tls = Tls { cert: format!("{d}/node.crt"), key: format!("{d}/node.key"), ca: format!("{d}/ca.crt") };
let (acceptor, connector) = build_tls(&tls).expect("tls config");
let (a, atx) = engine("A", "tls-a");
let (b, btx) = engine("B", "tls-b");
a.lock().await.test_register("alice");
let (sside, cside) = tokio::io::duplex(64 * 1024);
let name = ServerName::try_from("fedserv").unwrap();
let (server, client) = tokio::join!(acceptor.accept(sside), connector.connect(name, cside));
let sa = tokio::spawn(session(server.expect("tls accept"), a.clone(), "s3cret".into(), "A".into(), atx));
let sb = tokio::spawn(session(client.expect("tls connect"), b.clone(), "s3cret".into(), "B".into(), btx));
let mut converged = false;
for _ in 0..100 {
if b.lock().await.test_has_account("alice") {
converged = true;
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
sa.abort();
sb.abort();
assert!(converged, "B should converge over the TLS link");
}
}