From aa7e843e2f592e9783ea21b9c70019827dec14c8 Mon Sep 17 00:00:00 2001 From: "glm-5.1" Date: Tue, 9 Jun 2026 10:50:19 +0000 Subject: [PATCH] feat(core): add HttpListenerConfig/DnsListenerConfig builders, ListenerConfig validation with is_valid_pair, and Server::run Http/Dns stubs --- crates/alknet-core/src/server/serve.rs | 242 +++++++++++++++++++++++-- 1 file changed, 226 insertions(+), 16 deletions(-) diff --git a/crates/alknet-core/src/server/serve.rs b/crates/alknet-core/src/server/serve.rs index ae73cee..5f96a98 100644 --- a/crates/alknet-core/src/server/serve.rs +++ b/crates/alknet-core/src/server/serve.rs @@ -18,6 +18,7 @@ use tracing::{error, info, warn}; use crate::auth::keys::KeySource; use crate::config::{ConfigReloadHandle, DynamicConfig}; use crate::error::ConfigError; +use crate::interface::pairs::is_valid_pair; use crate::interface::StreamInterfaceKind; use crate::server::handler::{ProxyConfig, ServerHandler}; use crate::server::rate_limit::ConnectionRateLimiter; @@ -58,6 +59,15 @@ pub struct StreamListenerConfig { impl StreamListenerConfig { pub fn validate(&self) -> Result<(), ConfigError> { + if !is_valid_pair(&self.transport_kind, self.interface) { + return Err(ConfigError::InvalidFlag { + name: format!( + "invalid transport/interface pair: {}/{}", + self.transport_kind, self.interface + ), + }); + } + if self.stealth && !matches!(self.transport_kind, TransportKind::Tls { .. }) { return Err(ConfigError::InvalidFlag { name: "stealth mode requires TLS transport".to_string(), @@ -120,6 +130,26 @@ pub struct HttpListenerConfig { pub stealth: bool, } +impl HttpListenerConfig { + pub fn new(bind_addr: SocketAddr) -> Self { + Self { + bind_addr, + tls: false, + stealth: false, + } + } + + pub fn tls(mut self, enabled: bool) -> Self { + self.tls = enabled; + self + } + + pub fn stealth(mut self, enabled: bool) -> Self { + self.stealth = enabled; + self + } +} + impl std::fmt::Display for HttpListenerConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{} (http)", self.bind_addr) @@ -132,6 +162,20 @@ pub struct DnsListenerConfig { pub tls: bool, } +impl DnsListenerConfig { + pub fn new(bind_addr: SocketAddr) -> Self { + Self { + bind_addr, + tls: false, + } + } + + pub fn tls(mut self, enabled: bool) -> Self { + self.tls = enabled; + self + } +} + impl std::fmt::Display for DnsListenerConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{} (dns)", self.bind_addr) @@ -268,7 +312,15 @@ impl ListenerConfig { pub fn validate(&self) -> Result<(), ConfigError> { match self { ListenerConfig::Stream { config } => config.validate(), - ListenerConfig::Http { .. } | ListenerConfig::Dns { .. } => Ok(()), + ListenerConfig::Http { config } => { + if config.stealth && !config.tls { + return Err(ConfigError::InvalidFlag { + name: "stealth mode requires TLS on HTTP listener".to_string(), + }); + } + Ok(()) + } + ListenerConfig::Dns { .. } => Ok(()), } } } @@ -568,23 +620,42 @@ impl Server { let listener = self .listeners .first() - .expect("at least one listener required"); + .expect("at least one listener required") + .clone(); - let (transport_kind, stealth, listen_addr) = match listener { - ListenerConfig::Stream { config } => ( - config.transport_kind.clone(), - config.stealth, - config.listen_addr.clone(), - ), - ListenerConfig::Http { config } => ( - TransportKind::Tcp, - config.stealth, - config.bind_addr.to_string(), - ), - ListenerConfig::Dns { config } => { - (TransportKind::Tcp, false, config.bind_addr.to_string()) + match listener { + ListenerConfig::Stream { config } => { + self.run_stream(acceptor, endpoint_info, config).await } - }; + ListenerConfig::Http { config } => { + warn!( + "HTTP listener on {} not yet implemented (tls={}, stealth={})", + config.bind_addr, config.tls, config.stealth + ); + Ok(()) + } + ListenerConfig::Dns { config } => { + warn!( + "DNS listener on {} not yet implemented (tls={})", + config.bind_addr, config.tls + ); + Ok(()) + } + } + } + + async fn run_stream( + self, + acceptor: A, + endpoint_info: Option<&str>, + stream_config: StreamListenerConfig, + ) -> Result<(), ServeError> + where + A: crate::transport::TransportAcceptor, + { + let transport_kind = stream_config.transport_kind.clone(); + let stealth = stream_config.stealth; + let listen_addr = stream_config.listen_addr.clone(); if matches!(transport_kind, TransportKind::Iroh { .. }) { if let Some(id) = endpoint_info { @@ -1298,4 +1369,143 @@ mod tests { assert_eq!(deserialized.bind_addr, config.bind_addr); assert_eq!(deserialized.tls, config.tls); } + + #[test] + fn http_listener_config_builder_new() { + let config = HttpListenerConfig::new("127.0.0.1:8080".parse().unwrap()); + assert_eq!( + config.bind_addr, + "127.0.0.1:8080".parse::().unwrap() + ); + assert!(!config.tls); + assert!(!config.stealth); + } + + #[test] + fn http_listener_config_builder_tls() { + let config = HttpListenerConfig::new("127.0.0.1:8443".parse().unwrap()).tls(true); + assert!(config.tls); + } + + #[test] + fn http_listener_config_builder_stealth() { + let config = HttpListenerConfig::new("127.0.0.1:443".parse().unwrap()) + .tls(true) + .stealth(true); + assert!(config.tls); + assert!(config.stealth); + } + + #[test] + fn dns_listener_config_builder_new() { + let config = DnsListenerConfig::new("0.0.0.0:53".parse().unwrap()); + assert_eq!( + config.bind_addr, + "0.0.0.0:53".parse::().unwrap() + ); + assert!(!config.tls); + } + + #[test] + fn dns_listener_config_builder_tls() { + let config = DnsListenerConfig::new("0.0.0.0:853".parse().unwrap()).tls(true); + assert!(config.tls); + } + + #[test] + fn listener_config_http_with_builder() { + let lc = ListenerConfig::Http { + config: HttpListenerConfig::new("127.0.0.1:8080".parse().unwrap()) + .tls(true) + .stealth(true), + }; + match &lc { + ListenerConfig::Http { config } => { + assert_eq!( + config.bind_addr, + "127.0.0.1:8080".parse::().unwrap() + ); + assert!(config.tls); + assert!(config.stealth); + } + _ => panic!("expected Http variant"), + } + } + + #[test] + fn listener_config_dns_with_builder() { + let lc = ListenerConfig::Dns { + config: DnsListenerConfig::new("0.0.0.0:53".parse().unwrap()).tls(true), + }; + match &lc { + ListenerConfig::Dns { config } => { + assert_eq!( + config.bind_addr, + "0.0.0.0:53".parse::().unwrap() + ); + assert!(config.tls); + } + _ => panic!("expected Dns variant"), + } + } + + #[test] + fn listener_config_validate_stream_tls_cert_key_required() { + let lc = ListenerConfig::tls("0.0.0.0:443"); + assert!(lc.validate().is_err()); + } + + #[test] + fn listener_config_validate_http_stealth_without_tls_rejected() { + let lc = ListenerConfig::Http { + config: HttpListenerConfig { + bind_addr: "0.0.0.0:8080".parse().unwrap(), + tls: false, + stealth: true, + }, + }; + assert!(lc.validate().is_err()); + } + + #[test] + fn listener_config_validate_http_stealth_with_tls_ok() { + let lc = ListenerConfig::Http { + config: HttpListenerConfig { + bind_addr: "0.0.0.0:443".parse().unwrap(), + tls: true, + stealth: true, + }, + }; + assert!(lc.validate().is_ok()); + } + + #[test] + fn listener_config_validate_dns_minimal_ok() { + let lc = ListenerConfig::Dns { + config: DnsListenerConfig { + bind_addr: "0.0.0.0:53".parse().unwrap(), + tls: false, + }, + }; + assert!(lc.validate().is_ok()); + } + + #[test] + fn listener_config_validate_stream_invalid_pair() { + let lc = ListenerConfig::Stream { + config: StreamListenerConfig { + transport_kind: TransportKind::Iroh { + endpoint_id: String::new(), + }, + interface: StreamInterfaceKind::RawFraming, + listen_addr: "0.0.0.0:0".to_string(), + tls_cert: None, + tls_key: None, + acme_domain: None, + stealth: false, + iroh_relay: None, + }, + }; + assert!(lc.validate().is_err()); + } }