Extract SshInterface from ServerHandler, add RawFramingInterface stub

- SshInterface implements Interface trait with accept() method
- SshSession implements InterfaceSession trait (stub for call protocol events)
- RawFramingInterface is type-only stub (Phase 4+ for DNS, WebTransport)
- TransportKind consolidated into transport module with Display, PartialEq, Eq
- ListenerConfig gains interface_kind field for (Transport, Interface) pairs
- SshInterface wraps existing russh handler logic (SshHandler)
- Auth delegation through IdentityProvider (not embedded in SshInterface)
- Channel routing through session to Layer 3 (forwarding policy)
- Server accept loop uses (Transport, Interface) pairs

Per ADR-026: SSH is Layer 2, not Layer 1. This is the highest-risk Phase 1
task, implementing the Interface trait to separate transport from interface.
This commit is contained in:
2026-06-07 16:24:31 +00:00
parent bd38c94cae
commit 22724228f8
10 changed files with 982 additions and 75 deletions

View File

@@ -14,6 +14,8 @@ use crate::config::DynamicConfig;
use crate::server::control_channel::{ControlChannelHandler, ControlChannelRouter, ALKNET_PREFIX};
use crate::server::rate_limit::{AuthAttemptLimiter, ConnectionRateLimiter};
pub use crate::transport::TransportKind;
#[derive(Debug, Clone)]
pub enum ProxyMode {
Direct,
@@ -26,27 +28,6 @@ pub struct ProxyConfig {
pub mode: ProxyMode,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TransportKind {
Tcp,
Tls,
Iroh,
Dns,
WebTransport,
}
impl std::fmt::Display for TransportKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
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"),
}
}
}
pub struct ServerHandler {
dynamic: Arc<ArcSwap<DynamicConfig>>,
identity_provider: Arc<dyn IdentityProvider>,
@@ -252,7 +233,7 @@ impl Handler for ServerHandler {
host_to_connect,
port_to_connect as u16,
&identity,
self.transport,
self.transport.clone(),
);
if !allowed {
@@ -784,10 +765,28 @@ mod tests {
#[test]
fn transport_kind_display() {
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");
assert_eq!(TransportKind::Tls { server_name: None }.to_string(), "tls");
assert_eq!(
TransportKind::Iroh {
endpoint_id: String::new()
}
.to_string(),
"iroh"
);
assert_eq!(
TransportKind::Dns {
domain: String::new()
}
.to_string(),
"dns"
);
assert_eq!(
TransportKind::WebTransport {
host: String::new()
}
.to_string(),
"webtransport"
);
}
#[tokio::test]
@@ -797,7 +796,7 @@ mod tests {
auth_config,
None,
Some("203.0.113.50:12345".parse().unwrap()),
TransportKind::Tls,
TransportKind::Tls { server_name: None },
Arc::new(ConnectionRateLimiter::new(0)),
10,
);

View File

@@ -19,9 +19,11 @@ pub use control_channel::{
is_reserved_destination, ControlChannelHandler, ControlChannelRouter, DuplexStream,
ALKNET_CONTROL_DESTINATION, ALKNET_PREFIX,
};
pub use handler::{ProxyConfig, ProxyMode, ServerHandler, TransportKind};
pub use handler::{ProxyConfig, ProxyMode, ServerHandler};
pub use rate_limit::{AuthAttemptLimiter, ConnectionRateLimiter};
pub use serve::{ListenerConfig, ServeError, ServeOptions, ServeTransportMode, Server};
pub use crate::transport::TransportKind;
pub use stealth::{
detect_protocol, send_fake_nginx_404, validate_stealth_config, ProtocolDetection,
};

View File

@@ -16,9 +16,11 @@ use tracing::{error, info, warn};
use crate::auth::keys::KeySource;
use crate::config::{ConfigReloadHandle, DynamicConfig};
use crate::error::ConfigError;
use crate::server::handler::{ProxyConfig, ServerHandler, TransportKind};
use crate::interface::InterfaceKind;
use crate::server::handler::{ProxyConfig, ServerHandler};
use crate::server::rate_limit::ConnectionRateLimiter;
use crate::server::stealth::{self, ProtocolDetection};
use crate::transport::TransportKind;
const DEFAULT_LISTEN_ADDR: &str = "0.0.0.0:22";
const DRAIN_TIMEOUT: Duration = Duration::from_secs(2);
@@ -43,6 +45,7 @@ impl std::fmt::Display for ServeTransportMode {
#[derive(Debug, Clone, PartialEq)]
pub struct ListenerConfig {
pub transport_kind: TransportKind,
pub interface_kind: InterfaceKind,
pub listen_addr: String,
pub tls_cert: Option<String>,
pub tls_key: Option<String>,
@@ -55,6 +58,7 @@ impl ListenerConfig {
pub fn tcp(addr: impl Into<String>) -> Self {
Self {
transport_kind: TransportKind::Tcp,
interface_kind: InterfaceKind::Ssh,
listen_addr: addr.into(),
tls_cert: None,
tls_key: None,
@@ -66,7 +70,8 @@ impl ListenerConfig {
pub fn tls(addr: impl Into<String>) -> Self {
Self {
transport_kind: TransportKind::Tls,
transport_kind: TransportKind::Tls { server_name: None },
interface_kind: InterfaceKind::Ssh,
listen_addr: addr.into(),
tls_cert: None,
tls_key: None,
@@ -78,7 +83,10 @@ impl ListenerConfig {
pub fn iroh(addr: impl Into<String>) -> Self {
Self {
transport_kind: TransportKind::Iroh,
transport_kind: TransportKind::Iroh {
endpoint_id: String::new(),
},
interface_kind: InterfaceKind::Ssh,
listen_addr: addr.into(),
tls_cert: None,
tls_key: None,
@@ -90,7 +98,10 @@ impl ListenerConfig {
pub fn dns(domain: impl Into<String>) -> Self {
Self {
transport_kind: TransportKind::Dns,
transport_kind: TransportKind::Dns {
domain: String::new(),
},
interface_kind: InterfaceKind::RawFraming,
listen_addr: domain.into(),
tls_cert: None,
tls_key: None,
@@ -102,7 +113,10 @@ impl ListenerConfig {
pub fn webtransport(host: impl Into<String>) -> Self {
Self {
transport_kind: TransportKind::WebTransport,
transport_kind: TransportKind::WebTransport {
host: String::new(),
},
interface_kind: InterfaceKind::Ssh,
listen_addr: host.into(),
tls_cert: None,
tls_key: None,
@@ -138,14 +152,14 @@ impl ListenerConfig {
}
pub fn validate(&self) -> Result<(), ConfigError> {
if self.stealth && self.transport_kind != TransportKind::Tls {
if self.stealth && !matches!(self.transport_kind, TransportKind::Tls { .. }) {
return Err(ConfigError::InvalidFlag {
name: "stealth mode requires TLS transport".to_string(),
});
}
match self.transport_kind {
TransportKind::Tls => {
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(),
@@ -163,9 +177,9 @@ impl ListenerConfig {
}
}
TransportKind::Tcp
| TransportKind::Iroh
| TransportKind::Dns
| TransportKind::WebTransport => {
| 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);
}
@@ -179,9 +193,9 @@ impl ListenerConfig {
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),
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),
}
}
@@ -474,11 +488,11 @@ impl Server {
.first()
.expect("at least one listener required");
let transport_kind = listener.transport_kind;
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 matches!(transport_kind, TransportKind::Iroh { .. }) {
if let Some(id) = endpoint_info {
info!("alknet server running: transport=iroh endpoint_id={}", id);
} else {
@@ -538,7 +552,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),
@@ -555,7 +569,7 @@ impl Server {
let config = Arc::clone(&server.config);
let sessions = Arc::clone(&server.sessions);
let transport_is_tls = matches!(transport_kind, TransportKind::Tls);
let transport_is_tls = matches!(transport_kind, TransportKind::Tls { .. });
tokio::spawn(async move {
let result =
@@ -830,7 +844,7 @@ mod tests {
.tls_cert("/cert.pem")
.tls_key("/key.pem")
.stealth(true);
assert_eq!(lc.transport_kind, TransportKind::Tls);
assert_eq!(lc.transport_kind, TransportKind::Tls { server_name: None });
assert_eq!(lc.listen_addr, "0.0.0.0:443");
assert!(lc.stealth);
assert_eq!(lc.tls_cert.as_deref(), Some("/cert.pem"));
@@ -840,21 +854,36 @@ mod tests {
#[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.transport_kind,
TransportKind::Iroh {
endpoint_id: String::new()
}
);
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.transport_kind,
TransportKind::Dns {
domain: String::new()
}
);
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.transport_kind,
TransportKind::WebTransport {
host: String::new()
}
);
assert_eq!(lc.listen_addr, "example.com");
}
@@ -1006,7 +1035,10 @@ mod tests {
.stealth(true);
let server = Server::new(opts).unwrap();
assert_eq!(server.listeners.len(), 1);
assert_eq!(server.listeners[0].transport_kind, TransportKind::Tls);
assert_eq!(
server.listeners[0].transport_kind,
TransportKind::Tls { server_name: None }
);
assert!(server.listeners[0].stealth);
assert_eq!(server.listeners[0].tls_cert.as_deref(), Some("/cert.pem"));
}
@@ -1025,7 +1057,10 @@ mod tests {
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);
assert_eq!(
server.listeners()[1].transport_kind,
TransportKind::Tls { server_name: None }
);
}
#[test]