Files
alknet/crates/alknet-core/src/config/static_config.rs
glm-5.1 ee1b3f3819 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
2026-06-07 14:03:46 +00:00

102 lines
3.5 KiB
Rust

use crate::server::handler::{ProxyConfig, ProxyMode};
use crate::server::serve::ServeTransportMode;
use std::net::SocketAddr;
pub struct StaticConfig {
pub transport_mode: ServeTransportMode,
pub listen_addr: String,
pub tls_cert: Option<String>,
pub tls_key: Option<String>,
pub acme_domain: Option<String>,
pub stealth: bool,
pub host_key: russh::keys::PrivateKey,
pub host_key_algorithm: russh::keys::Algorithm,
pub max_auth_attempts: usize,
pub max_connections_per_ip: usize,
pub proxy_config: Option<ProxyConfig>,
pub iroh_relay: Option<String>,
}
impl std::fmt::Debug for StaticConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StaticConfig")
.field("transport_mode", &self.transport_mode)
.field("listen_addr", &self.listen_addr)
.field("tls_cert", &self.tls_cert.as_ref().map(|_| "<redacted>"))
.field("tls_key", &self.tls_key.as_ref().map(|_| "<redacted>"))
.field("acme_domain", &self.acme_domain)
.field("stealth", &self.stealth)
.field("host_key_algorithm", &self.host_key_algorithm)
.field("max_auth_attempts", &self.max_auth_attempts)
.field("max_connections_per_ip", &self.max_connections_per_ip)
.field("proxy_config", &self.proxy_config)
.field("iroh_relay", &self.iroh_relay)
.finish()
}
}
impl StaticConfig {
pub fn from_serve_options(
opts: crate::server::serve::ServeOptions,
) -> Result<(Self, crate::config::DynamicConfig), crate::error::ConfigError> {
opts.validate()?;
let host_key = crate::auth::keys::load_private_key(opts.key.clone())?;
let host_key_algorithm = host_key.algorithm();
let auth_config = crate::auth::ServerAuthConfig::from_keys_and_ca(
opts.authorized_keys.clone(),
opts.cert_authority.clone(),
)?;
let auth_policy = crate::config::AuthPolicy::from_server_auth_config(auth_config);
let dynamic = crate::config::DynamicConfig::new(auth_policy);
let proxy_config = parse_proxy_config(opts.proxy.as_deref());
let static_config = StaticConfig {
transport_mode: opts.transport_mode,
listen_addr: opts.listen_addr,
tls_cert: opts.tls_cert,
tls_key: opts.tls_key,
acme_domain: opts.acme_domain,
stealth: opts.stealth,
host_key,
host_key_algorithm,
max_auth_attempts: opts.max_auth_attempts,
max_connections_per_ip: opts.max_connections_per_ip,
proxy_config,
iroh_relay: opts.iroh_relay,
};
Ok((static_config, dynamic))
}
}
fn parse_proxy_config(proxy: Option<&str>) -> Option<ProxyConfig> {
proxy.map(|url| {
if url.starts_with("socks5://") {
let addr: SocketAddr = url
.strip_prefix("socks5://")
.unwrap()
.parse()
.expect("invalid socks5 proxy address");
ProxyConfig {
mode: ProxyMode::Socks5(addr),
}
} else if url.starts_with("http://") {
let addr: SocketAddr = url
.strip_prefix("http://")
.unwrap()
.parse()
.expect("invalid http connect proxy address");
ProxyConfig {
mode: ProxyMode::HttpConnect(addr),
}
} else {
panic!("unsupported proxy URL scheme: {url}");
}
})
}