1use std::collections::BTreeMap;
53use std::fs::File;
54use std::io::{self, BufReader};
55use std::net::SocketAddr;
56use std::path::{Path, PathBuf};
57use std::pin::Pin;
58use std::sync::{Arc, OnceLock};
59use std::task::{Context, Poll};
60
61use parking_lot::RwLock;
62use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
63use rustls::server::{ClientHello, ResolvesServerCert, WebPkiClientVerifier};
64use rustls::sign::CertifiedKey;
65use rustls::{ClientConfig, RootCertStore, ServerConfig};
66use thiserror::Error;
67use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
68use tokio::net::TcpStream;
69use tokio_rustls::{TlsAcceptor, TlsConnector};
70
71use crate::io::reactor::{ConnRole, Transport};
72
73#[derive(Debug, Error)]
75pub enum TlsError {
76 #[error("tls: io reading {path}: {source}")]
78 Io {
79 path: String,
81 #[source]
83 source: io::Error,
84 },
85 #[error("tls: no usable {kind} found in {path}")]
87 NoMaterial {
88 kind: &'static str,
90 path: String,
92 },
93 #[error("tls: rustls rejected configuration: {0}")]
95 Rustls(String),
96}
97
98fn ensure_provider_installed() {
103 static INSTALL: OnceLock<()> = OnceLock::new();
104 INSTALL.get_or_init(|| {
105 let _ = rustls::crypto::ring::default_provider().install_default();
111 });
112}
113
114fn load_certs(path: &Path) -> Result<Vec<CertificateDer<'static>>, TlsError> {
115 let file = File::open(path).map_err(|e| TlsError::Io {
116 path: path.display().to_string(),
117 source: e,
118 })?;
119 let mut reader = BufReader::new(file);
120 let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut reader)
121 .collect::<io::Result<Vec<_>>>()
122 .map_err(|e| TlsError::Io {
123 path: path.display().to_string(),
124 source: e,
125 })?;
126 if certs.is_empty() {
127 return Err(TlsError::NoMaterial {
128 kind: "certificate",
129 path: path.display().to_string(),
130 });
131 }
132 Ok(certs)
133}
134
135fn load_private_key(path: &Path) -> Result<PrivateKeyDer<'static>, TlsError> {
136 let file = File::open(path).map_err(|e| TlsError::Io {
137 path: path.display().to_string(),
138 source: e,
139 })?;
140 let mut reader = BufReader::new(file);
141 let key = rustls_pemfile::private_key(&mut reader).map_err(|e| TlsError::Io {
142 path: path.display().to_string(),
143 source: e,
144 })?;
145 key.ok_or_else(|| TlsError::NoMaterial {
146 kind: "private key",
147 path: path.display().to_string(),
148 })
149}
150
151pub fn load_server_config(
162 cert_path: &Path,
163 key_path: &Path,
164 client_ca: Option<&Path>,
165) -> Result<Arc<ServerConfig>, TlsError> {
166 ensure_provider_installed();
167 let certs = load_certs(cert_path)?;
168 let key = load_private_key(key_path)?;
169
170 let builder = ServerConfig::builder();
171 let cfg = if let Some(ca_path) = client_ca {
172 let ca_certs = load_certs(ca_path)?;
173 let mut roots = RootCertStore::empty();
174 for c in ca_certs {
175 roots
176 .add(c)
177 .map_err(|e| TlsError::Rustls(format!("ca add: {e}")))?;
178 }
179 let verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(roots))
180 .build()
181 .map_err(|e| TlsError::Rustls(format!("client verifier: {e}")))?;
182 builder
183 .with_client_cert_verifier(verifier)
184 .with_single_cert(certs, key)
185 .map_err(|e| TlsError::Rustls(e.to_string()))?
186 } else {
187 builder
188 .with_no_client_auth()
189 .with_single_cert(certs, key)
190 .map_err(|e| TlsError::Rustls(e.to_string()))?
191 };
192 Ok(Arc::new(cfg))
193}
194
195pub fn load_client_config(ca_path: Option<&Path>) -> Result<Arc<ClientConfig>, TlsError> {
207 ensure_provider_installed();
208 let mut roots = RootCertStore::empty();
209 if let Some(p) = ca_path {
210 let ca_certs = load_certs(p)?;
211 for c in ca_certs {
212 roots
213 .add(c)
214 .map_err(|e| TlsError::Rustls(format!("ca add: {e}")))?;
215 }
216 } else {
217 roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
218 }
219 let cfg = ClientConfig::builder()
220 .with_root_certificates(roots)
221 .with_no_client_auth();
222 Ok(Arc::new(cfg))
223}
224
225#[must_use]
228pub fn acceptor_from(server_config: Arc<ServerConfig>) -> TlsAcceptor {
229 TlsAcceptor::from(server_config)
230}
231
232#[must_use]
235pub fn connector_from(client_config: Arc<ClientConfig>) -> TlsConnector {
236 TlsConnector::from(client_config)
237}
238
239pub fn server_name_owned(host: &str) -> Result<ServerName<'static>, TlsError> {
245 ServerName::try_from(host.to_string())
246 .map_err(|e| TlsError::Rustls(format!("server name: {e}")))
247}
248
249#[must_use]
264pub fn dc_sni_hostname(dc: &str) -> String {
265 format!("dc-{dc}.dynomite.local")
266}
267
268fn dc_from_sni_label(name: &str) -> Option<&str> {
272 name.strip_prefix("dc-")
273 .and_then(|rest| rest.strip_suffix(".dynomite.local"))
274 .filter(|dc| !dc.is_empty())
275}
276
277#[derive(Debug, Clone)]
285pub struct TlsProfileSpec {
286 pub cert: PathBuf,
288 pub key: PathBuf,
290 pub ca: Option<PathBuf>,
292}
293
294#[derive(Clone, Default)]
325pub struct TlsProfileMap {
326 per_dc_server: BTreeMap<String, Arc<ServerConfig>>,
327 per_dc_client: BTreeMap<String, Arc<ClientConfig>>,
328 per_dc_certified: BTreeMap<String, Arc<CertifiedKey>>,
329 default_server: Option<Arc<ServerConfig>>,
330 default_client: Option<Arc<ClientConfig>>,
331 default_certified: Option<Arc<CertifiedKey>>,
332 combined_ca_certs: Vec<CertificateDer<'static>>,
337 has_any_client_ca: bool,
338}
339
340impl std::fmt::Debug for TlsProfileMap {
341 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342 f.debug_struct("TlsProfileMap")
343 .field("per_dc", &self.per_dc_server.keys().collect::<Vec<_>>())
344 .field("has_default", &self.default_server.is_some())
345 .field("has_any_client_ca", &self.has_any_client_ca)
346 .finish_non_exhaustive()
347 }
348}
349
350impl TlsProfileMap {
351 pub fn build(
361 default: Option<TlsProfileSpec>,
362 per_dc: BTreeMap<String, TlsProfileSpec>,
363 ) -> Result<Self, TlsError> {
364 ensure_provider_installed();
365 let provider = rustls::crypto::CryptoProvider::get_default()
366 .cloned()
367 .unwrap_or_else(|| Arc::new(rustls::crypto::ring::default_provider()));
368
369 let mut map = Self::default();
370
371 if let Some(spec) = default {
372 let server_cfg = load_server_config(&spec.cert, &spec.key, spec.ca.as_deref())?;
373 let client_cfg = load_client_config(spec.ca.as_deref())?;
374 let certified = load_certified_key(&spec.cert, &spec.key, provider.as_ref())?;
375 if let Some(ca_path) = spec.ca.as_deref() {
376 map.combined_ca_certs.extend(load_certs(ca_path)?);
377 map.has_any_client_ca = true;
378 }
379 map.default_server = Some(server_cfg);
380 map.default_client = Some(client_cfg);
381 map.default_certified = Some(certified);
382 }
383
384 for (dc, spec) in per_dc {
385 let server_cfg = load_server_config(&spec.cert, &spec.key, spec.ca.as_deref())?;
386 let client_cfg = load_client_config(spec.ca.as_deref())?;
387 let certified = load_certified_key(&spec.cert, &spec.key, provider.as_ref())?;
388 if let Some(ca_path) = spec.ca.as_deref() {
389 map.combined_ca_certs.extend(load_certs(ca_path)?);
390 map.has_any_client_ca = true;
391 }
392 map.per_dc_server.insert(dc.clone(), server_cfg);
393 map.per_dc_client.insert(dc.clone(), client_cfg);
394 map.per_dc_certified.insert(dc, certified);
395 }
396
397 Ok(map)
398 }
399
400 #[must_use]
403 pub fn is_empty(&self) -> bool {
404 self.default_server.is_none() && self.per_dc_server.is_empty()
405 }
406
407 #[must_use]
411 pub fn server_config_for_dc(&self, dc: &str) -> Option<Arc<ServerConfig>> {
412 self.per_dc_server
413 .get(dc)
414 .cloned()
415 .or_else(|| self.default_server.clone())
416 }
417
418 #[must_use]
422 pub fn client_config_for_dc(&self, dc: &str) -> Option<Arc<ClientConfig>> {
423 self.per_dc_client
424 .get(dc)
425 .cloned()
426 .or_else(|| self.default_client.clone())
427 }
428
429 #[must_use]
431 pub fn default_server_config(&self) -> Option<Arc<ServerConfig>> {
432 self.default_server.clone()
433 }
434
435 #[must_use]
437 pub fn default_client_config(&self) -> Option<Arc<ClientConfig>> {
438 self.default_client.clone()
439 }
440
441 #[must_use]
446 pub fn requires_client_auth(&self) -> bool {
447 self.has_any_client_ca
448 }
449
450 #[must_use]
452 pub fn dc_names(&self) -> Vec<String> {
453 self.per_dc_certified.keys().cloned().collect()
454 }
455
456 pub fn build_sni_acceptor(&self) -> Result<Option<tokio_rustls::TlsAcceptor>, TlsError> {
468 if self.is_empty() {
469 return Ok(None);
470 }
471 ensure_provider_installed();
472 let resolver = DcSniResolver {
473 by_dc: self.per_dc_certified.clone(),
474 default: self.default_certified.clone(),
475 };
476 let builder = ServerConfig::builder();
477 let cfg = if self.has_any_client_ca {
478 let mut roots = RootCertStore::empty();
484 self.populate_combined_ca_roots(&mut roots)?;
485 let verifier = WebPkiClientVerifier::builder(Arc::new(roots))
486 .build()
487 .map_err(|e| TlsError::Rustls(format!("client verifier: {e}")))?;
488 builder
489 .with_client_cert_verifier(verifier)
490 .with_cert_resolver(Arc::new(resolver))
491 } else {
492 builder
493 .with_no_client_auth()
494 .with_cert_resolver(Arc::new(resolver))
495 };
496 Ok(Some(tokio_rustls::TlsAcceptor::from(Arc::new(cfg))))
497 }
498
499 fn populate_combined_ca_roots(&self, roots: &mut RootCertStore) -> Result<(), TlsError> {
504 for cert in &self.combined_ca_certs {
505 roots
506 .add(cert.clone())
507 .map_err(|e| TlsError::Rustls(format!("ca add: {e}")))?;
508 }
509 Ok(())
510 }
511}
512
513#[derive(Debug)]
518struct DcSniResolver {
519 by_dc: BTreeMap<String, Arc<CertifiedKey>>,
520 default: Option<Arc<CertifiedKey>>,
521}
522
523impl ResolvesServerCert for DcSniResolver {
524 fn resolve(&self, hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
525 if let Some(name) = hello.server_name() {
526 if let Some(dc) = dc_from_sni_label(name) {
527 if let Some(ck) = self.by_dc.get(dc) {
528 return Some(ck.clone());
529 }
530 }
531 }
532 self.default.clone()
533 }
534}
535
536#[derive(Debug)]
542struct ReloadingDcSniResolver {
543 profiles: Arc<RwLock<TlsProfileMap>>,
544}
545
546impl ResolvesServerCert for ReloadingDcSniResolver {
547 fn resolve(&self, hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
548 let profiles = self.profiles.read();
549 if let Some(name) = hello.server_name() {
550 if let Some(dc) = dc_from_sni_label(name) {
551 if let Some(ck) = profiles.per_dc_certified.get(dc) {
552 return Some(ck.clone());
553 }
554 }
555 }
556 profiles.default_certified.clone()
557 }
558}
559
560#[derive(Clone, Debug, Default)]
582pub struct SharedTlsProfiles {
583 inner: Arc<RwLock<TlsProfileMap>>,
584}
585
586impl SharedTlsProfiles {
587 #[must_use]
589 pub fn from_map(map: TlsProfileMap) -> Self {
590 Self {
591 inner: Arc::new(RwLock::new(map)),
592 }
593 }
594
595 pub fn replace(&self, map: TlsProfileMap) {
601 *self.inner.write() = map;
602 }
603
604 #[must_use]
606 pub fn is_empty(&self) -> bool {
607 self.inner.read().is_empty()
608 }
609
610 #[must_use]
613 pub fn client_config_for_dc(&self, dc: &str) -> Option<Arc<ClientConfig>> {
614 self.inner.read().client_config_for_dc(dc)
615 }
616
617 #[must_use]
619 pub fn requires_client_auth(&self) -> bool {
620 self.inner.read().requires_client_auth()
621 }
622
623 #[must_use]
625 pub fn dc_names(&self) -> Vec<String> {
626 self.inner.read().dc_names()
627 }
628
629 pub fn build_sni_acceptor(&self) -> Result<Option<TlsAcceptor>, TlsError> {
645 if self.is_empty() {
646 return Ok(None);
647 }
648 ensure_provider_installed();
649 let resolver = ReloadingDcSniResolver {
650 profiles: self.inner.clone(),
651 };
652 let has_any_client_ca = self.inner.read().has_any_client_ca;
653 let builder = ServerConfig::builder();
654 let cfg = if has_any_client_ca {
655 let mut roots = RootCertStore::empty();
656 self.inner.read().populate_combined_ca_roots(&mut roots)?;
657 let verifier = WebPkiClientVerifier::builder(Arc::new(roots))
658 .build()
659 .map_err(|e| TlsError::Rustls(format!("client verifier: {e}")))?;
660 builder
661 .with_client_cert_verifier(verifier)
662 .with_cert_resolver(Arc::new(resolver))
663 } else {
664 builder
665 .with_no_client_auth()
666 .with_cert_resolver(Arc::new(resolver))
667 };
668 Ok(Some(TlsAcceptor::from(Arc::new(cfg))))
669 }
670}
671
672fn load_certified_key(
673 cert_path: &Path,
674 key_path: &Path,
675 provider: &rustls::crypto::CryptoProvider,
676) -> Result<Arc<CertifiedKey>, TlsError> {
677 let certs = load_certs(cert_path)?;
678 let key = load_private_key(key_path)?;
679 let ck = CertifiedKey::from_der(certs, key, provider)
680 .map_err(|e| TlsError::Rustls(format!("certified key: {e}")))?;
681 Ok(Arc::new(ck))
682}
683
684#[derive(Debug)]
687pub struct TlsServerTransport {
688 inner: tokio_rustls::server::TlsStream<TcpStream>,
689 role: ConnRole,
690 peer_addr: Option<SocketAddr>,
691}
692
693impl TlsServerTransport {
694 #[must_use]
696 pub fn new(stream: tokio_rustls::server::TlsStream<TcpStream>, role: ConnRole) -> Self {
697 let peer_addr = stream.get_ref().0.peer_addr().ok();
698 Self {
699 inner: stream,
700 role,
701 peer_addr,
702 }
703 }
704}
705
706impl Transport for TlsServerTransport {
707 fn role(&self) -> ConnRole {
708 self.role
709 }
710 fn peer_addr(&self) -> Option<SocketAddr> {
711 self.peer_addr
712 }
713}
714
715impl AsyncRead for TlsServerTransport {
716 fn poll_read(
717 mut self: Pin<&mut Self>,
718 cx: &mut Context<'_>,
719 buf: &mut ReadBuf<'_>,
720 ) -> Poll<io::Result<()>> {
721 Pin::new(&mut self.inner).poll_read(cx, buf)
722 }
723}
724
725impl AsyncWrite for TlsServerTransport {
726 fn poll_write(
727 mut self: Pin<&mut Self>,
728 cx: &mut Context<'_>,
729 buf: &[u8],
730 ) -> Poll<io::Result<usize>> {
731 Pin::new(&mut self.inner).poll_write(cx, buf)
732 }
733 fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
734 Pin::new(&mut self.inner).poll_flush(cx)
735 }
736 fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
737 Pin::new(&mut self.inner).poll_shutdown(cx)
738 }
739}
740
741#[derive(Debug)]
744pub struct TlsClientTransport {
745 inner: tokio_rustls::client::TlsStream<TcpStream>,
746 role: ConnRole,
747 peer_addr: Option<SocketAddr>,
748}
749
750impl TlsClientTransport {
751 #[must_use]
753 pub fn new(stream: tokio_rustls::client::TlsStream<TcpStream>, role: ConnRole) -> Self {
754 let peer_addr = stream.get_ref().0.peer_addr().ok();
755 Self {
756 inner: stream,
757 role,
758 peer_addr,
759 }
760 }
761}
762
763impl Transport for TlsClientTransport {
764 fn role(&self) -> ConnRole {
765 self.role
766 }
767 fn peer_addr(&self) -> Option<SocketAddr> {
768 self.peer_addr
769 }
770}
771
772impl AsyncRead for TlsClientTransport {
773 fn poll_read(
774 mut self: Pin<&mut Self>,
775 cx: &mut Context<'_>,
776 buf: &mut ReadBuf<'_>,
777 ) -> Poll<io::Result<()>> {
778 Pin::new(&mut self.inner).poll_read(cx, buf)
779 }
780}
781
782impl AsyncWrite for TlsClientTransport {
783 fn poll_write(
784 mut self: Pin<&mut Self>,
785 cx: &mut Context<'_>,
786 buf: &[u8],
787 ) -> Poll<io::Result<usize>> {
788 Pin::new(&mut self.inner).poll_write(cx, buf)
789 }
790 fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
791 Pin::new(&mut self.inner).poll_flush(cx)
792 }
793 fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
794 Pin::new(&mut self.inner).poll_shutdown(cx)
795 }
796}
797
798#[cfg(test)]
799mod tests {
800 use super::*;
801 use std::io::Write;
802 use tempfile::TempDir;
803
804 fn write_pem(dir: &TempDir, name: &str, body: &str) -> std::path::PathBuf {
805 let p = dir.path().join(name);
806 let mut f = File::create(&p).unwrap();
807 f.write_all(body.as_bytes()).unwrap();
808 p
809 }
810
811 fn issue_self_signed() -> (String, String) {
812 let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap();
813 (cert.cert.pem(), cert.signing_key.serialize_pem())
814 }
815
816 #[test]
817 fn load_server_config_round_trip() {
818 let dir = tempfile::tempdir().unwrap();
819 let (cert_pem, key_pem) = issue_self_signed();
820 let cert = write_pem(&dir, "cert.pem", &cert_pem);
821 let key = write_pem(&dir, "key.pem", &key_pem);
822 let cfg = load_server_config(&cert, &key, None).unwrap();
823 assert!(Arc::strong_count(&cfg) >= 1);
824 }
825
826 #[test]
827 fn load_server_config_rejects_missing_cert() {
828 let dir = tempfile::tempdir().unwrap();
829 let bogus = dir.path().join("missing.pem");
830 let key = write_pem(&dir, "key.pem", "");
831 let err = load_server_config(&bogus, &key, None).expect_err("missing");
832 assert!(matches!(err, TlsError::Io { .. }), "got {err:?}");
833 }
834
835 #[test]
836 fn load_server_config_rejects_empty_cert_file() {
837 let dir = tempfile::tempdir().unwrap();
838 let cert = write_pem(&dir, "cert.pem", "");
839 let key = write_pem(&dir, "key.pem", "");
840 let err = load_server_config(&cert, &key, None).expect_err("empty");
841 assert!(matches!(
842 err,
843 TlsError::NoMaterial {
844 kind: "certificate",
845 ..
846 }
847 ));
848 }
849
850 #[test]
851 fn load_client_config_with_webpki_default() {
852 let cfg = load_client_config(None).unwrap();
853 assert!(Arc::strong_count(&cfg) >= 1);
854 }
855
856 #[test]
857 fn server_name_owned_accepts_dns_label() {
858 assert!(server_name_owned("localhost").is_ok());
859 }
860
861 fn write_self_signed(dir: &TempDir, prefix: &str) -> (std::path::PathBuf, std::path::PathBuf) {
862 let (cert_pem, key_pem) = issue_self_signed();
863 (
864 write_pem(dir, &format!("{prefix}-cert.pem"), &cert_pem),
865 write_pem(dir, &format!("{prefix}-key.pem"), &key_pem),
866 )
867 }
868
869 #[test]
870 fn dc_sni_hostname_round_trips() {
871 assert_eq!(dc_sni_hostname("dc1"), "dc-dc1.dynomite.local");
872 assert_eq!(dc_from_sni_label("dc-dc1.dynomite.local"), Some("dc1"));
873 assert_eq!(dc_from_sni_label("localhost"), None);
874 assert_eq!(dc_from_sni_label("dc-.dynomite.local"), None);
875 assert_eq!(dc_from_sni_label("dc-dc1.example.com"), None);
876 }
877
878 #[test]
879 fn tls_profile_map_empty_is_empty() {
880 let map = TlsProfileMap::build(None, BTreeMap::new()).unwrap();
881 assert!(map.is_empty());
882 assert!(map.client_config_for_dc("dc1").is_none());
883 assert!(map.server_config_for_dc("dc1").is_none());
884 assert!(!map.requires_client_auth());
885 assert!(map.build_sni_acceptor().unwrap().is_none());
886 }
887
888 #[test]
889 fn tls_profile_map_default_only_falls_back() {
890 let dir = tempfile::tempdir().unwrap();
891 let (cert, key) = write_self_signed(&dir, "default");
892 let map = TlsProfileMap::build(
893 Some(TlsProfileSpec {
894 cert,
895 key,
896 ca: None,
897 }),
898 BTreeMap::new(),
899 )
900 .unwrap();
901 assert!(!map.is_empty());
902 assert!(map.client_config_for_dc("dc1").is_some());
904 assert!(map.server_config_for_dc("dc-without-profile").is_some());
905 assert!(map.default_client_config().is_some());
906 assert!(map.build_sni_acceptor().unwrap().is_some());
907 }
908
909 #[test]
910 fn tls_profile_map_per_dc_overrides_default() {
911 let dir = tempfile::tempdir().unwrap();
912 let (def_cert, def_key) = write_self_signed(&dir, "default");
913 let (dc1_cert, dc1_key) = write_self_signed(&dir, "dc1");
914 let mut per_dc = BTreeMap::new();
915 per_dc.insert(
916 "dc1".into(),
917 TlsProfileSpec {
918 cert: dc1_cert,
919 key: dc1_key,
920 ca: None,
921 },
922 );
923 let map = TlsProfileMap::build(
924 Some(TlsProfileSpec {
925 cert: def_cert,
926 key: def_key,
927 ca: None,
928 }),
929 per_dc,
930 )
931 .unwrap();
932 let dc1 = map.client_config_for_dc("dc1").unwrap();
934 let other = map.client_config_for_dc("other-dc").unwrap();
936 assert!(
937 !Arc::ptr_eq(&dc1, &other),
938 "per-DC entry must differ from the default fallback"
939 );
940 assert_eq!(map.dc_names(), vec!["dc1".to_string()]);
941 }
942
943 #[test]
944 fn tls_profile_map_per_dc_only_no_default() {
945 let dir = tempfile::tempdir().unwrap();
946 let (cert, key) = write_self_signed(&dir, "dc2");
947 let mut per_dc = BTreeMap::new();
948 per_dc.insert(
949 "dc2".into(),
950 TlsProfileSpec {
951 cert,
952 key,
953 ca: None,
954 },
955 );
956 let map = TlsProfileMap::build(None, per_dc).unwrap();
957 assert!(map.client_config_for_dc("dc2").is_some());
958 assert!(map.client_config_for_dc("dc3").is_none());
961 assert!(map.server_config_for_dc("dc3").is_none());
962 }
963
964 #[test]
965 fn tls_profile_map_propagates_load_error() {
966 let dir = tempfile::tempdir().unwrap();
967 let bogus = dir.path().join("missing.pem");
969 let mut per_dc = BTreeMap::new();
970 per_dc.insert(
971 "dc1".into(),
972 TlsProfileSpec {
973 cert: bogus.clone(),
974 key: bogus,
975 ca: None,
976 },
977 );
978 let err = TlsProfileMap::build(None, per_dc).expect_err("missing");
979 assert!(matches!(err, TlsError::Io { .. }), "got {err:?}");
980 }
981}