Merge branch 'feat/core/multi-transport-listeners'

This commit is contained in:
2026-06-07 14:25:35 +00:00
6 changed files with 479 additions and 46 deletions

View File

@@ -1,5 +1,5 @@
use crate::server::handler::{ProxyConfig, ProxyMode};
use crate::server::serve::ServeTransportMode;
use crate::server::serve::{ListenerConfig, ServeTransportMode};
use std::net::SocketAddr;
pub struct StaticConfig {
@@ -15,6 +15,7 @@ pub struct StaticConfig {
pub max_connections_per_ip: usize,
pub proxy_config: Option<ProxyConfig>,
pub iroh_relay: Option<String>,
pub listeners: Vec<ListenerConfig>,
}
impl std::fmt::Debug for StaticConfig {
@@ -31,6 +32,7 @@ impl std::fmt::Debug for StaticConfig {
.field("max_connections_per_ip", &self.max_connections_per_ip)
.field("proxy_config", &self.proxy_config)
.field("iroh_relay", &self.iroh_relay)
.field("listeners", &self.listeners)
.finish()
}
}
@@ -55,6 +57,24 @@ impl StaticConfig {
let proxy_config = parse_proxy_config(opts.proxy.as_deref());
let listeners = if let Some(listeners) = opts.listeners {
listeners
} else {
vec![ListenerConfig {
transport_kind: match opts.transport_mode {
ServeTransportMode::Tcp => crate::server::handler::TransportKind::Tcp,
ServeTransportMode::Tls => crate::server::handler::TransportKind::Tls,
ServeTransportMode::Iroh => crate::server::handler::TransportKind::Iroh,
},
listen_addr: opts.listen_addr.clone(),
tls_cert: opts.tls_cert.clone(),
tls_key: opts.tls_key.clone(),
acme_domain: opts.acme_domain.clone(),
stealth: opts.stealth,
iroh_relay: opts.iroh_relay.clone(),
}]
};
let static_config = StaticConfig {
transport_mode: opts.transport_mode,
listen_addr: opts.listen_addr,
@@ -68,6 +88,7 @@ impl StaticConfig {
max_connections_per_ip: opts.max_connections_per_ip,
proxy_config,
iroh_relay: opts.iroh_relay,
listeners,
};
Ok((static_config, dynamic))

View File

@@ -69,5 +69,5 @@ pub use config::{
ForwardingPolicy, ForwardingRule, RateLimitConfig, StaticConfig,
};
pub use error::{AuthError, ChannelError, ConfigError, ForwardError, TransportError};
pub use server::serve::{ServeError, ServeOptions, ServeTransportMode, Server};
pub use server::serve::{ListenerConfig, ServeError, ServeOptions, ServeTransportMode, Server};
pub use transport::{Transport, TransportAcceptor, TransportInfo, TransportKind};

View File

@@ -26,11 +26,13 @@ pub struct ProxyConfig {
pub mode: ProxyMode,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub enum TransportKind {
Tcp,
Tls,
Iroh,
Dns,
WebTransport,
}
impl std::fmt::Display for TransportKind {
@@ -39,6 +41,8 @@ impl std::fmt::Display for TransportKind {
TransportKind::Tcp => write!(f, "tcp"),
TransportKind::Tls => write!(f, "tls"),
TransportKind::Iroh => write!(f, "iroh"),
TransportKind::Dns => write!(f, "dns"),
TransportKind::WebTransport => write!(f, "webtransport"),
}
}
}
@@ -752,6 +756,8 @@ mod tests {
assert_eq!(TransportKind::Tcp.to_string(), "tcp");
assert_eq!(TransportKind::Tls.to_string(), "tls");
assert_eq!(TransportKind::Iroh.to_string(), "iroh");
assert_eq!(TransportKind::Dns.to_string(), "dns");
assert_eq!(TransportKind::WebTransport.to_string(), "webtransport");
}
#[tokio::test]

View File

@@ -21,7 +21,7 @@ pub use control_channel::{
};
pub use handler::{ProxyConfig, ProxyMode, ServerHandler, TransportKind};
pub use rate_limit::{AuthAttemptLimiter, ConnectionRateLimiter};
pub use serve::{ServeError, ServeOptions, ServeTransportMode, Server};
pub use serve::{ListenerConfig, ServeError, ServeOptions, ServeTransportMode, Server};
pub use stealth::{
detect_protocol, send_fake_nginx_404, validate_stealth_config, ProtocolDetection,
};

View File

@@ -3,6 +3,7 @@
//! `Server` binds to a transport acceptor and runs an accept loop, handling
//! authentication, stealth mode protocol detection, and graceful shutdown.
//! `ServeOptions` provides a builder-pattern API for programmatic configuration.
//! Supports multiple listeners via `ListenerConfig` for multi-transport operation.
use std::net::SocketAddr;
use std::sync::Arc;
@@ -24,7 +25,6 @@ use crate::server::stealth::{self, ProtocolDetection};
const DEFAULT_LISTEN_ADDR: &str = "0.0.0.0:22";
const DRAIN_TIMEOUT: Duration = Duration::from_secs(2);
/// Transport mode for the server listener.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ServeTransportMode {
Tcp,
@@ -42,22 +42,153 @@ impl std::fmt::Display for ServeTransportMode {
}
}
/// Programmatic configuration for an alknet server.
///
/// Construct with `ServeOptions::new(key_source)` and chain builder methods.
/// Call `validate()` before passing to `Server::new()`.
///
/// ```
/// use alknet_core::server::{ServeOptions, ServeTransportMode};
/// use alknet_core::auth::keys::KeySource;
///
/// let opts = ServeOptions::new(KeySource::File("/path/to/host_key".into()))
/// .transport_mode(ServeTransportMode::Tcp)
/// .listen_addr("0.0.0.0:22")
/// .max_connections_per_ip(5)
/// .max_auth_attempts(3);
/// opts.validate().unwrap();
/// ```
#[derive(Debug, Clone, PartialEq)]
pub struct ListenerConfig {
pub transport_kind: TransportKind,
pub listen_addr: String,
pub tls_cert: Option<String>,
pub tls_key: Option<String>,
pub acme_domain: Option<String>,
pub stealth: bool,
pub iroh_relay: Option<String>,
}
impl ListenerConfig {
pub fn tcp(addr: impl Into<String>) -> Self {
Self {
transport_kind: TransportKind::Tcp,
listen_addr: addr.into(),
tls_cert: None,
tls_key: None,
acme_domain: None,
stealth: false,
iroh_relay: None,
}
}
pub fn tls(addr: impl Into<String>) -> Self {
Self {
transport_kind: TransportKind::Tls,
listen_addr: addr.into(),
tls_cert: None,
tls_key: None,
acme_domain: None,
stealth: false,
iroh_relay: None,
}
}
pub fn iroh(addr: impl Into<String>) -> Self {
Self {
transport_kind: TransportKind::Iroh,
listen_addr: addr.into(),
tls_cert: None,
tls_key: None,
acme_domain: None,
stealth: false,
iroh_relay: None,
}
}
pub fn dns(domain: impl Into<String>) -> Self {
Self {
transport_kind: TransportKind::Dns,
listen_addr: domain.into(),
tls_cert: None,
tls_key: None,
acme_domain: None,
stealth: false,
iroh_relay: None,
}
}
pub fn webtransport(host: impl Into<String>) -> Self {
Self {
transport_kind: TransportKind::WebTransport,
listen_addr: host.into(),
tls_cert: None,
tls_key: None,
acme_domain: None,
stealth: false,
iroh_relay: None,
}
}
pub fn tls_cert(mut self, path: impl Into<String>) -> Self {
self.tls_cert = Some(path.into());
self
}
pub fn tls_key(mut self, path: impl Into<String>) -> Self {
self.tls_key = Some(path.into());
self
}
pub fn acme_domain(mut self, domain: impl Into<String>) -> Self {
self.acme_domain = Some(domain.into());
self
}
pub fn stealth(mut self, enabled: bool) -> Self {
self.stealth = enabled;
self
}
pub fn iroh_relay(mut self, url: impl Into<String>) -> Self {
self.iroh_relay = Some(url.into());
self
}
pub fn validate(&self) -> Result<(), ConfigError> {
if self.stealth && self.transport_kind != TransportKind::Tls {
return Err(ConfigError::InvalidFlag {
name: "stealth mode requires TLS transport".to_string(),
});
}
match self.transport_kind {
TransportKind::Tls => {
if self.tls_cert.is_none() && self.acme_domain.is_none() {
return Err(ConfigError::InvalidFlag {
name: "TLS transport requires tls_cert/tls_key or acme_domain".to_string(),
});
}
if self.tls_cert.is_some() && self.tls_key.is_none() {
return Err(ConfigError::InvalidFlag {
name: "tls_cert requires tls_key".to_string(),
});
}
if self.tls_key.is_some() && self.tls_cert.is_none() {
return Err(ConfigError::InvalidFlag {
name: "tls_key requires tls_cert".to_string(),
});
}
}
TransportKind::Tcp
| TransportKind::Iroh
| TransportKind::Dns
| TransportKind::WebTransport => {
if self.tls_cert.is_some() || self.tls_key.is_some() || self.acme_domain.is_some() {
return Err(ConfigError::IncompatibleOptions);
}
}
}
Ok(())
}
}
impl std::fmt::Display for ListenerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.transport_kind {
TransportKind::Iroh => write!(f, "{} (iroh)", self.listen_addr),
TransportKind::Dns => write!(f, "{} (dns)", self.listen_addr),
TransportKind::WebTransport => write!(f, "{} (webtransport)", self.listen_addr),
_ => write!(f, "{} ({})", self.listen_addr, self.transport_kind),
}
}
}
pub struct ServeOptions {
pub key: KeySource,
pub authorized_keys: Option<KeySource>,
@@ -72,6 +203,7 @@ pub struct ServeOptions {
pub iroh_relay: Option<String>,
pub max_connections_per_ip: usize,
pub max_auth_attempts: usize,
pub listeners: Option<Vec<ListenerConfig>>,
}
impl ServeOptions {
@@ -90,6 +222,7 @@ impl ServeOptions {
iroh_relay: None,
max_connections_per_ip: 0,
max_auth_attempts: 10,
listeners: None,
}
}
@@ -153,7 +286,24 @@ impl ServeOptions {
self
}
pub fn listeners(mut self, listeners: Vec<ListenerConfig>) -> Self {
self.listeners = Some(listeners);
self
}
pub fn validate(&self) -> Result<(), ConfigError> {
if let Some(ref listeners) = self.listeners {
if listeners.is_empty() {
return Err(ConfigError::InvalidFlag {
name: "listeners must not be empty".to_string(),
});
}
for listener in listeners {
listener.validate()?;
}
return Ok(());
}
if self.stealth && self.transport_mode != ServeTransportMode::Tls {
return Err(ConfigError::InvalidFlag {
name: "stealth mode requires TLS transport (--transport tls)".to_string(),
@@ -201,11 +351,11 @@ impl std::fmt::Debug for ServeOptions {
.field("stealth", &self.stealth)
.field("max_connections_per_ip", &self.max_connections_per_ip)
.field("max_auth_attempts", &self.max_auth_attempts)
.field("listeners", &self.listeners)
.finish()
}
}
/// Errors that can occur during server setup and operation.
#[derive(Debug, thiserror::Error)]
pub enum ServeError {
#[error("config error: {0}")]
@@ -223,19 +373,12 @@ struct ActiveSession {
join: tokio::task::JoinHandle<()>,
}
/// The alknet SSH server.
///
/// Accepts connections over any `TransportAcceptor`, authenticates via Ed25519 keys
/// or certificate authority, and proxies `direct-tcpip` channels to their targets.
/// Supports stealth mode (TLS only), outbound proxy routing, and connection rate limiting.
pub struct Server {
config: Arc<server::Config>,
dynamic: Arc<ArcSwap<DynamicConfig>>,
connection_limiter: Arc<ConnectionRateLimiter>,
outbound_proxy: Option<ProxyConfig>,
stealth: bool,
transport_mode: ServeTransportMode,
listen_addr: String,
listeners: Vec<ListenerConfig>,
max_auth_attempts: usize,
shutdown_tx: tokio::sync::watch::Sender<bool>,
shutdown_rx: tokio::sync::watch::Receiver<bool>,
@@ -277,14 +420,31 @@ impl Server {
let dynamic = Arc::new(ArcSwap::new(Arc::new(dynamic_config)));
let listeners = if let Some(listeners) = opts.listeners {
listeners
} else {
let transport_kind = match opts.transport_mode {
ServeTransportMode::Tcp => TransportKind::Tcp,
ServeTransportMode::Tls => TransportKind::Tls,
ServeTransportMode::Iroh => TransportKind::Iroh,
};
vec![ListenerConfig {
transport_kind,
listen_addr: opts.listen_addr.clone(),
tls_cert: opts.tls_cert.clone(),
tls_key: opts.tls_key.clone(),
acme_domain: opts.acme_domain.clone(),
stealth: opts.stealth,
iroh_relay: opts.iroh_relay.clone(),
}]
};
Ok(Self {
config,
dynamic,
connection_limiter,
outbound_proxy,
stealth: opts.stealth,
transport_mode: opts.transport_mode,
listen_addr: opts.listen_addr,
listeners,
max_auth_attempts,
shutdown_tx,
shutdown_rx,
@@ -344,13 +504,16 @@ impl Server {
where
A: crate::transport::TransportAcceptor,
{
let transport_kind = match self.transport_mode {
ServeTransportMode::Tcp => TransportKind::Tcp,
ServeTransportMode::Tls => TransportKind::Tls,
ServeTransportMode::Iroh => TransportKind::Iroh,
};
let listener = self
.listeners
.first()
.expect("at least one listener required");
if self.transport_mode == ServeTransportMode::Iroh {
let transport_kind = listener.transport_kind.clone();
let stealth = listener.stealth;
let listen_addr = listener.listen_addr.clone();
if matches!(transport_kind, TransportKind::Iroh) {
if let Some(id) = endpoint_info {
info!("alknet server running: transport=iroh endpoint_id={}", id);
} else {
@@ -359,7 +522,7 @@ impl Server {
} else {
info!(
"alknet server running: transport={} listen={}",
self.transport_mode, self.listen_addr
transport_kind, listen_addr
);
}
@@ -410,7 +573,7 @@ impl Server {
};
let remote_addr = info.remote_addr;
let handler_transport_kind = transport_kind;
let handler_transport_kind = transport_kind.clone();
let handler = ServerHandler::new(
Arc::clone(&server.dynamic),
@@ -427,8 +590,7 @@ impl Server {
let config = Arc::clone(&server.config);
let sessions = Arc::clone(&server.sessions);
let stealth = server.stealth;
let transport_is_tls = server.transport_mode == ServeTransportMode::Tls;
let transport_is_tls = matches!(transport_kind, TransportKind::Tls);
tokio::spawn(async move {
let result =
@@ -448,6 +610,10 @@ impl Server {
Ok(())
}
pub fn listeners(&self) -> &[ListenerConfig] {
&self.listeners
}
}
async fn handle_connection<S>(
@@ -547,6 +713,7 @@ mod tests {
assert!(opts.iroh_relay.is_none());
assert_eq!(opts.max_connections_per_ip, 0);
assert_eq!(opts.max_auth_attempts, 10);
assert!(opts.listeners.is_none());
}
#[test]
@@ -739,10 +906,235 @@ mod tests {
}
#[test]
fn server_holds_listen_addr() {
let opts = ServeOptions::new(make_key_source()).listen_addr("0.0.0.0:443");
fn listener_config_tcp_constructor() {
let lc = ListenerConfig::tcp("0.0.0.0:22");
assert_eq!(lc.transport_kind, TransportKind::Tcp);
assert_eq!(lc.listen_addr, "0.0.0.0:22");
assert!(!lc.stealth);
assert!(lc.tls_cert.is_none());
}
#[test]
fn listener_config_tls_constructor() {
let lc = ListenerConfig::tls("0.0.0.0:443")
.tls_cert("/cert.pem")
.tls_key("/key.pem")
.stealth(true);
assert_eq!(lc.transport_kind, TransportKind::Tls);
assert_eq!(lc.listen_addr, "0.0.0.0:443");
assert!(lc.stealth);
assert_eq!(lc.tls_cert.as_deref(), Some("/cert.pem"));
assert_eq!(lc.tls_key.as_deref(), Some("/key.pem"));
}
#[test]
fn listener_config_iroh_constructor() {
let lc = ListenerConfig::iroh("0.0.0.0:0").iroh_relay("https://relay.example.com");
assert_eq!(lc.transport_kind, TransportKind::Iroh);
assert_eq!(lc.iroh_relay.as_deref(), Some("https://relay.example.com"));
}
#[test]
fn listener_config_dns_constructor() {
let lc = ListenerConfig::dns("example.com");
assert_eq!(lc.transport_kind, TransportKind::Dns);
assert_eq!(lc.listen_addr, "example.com");
}
#[test]
fn listener_config_webtransport_constructor() {
let lc = ListenerConfig::webtransport("example.com");
assert_eq!(lc.transport_kind, TransportKind::WebTransport);
assert_eq!(lc.listen_addr, "example.com");
}
#[test]
fn listener_config_validate_tls_requires_certs() {
let lc = ListenerConfig::tls("0.0.0.0:443");
assert!(lc.validate().is_err());
}
#[test]
fn listener_config_validate_tls_with_certs_ok() {
let lc = ListenerConfig::tls("0.0.0.0:443")
.tls_cert("/cert.pem")
.tls_key("/key.pem");
assert!(lc.validate().is_ok());
}
#[test]
fn listener_config_validate_tls_with_acme_ok() {
let lc = ListenerConfig::tls("0.0.0.0:443").acme_domain("example.com");
assert!(lc.validate().is_ok());
}
#[test]
fn listener_config_validate_stealth_without_tls_rejected() {
let lc = ListenerConfig::tcp("0.0.0.0:22").stealth(true);
assert!(lc.validate().is_err());
}
#[test]
fn listener_config_validate_tcp_cannot_have_tls_certs() {
let lc = ListenerConfig::tcp("0.0.0.0:22").tls_cert("/cert.pem");
assert!(lc.validate().is_err());
}
#[test]
fn listener_config_display() {
let tcp = ListenerConfig::tcp("0.0.0.0:22");
assert_eq!(format!("{}", tcp), "0.0.0.0:22 (tcp)");
let tls = ListenerConfig::tls("0.0.0.0:443");
assert_eq!(format!("{}", tls), "0.0.0.0:443 (tls)");
let iroh = ListenerConfig::iroh("0.0.0.0:0");
assert_eq!(format!("{}", iroh), "0.0.0.0:0 (iroh)");
let dns = ListenerConfig::dns("example.com");
assert_eq!(format!("{}", dns), "example.com (dns)");
let wt = ListenerConfig::webtransport("example.com");
assert_eq!(format!("{}", wt), "example.com (webtransport)");
}
#[test]
fn listener_config_equality() {
let lc1 = ListenerConfig::tcp("0.0.0.0:22");
let lc2 = ListenerConfig::tcp("0.0.0.0:22");
assert_eq!(lc1, lc2);
let lc3 = ListenerConfig::tls("0.0.0.0:443");
assert_ne!(lc1, lc3);
}
#[test]
fn serve_options_with_listeners() {
let listeners = vec![
ListenerConfig::tcp("0.0.0.0:22"),
ListenerConfig::tls("0.0.0.0:443")
.tls_cert("/cert.pem")
.tls_key("/key.pem"),
];
let opts = ServeOptions::new(make_key_source())
.authorized_keys(make_authorized_keys_source())
.listeners(listeners);
assert!(opts.listeners.is_some());
assert_eq!(opts.listeners.as_ref().unwrap().len(), 2);
}
#[test]
fn serve_options_validate_listeners_ok() {
let listeners = vec![
ListenerConfig::tcp("0.0.0.0:22"),
ListenerConfig::tls("0.0.0.0:443")
.tls_cert("/cert.pem")
.tls_key("/key.pem"),
];
let opts = ServeOptions::new(make_key_source())
.authorized_keys(make_authorized_keys_source())
.listeners(listeners);
assert!(opts.validate().is_ok());
}
#[test]
fn serve_options_validate_listeners_bypasses_single_validation() {
let listeners = vec![ListenerConfig::tcp("0.0.0.0:22")];
let opts = ServeOptions::new(make_key_source())
.stealth(true)
.listeners(listeners);
assert!(opts.validate().is_ok());
}
#[test]
fn serve_options_validate_listeners_per_listener_stealth_requires_tls() {
let listeners = vec![ListenerConfig::tcp("0.0.0.0:22").stealth(true)];
let opts = ServeOptions::new(make_key_source())
.authorized_keys(make_authorized_keys_source())
.listeners(listeners);
assert!(opts.validate().is_err());
}
#[test]
fn serve_options_validate_empty_listeners_rejected() {
let opts = ServeOptions::new(make_key_source())
.authorized_keys(make_authorized_keys_source())
.listeners(vec![]);
assert!(opts.validate().is_err());
}
#[test]
fn server_new_with_listeners() {
let listeners = vec![ListenerConfig::tcp("0.0.0.0:22")];
let opts = ServeOptions::new(make_key_source())
.authorized_keys(make_authorized_keys_source())
.listeners(listeners);
let server = Server::new(opts).unwrap();
assert_eq!(server.listen_addr, "0.0.0.0:443");
assert_eq!(server.listeners.len(), 1);
assert_eq!(server.listeners[0].transport_kind, TransportKind::Tcp);
}
#[test]
fn server_new_single_transport_creates_listener() {
let opts =
ServeOptions::new(make_key_source()).authorized_keys(make_authorized_keys_source());
let server = Server::new(opts).unwrap();
assert_eq!(server.listeners.len(), 1);
assert_eq!(server.listeners[0].transport_kind, TransportKind::Tcp);
assert_eq!(server.listeners[0].listen_addr, "0.0.0.0:22");
}
#[test]
fn server_new_tls_transport_creates_tls_listener() {
let opts = ServeOptions::new(make_key_source())
.authorized_keys(make_authorized_keys_source())
.transport_mode(ServeTransportMode::Tls)
.tls_cert("/cert.pem")
.tls_key("/key.pem")
.listen_addr("0.0.0.0:443")
.stealth(true);
let server = Server::new(opts).unwrap();
assert_eq!(server.listeners.len(), 1);
assert_eq!(server.listeners[0].transport_kind, TransportKind::Tls);
assert!(server.listeners[0].stealth);
assert_eq!(server.listeners[0].tls_cert.as_deref(), Some("/cert.pem"));
}
#[test]
fn server_listeners_accessor() {
let listeners = vec![
ListenerConfig::tcp("0.0.0.0:22"),
ListenerConfig::tls("0.0.0.0:443")
.tls_cert("/cert.pem")
.tls_key("/key.pem"),
];
let opts = ServeOptions::new(make_key_source())
.authorized_keys(make_authorized_keys_source())
.listeners(listeners);
let server = Server::new(opts).unwrap();
assert_eq!(server.listeners().len(), 2);
assert_eq!(server.listeners()[0].transport_kind, TransportKind::Tcp);
assert_eq!(server.listeners()[1].transport_kind, TransportKind::Tls);
}
#[test]
fn server_new_multi_listener_tcp_and_tls() {
let listeners = vec![
ListenerConfig::tcp("0.0.0.0:22"),
ListenerConfig::tls("0.0.0.0:443")
.tls_cert("/cert.pem")
.tls_key("/key.pem"),
];
let opts = ServeOptions::new(make_key_source())
.authorized_keys(make_authorized_keys_source())
.listeners(listeners);
let server = Server::new(opts).unwrap();
assert_eq!(server.listeners.len(), 2);
let dynamic = server.config_reload_handle();
let config = dynamic.dynamic();
assert!(config.auth.authorized_keys.len() > 0);
}
#[tokio::test]

View File

@@ -91,6 +91,8 @@ pub enum TransportKind {
Tcp,
Tls { server_name: Option<String> },
Iroh { endpoint_id: String },
Dns { domain: String },
WebTransport { host: String },
}
#[cfg(test)]
@@ -169,6 +171,12 @@ mod tests {
let iroh = TransportKind::Iroh {
endpoint_id: "abc123".to_string(),
};
let dns = TransportKind::Dns {
domain: "example.com".to_string(),
};
let wt = TransportKind::WebTransport {
host: "example.com".to_string(),
};
if let TransportKind::Tcp = tcp {}
if let TransportKind::Tls {
@@ -180,5 +188,11 @@ mod tests {
if let TransportKind::Iroh { endpoint_id } = iroh {
assert_eq!(endpoint_id, "abc123");
}
if let TransportKind::Dns { domain } = dns {
assert_eq!(domain, "example.com");
}
if let TransportKind::WebTransport { host } = wt {
assert_eq!(host, "example.com");
}
}
}