feat(core): implement StaticConfig/DynamicConfig split with ArcSwap hot-reload
Split alknet-core configuration into StaticConfig (immutable after startup) and DynamicConfig (hot-reloadable at runtime via ArcSwap). - Add StaticConfig struct in config/static_config.rs with all fields per ADR-030 - Add DynamicConfig struct with AuthPolicy, ForwardingPolicy, RateLimitConfig - Add ForwardingPolicy with allow_all()/deny_all() defaults (ADR-031) - Add ConfigReloadHandle with reload() method for runtime config updates - Replace Arc<ServerAuthConfig> with Arc<ArcSwap<DynamicConfig>> in ServerHandler - Add config_reload_handle() to Server for obtaining reload handles - Add AuthPolicy with authenticate_publickey/authenticate_certificate methods - All existing tests pass with the new config structure - Default DynamicConfig produces identical behavior to current code
This commit is contained in:
@@ -7,9 +7,9 @@ use rustls::crypto::aws_lc_rs::default_provider;
|
||||
use rustls::ServerConfig;
|
||||
use rustls_acme::caches::DirCache;
|
||||
use rustls_acme::{AcmeConfig, AcmeState, ResolvesServerCertAcme};
|
||||
use tracing::{error, info};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_rustls::TlsAcceptor as TokioTlsAcceptor;
|
||||
use tracing::{error, info};
|
||||
|
||||
use super::{TransportAcceptor, TransportInfo, TransportKind};
|
||||
|
||||
@@ -94,14 +94,10 @@ impl AcmeCertProvider {
|
||||
.contact(self.contact.clone());
|
||||
|
||||
let state = match &self.cache_dir {
|
||||
Some(cache_dir) => {
|
||||
base_config.cache(DirCache::new(cache_dir.clone())).state()
|
||||
}
|
||||
None => {
|
||||
base_config
|
||||
.cache(rustls_acme::caches::NoCache::default())
|
||||
.state()
|
||||
}
|
||||
Some(cache_dir) => base_config.cache(DirCache::new(cache_dir.clone())).state(),
|
||||
None => base_config
|
||||
.cache(rustls_acme::caches::NoCache::default())
|
||||
.state(),
|
||||
};
|
||||
|
||||
let resolver = state.resolver();
|
||||
@@ -132,10 +128,7 @@ pub struct AcmeTlsAcceptor {
|
||||
}
|
||||
|
||||
impl AcmeTlsAcceptor {
|
||||
pub async fn bind_acme(
|
||||
addr: SocketAddr,
|
||||
provider: Arc<AcmeCertProvider>,
|
||||
) -> Result<Self> {
|
||||
pub async fn bind_acme(addr: SocketAddr, provider: Arc<AcmeCertProvider>) -> Result<Self> {
|
||||
let (state, resolver) = provider.build_acme_state();
|
||||
|
||||
let server_config = provider.build_server_config_with_resolver(resolver.clone())?;
|
||||
@@ -193,11 +186,7 @@ impl TransportAcceptor for AcmeTlsAcceptor {
|
||||
let (tcp_stream, remote_addr) = self.listener.accept().await?;
|
||||
let tls_stream = self.tokio_acceptor.accept(tcp_stream).await?;
|
||||
|
||||
let server_name = tls_stream
|
||||
.get_ref()
|
||||
.1
|
||||
.server_name()
|
||||
.map(|s| s.to_string());
|
||||
let server_name = tls_stream.get_ref().1.server_name().map(|s| s.to_string());
|
||||
|
||||
let info = TransportInfo {
|
||||
remote_addr: Some(remote_addr),
|
||||
@@ -277,8 +266,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn acme_cert_provider_build_state_with_cache() {
|
||||
let provider =
|
||||
AcmeCertProvider::domain("example.com").with_cache_dir("/tmp/test_cache");
|
||||
let provider = AcmeCertProvider::domain("example.com").with_cache_dir("/tmp/test_cache");
|
||||
let (_state, resolver) = provider.build_acme_state();
|
||||
assert!(Arc::strong_count(&resolver) >= 2);
|
||||
}
|
||||
@@ -288,7 +276,9 @@ mod tests {
|
||||
let _ = default_provider().install_default();
|
||||
let provider = AcmeCertProvider::domain("example.com");
|
||||
let (_, resolver) = provider.build_acme_state();
|
||||
let config = provider.build_server_config_with_resolver(resolver).unwrap();
|
||||
let config = provider
|
||||
.build_server_config_with_resolver(resolver)
|
||||
.unwrap();
|
||||
assert!(!config.alpn_protocols.is_empty());
|
||||
assert!(config
|
||||
.alpn_protocols
|
||||
@@ -359,4 +349,4 @@ mod tests {
|
||||
let acceptor = result.unwrap();
|
||||
assert_eq!(acceptor.listen_addr().port(), 443);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use iroh::{
|
||||
endpoint::RecvStream,
|
||||
node_info::NodeIdExt,
|
||||
Endpoint, NodeId, RelayMap, RelayMode, RelayUrl,
|
||||
endpoint::RecvStream, node_info::NodeIdExt, Endpoint, NodeId, RelayMap, RelayMode, RelayUrl,
|
||||
};
|
||||
use tokio::io;
|
||||
|
||||
@@ -39,7 +37,9 @@ impl IrohTransport {
|
||||
proxy_url: Option<url::Url>,
|
||||
) -> Result<Self> {
|
||||
let relay_url = relay_url.unwrap_or_else(|| {
|
||||
DEFAULT_RELAY_URL.parse().expect("default relay URL is valid")
|
||||
DEFAULT_RELAY_URL
|
||||
.parse()
|
||||
.expect("default relay URL is valid")
|
||||
});
|
||||
let relay_map = RelayMap::from_url(relay_url);
|
||||
let mut builder = Endpoint::builder()
|
||||
@@ -49,7 +49,11 @@ impl IrohTransport {
|
||||
builder = builder.proxy_url(proxy.clone());
|
||||
}
|
||||
let endpoint = builder.bind().await?;
|
||||
Ok(Self { node_id, endpoint, owned: true })
|
||||
Ok(Self {
|
||||
node_id,
|
||||
endpoint,
|
||||
owned: true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create an iroh transport using an existing shared endpoint.
|
||||
@@ -60,7 +64,11 @@ impl IrohTransport {
|
||||
/// other protocol handlers on the same QUIC endpoint — one connection
|
||||
/// per peer, multiplexed by ALPN.
|
||||
pub fn from_endpoint(node_id: NodeId, endpoint: Endpoint) -> Self {
|
||||
Self { node_id, endpoint, owned: false }
|
||||
Self {
|
||||
node_id,
|
||||
endpoint,
|
||||
owned: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn endpoint_id(&self) -> String {
|
||||
@@ -115,12 +123,11 @@ impl IrohAcceptor {
|
||||
/// Bind a new iroh endpoint with a dedicated `alknet-ssh` ALPN.
|
||||
///
|
||||
/// Use this when alknet is the only iroh service on this node.
|
||||
pub async fn bind(
|
||||
relay_url: Option<RelayUrl>,
|
||||
proxy_url: Option<url::Url>,
|
||||
) -> Result<Self> {
|
||||
pub async fn bind(relay_url: Option<RelayUrl>, proxy_url: Option<url::Url>) -> Result<Self> {
|
||||
let relay_url = relay_url.unwrap_or_else(|| {
|
||||
DEFAULT_RELAY_URL.parse().expect("default relay URL is valid")
|
||||
DEFAULT_RELAY_URL
|
||||
.parse()
|
||||
.expect("default relay URL is valid")
|
||||
});
|
||||
let relay_map = RelayMap::from_url(relay_url);
|
||||
let mut builder = Endpoint::builder()
|
||||
@@ -130,7 +137,10 @@ impl IrohAcceptor {
|
||||
builder = builder.proxy_url(proxy.clone());
|
||||
}
|
||||
let endpoint = builder.bind().await?;
|
||||
Ok(Self { endpoint, owned: true })
|
||||
Ok(Self {
|
||||
endpoint,
|
||||
owned: true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create an iroh acceptor using an existing shared endpoint.
|
||||
@@ -146,7 +156,10 @@ impl IrohAcceptor {
|
||||
/// [`IrohAcceptor::bind`] instead, which handles the accept loop
|
||||
/// internally.
|
||||
pub fn from_endpoint(endpoint: Endpoint) -> Self {
|
||||
Self { endpoint, owned: false }
|
||||
Self {
|
||||
endpoint,
|
||||
owned: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn endpoint_id(&self) -> String {
|
||||
@@ -219,18 +232,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn iroh_transport_describe_format() {
|
||||
let node_id: NodeId = iroh::SecretKey::generate(rand_core::OsRng)
|
||||
.public()
|
||||
.into();
|
||||
let node_id: NodeId = iroh::SecretKey::generate(rand_core::OsRng).public().into();
|
||||
let desc = format!("iroh://{}", node_id.to_z32());
|
||||
assert!(desc.starts_with("iroh://"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn iroh_transport_connect_builds_endpoint() {
|
||||
let node_id: NodeId = iroh::SecretKey::generate(rand_core::OsRng)
|
||||
.public()
|
||||
.into();
|
||||
let node_id: NodeId = iroh::SecretKey::generate(rand_core::OsRng).public().into();
|
||||
let transport = IrohTransport::new(node_id, None, None).await.unwrap();
|
||||
assert!(transport.describe().starts_with("iroh://"));
|
||||
assert!(!transport.endpoint_id().is_empty());
|
||||
@@ -239,9 +248,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn iroh_transport_from_endpoint() {
|
||||
let node_id: NodeId = iroh::SecretKey::generate(rand_core::OsRng)
|
||||
.public()
|
||||
.into();
|
||||
let node_id: NodeId = iroh::SecretKey::generate(rand_core::OsRng).public().into();
|
||||
let acceptor = IrohAcceptor::bind(None, None).await.unwrap();
|
||||
let endpoint = acceptor.endpoint.clone();
|
||||
let transport = IrohTransport::from_endpoint(node_id, endpoint);
|
||||
@@ -318,4 +325,4 @@ mod tests {
|
||||
transport.connect().await.unwrap();
|
||||
let _server_stream = accept_handle.await.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
//! See [ADR-001](docs/architecture/decisions/001-pluggable-transport.md) and
|
||||
//! [ADR-004](docs/architecture/decisions/004-ssh-over-transport.md) for design rationale.
|
||||
|
||||
mod tcp;
|
||||
#[cfg(feature = "iroh")]
|
||||
mod iroh_transport;
|
||||
mod tcp;
|
||||
|
||||
pub use tcp::{TcpAcceptor, TcpTransport};
|
||||
#[cfg(feature = "iroh")]
|
||||
pub use iroh_transport::{IrohAcceptor, IrohTransport, ALPN as IROH_ALPN};
|
||||
pub use tcp::{TcpAcceptor, TcpTransport};
|
||||
|
||||
#[cfg(feature = "tls")]
|
||||
mod tls;
|
||||
@@ -89,12 +89,8 @@ pub struct TransportInfo {
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TransportKind {
|
||||
Tcp,
|
||||
Tls {
|
||||
server_name: Option<String>,
|
||||
},
|
||||
Iroh {
|
||||
endpoint_id: String,
|
||||
},
|
||||
Tls { server_name: Option<String> },
|
||||
Iroh { endpoint_id: String },
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -185,4 +181,4 @@ mod tests {
|
||||
assert_eq!(endpoint_id, "abc123");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,4 +159,4 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_ne!(acceptor.listen_addr().port(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, Server
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
|
||||
use rustls::{ClientConfig, DigitallySignedStruct, RootCertStore, ServerConfig};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio_rustls::{client::TlsStream as ClientTlsStream, TlsAcceptor as TokioTlsAcceptor, TlsConnector};
|
||||
use tokio_rustls::{
|
||||
client::TlsStream as ClientTlsStream, TlsAcceptor as TokioTlsAcceptor, TlsConnector,
|
||||
};
|
||||
|
||||
#[cfg(feature = "acme")]
|
||||
use rustls::crypto::aws_lc_rs::default_provider;
|
||||
@@ -169,7 +171,9 @@ impl TlsAcceptor {
|
||||
.map_err(|e| anyhow!("failed to set protocol versions: {}", e))?
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(acme_resolver);
|
||||
server_config.alpn_protocols.push(ACME_TLS_ALPN_NAME.to_vec());
|
||||
server_config
|
||||
.alpn_protocols
|
||||
.push(ACME_TLS_ALPN_NAME.to_vec());
|
||||
|
||||
let server_config = Arc::new(server_config);
|
||||
let tokio_acceptor = TokioTlsAcceptor::from(server_config.clone());
|
||||
@@ -195,11 +199,7 @@ impl TransportAcceptor for TlsAcceptor {
|
||||
let (tcp_stream, remote_addr) = self.listener.accept().await?;
|
||||
let tls_stream = self.tokio_acceptor.accept(tcp_stream).await?;
|
||||
|
||||
let server_name = tls_stream
|
||||
.get_ref()
|
||||
.1
|
||||
.server_name()
|
||||
.map(|s| s.to_string());
|
||||
let server_name = tls_stream.get_ref().1.server_name().map(|s| s.to_string());
|
||||
|
||||
let info = TransportInfo {
|
||||
remote_addr: Some(remote_addr),
|
||||
@@ -324,10 +324,7 @@ mod tests {
|
||||
|
||||
let (mut server, info) = accept_handle.await.unwrap();
|
||||
assert!(info.remote_addr.is_some());
|
||||
assert!(matches!(
|
||||
info.transport_kind,
|
||||
TransportKind::Tls { .. }
|
||||
));
|
||||
assert!(matches!(info.transport_kind, TransportKind::Tls { .. }));
|
||||
|
||||
client.write_all(b"hello tls").await.unwrap();
|
||||
let mut buf = [0u8; 9];
|
||||
@@ -429,4 +426,4 @@ mod tests {
|
||||
let verifier = NoVerifier;
|
||||
assert!(verifier.supported_verify_schemes().len() > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user