feat(core): rename Interface to StreamInterface, add MessageInterface, restructure ListenerConfig
Per ADR-035: split Interface trait into StreamInterface (stream-based, SSH/RawFraming)
and MessageInterface (request/response, HTTP/DNS). Remove TransportKind::Dns (DNS is
a MessageInterface). Change WebTransport { host } to { server_name: Option<String> }.
Restructure ListenerConfig from flat struct to enum with Stream/Http/Dns variants.
This commit is contained in:
@@ -499,17 +499,13 @@ mod tests {
|
|||||||
target: TargetPattern::AlknetPrefix,
|
target: TargetPattern::AlknetPrefix,
|
||||||
action: ForwardingAction::Allow,
|
action: ForwardingAction::Allow,
|
||||||
principals: vec![],
|
principals: vec![],
|
||||||
transports: vec![TransportKind::WebTransport {
|
transports: vec![TransportKind::WebTransport { server_name: None }],
|
||||||
host: String::new(),
|
|
||||||
}],
|
|
||||||
},
|
},
|
||||||
ForwardingRule {
|
ForwardingRule {
|
||||||
target: TargetPattern::Any,
|
target: TargetPattern::Any,
|
||||||
action: ForwardingAction::Deny,
|
action: ForwardingAction::Deny,
|
||||||
principals: vec![],
|
principals: vec![],
|
||||||
transports: vec![TransportKind::WebTransport {
|
transports: vec![TransportKind::WebTransport { server_name: None }],
|
||||||
host: String::new(),
|
|
||||||
}],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -518,17 +514,13 @@ mod tests {
|
|||||||
"alknet-control",
|
"alknet-control",
|
||||||
0,
|
0,
|
||||||
&identity,
|
&identity,
|
||||||
TransportKind::WebTransport {
|
TransportKind::WebTransport { server_name: None }
|
||||||
host: String::new()
|
|
||||||
}
|
|
||||||
));
|
));
|
||||||
assert!(!policy.check(
|
assert!(!policy.check(
|
||||||
"example.com",
|
"example.com",
|
||||||
443,
|
443,
|
||||||
&identity,
|
&identity,
|
||||||
TransportKind::WebTransport {
|
TransportKind::WebTransport { server_name: None }
|
||||||
host: String::new()
|
|
||||||
}
|
|
||||||
));
|
));
|
||||||
assert!(policy.check("example.com", 443, &identity, TransportKind::Tcp));
|
assert!(policy.check("example.com", 443, &identity, TransportKind::Tcp));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
//!
|
//!
|
||||||
//! See [ADR-030](docs/architecture/decisions/030-dynamic-config.md).
|
//! See [ADR-030](docs/architecture/decisions/030-dynamic-config.md).
|
||||||
|
|
||||||
use crate::interface::InterfaceKind;
|
use crate::interface::StreamInterfaceKind;
|
||||||
use crate::server::handler::{ProxyConfig, ProxyMode};
|
use crate::server::handler::{ProxyConfig, ProxyMode};
|
||||||
use crate::server::serve::{ListenerConfig, ServeTransportMode};
|
use crate::server::serve::{ListenerConfig, ServeTransportMode, StreamListenerConfig};
|
||||||
use crate::transport::TransportKind;
|
use crate::transport::TransportKind;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
@@ -66,21 +66,23 @@ impl StaticConfig {
|
|||||||
let listeners = if let Some(listeners) = opts.listeners {
|
let listeners = if let Some(listeners) = opts.listeners {
|
||||||
listeners
|
listeners
|
||||||
} else {
|
} else {
|
||||||
vec![ListenerConfig {
|
vec![ListenerConfig::Stream {
|
||||||
transport_kind: match opts.transport_mode {
|
config: StreamListenerConfig {
|
||||||
ServeTransportMode::Tcp => TransportKind::Tcp,
|
transport_kind: match opts.transport_mode {
|
||||||
ServeTransportMode::Tls => TransportKind::Tls { server_name: None },
|
ServeTransportMode::Tcp => TransportKind::Tcp,
|
||||||
ServeTransportMode::Iroh => TransportKind::Iroh {
|
ServeTransportMode::Tls => TransportKind::Tls { server_name: None },
|
||||||
endpoint_id: String::new(),
|
ServeTransportMode::Iroh => TransportKind::Iroh {
|
||||||
|
endpoint_id: String::new(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
interface: StreamInterfaceKind::Ssh,
|
||||||
|
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(),
|
||||||
},
|
},
|
||||||
interface_kind: InterfaceKind::Ssh,
|
|
||||||
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(),
|
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,23 +113,23 @@ fn parse_proxy_config(
|
|||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
Some(url) => {
|
Some(url) => {
|
||||||
if let Some(rest) = url.strip_prefix("socks5://") {
|
if let Some(rest) = url.strip_prefix("socks5://") {
|
||||||
let addr: SocketAddr = rest.parse().map_err(|e| {
|
let addr: SocketAddr =
|
||||||
crate::error::ConfigError::ProxyConfigInvalid {
|
rest.parse()
|
||||||
message: format!("invalid socks5 proxy address '{}': {}", rest, e),
|
.map_err(|e| crate::error::ConfigError::ProxyConfigInvalid {
|
||||||
}
|
message: format!("invalid socks5 proxy address '{}': {}", rest, e),
|
||||||
})?;
|
})?;
|
||||||
Ok(Some(ProxyConfig {
|
Ok(Some(ProxyConfig {
|
||||||
mode: ProxyMode::Socks5(addr),
|
mode: ProxyMode::Socks5(addr),
|
||||||
}))
|
}))
|
||||||
} else if let Some(rest) = url.strip_prefix("http://") {
|
} else if let Some(rest) = url.strip_prefix("http://") {
|
||||||
let addr: SocketAddr = rest.parse().map_err(|e| {
|
let addr: SocketAddr =
|
||||||
crate::error::ConfigError::ProxyConfigInvalid {
|
rest.parse()
|
||||||
message: format!(
|
.map_err(|e| crate::error::ConfigError::ProxyConfigInvalid {
|
||||||
"invalid http connect proxy address '{}': {}",
|
message: format!(
|
||||||
rest, e
|
"invalid http connect proxy address '{}': {}",
|
||||||
),
|
rest, e
|
||||||
}
|
),
|
||||||
})?;
|
})?;
|
||||||
Ok(Some(ProxyConfig {
|
Ok(Some(ProxyConfig {
|
||||||
mode: ProxyMode::HttpConnect(addr),
|
mode: ProxyMode::HttpConnect(addr),
|
||||||
}))
|
}))
|
||||||
@@ -239,10 +241,12 @@ mod tests {
|
|||||||
.listeners(listeners);
|
.listeners(listeners);
|
||||||
let (static_config, _) = StaticConfig::from_serve_options(opts).unwrap();
|
let (static_config, _) = StaticConfig::from_serve_options(opts).unwrap();
|
||||||
assert_eq!(static_config.listeners.len(), 1);
|
assert_eq!(static_config.listeners.len(), 1);
|
||||||
assert_eq!(
|
match &static_config.listeners[0] {
|
||||||
static_config.listeners[0].transport_kind,
|
ListenerConfig::Stream { config } => {
|
||||||
TransportKind::Tcp
|
assert_eq!(config.transport_kind, TransportKind::Tcp);
|
||||||
);
|
}
|
||||||
|
_ => panic!("expected Stream variant"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -2,22 +2,39 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use russh::keys::PrivateKey;
|
use russh::keys::PrivateKey;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::auth::IdentityProvider;
|
use crate::auth::IdentityProvider;
|
||||||
use crate::config::DynamicConfig;
|
use crate::config::DynamicConfig;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum InterfaceKind {
|
pub enum StreamInterfaceKind {
|
||||||
Ssh,
|
Ssh,
|
||||||
RawFraming,
|
RawFraming,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for InterfaceKind {
|
impl std::fmt::Display for StreamInterfaceKind {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
InterfaceKind::Ssh => write!(f, "ssh"),
|
StreamInterfaceKind::Ssh => write!(f, "ssh"),
|
||||||
InterfaceKind::RawFraming => write!(f, "raw-framing"),
|
StreamInterfaceKind::RawFraming => write!(f, "raw-framing"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum MessageInterfaceKind {
|
||||||
|
Http,
|
||||||
|
Dns,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for MessageInterfaceKind {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
MessageInterfaceKind::Http => write!(f, "http"),
|
||||||
|
MessageInterfaceKind::Dns => write!(f, "dns"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,12 +46,61 @@ pub enum InterfaceConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl InterfaceConfig {
|
impl InterfaceConfig {
|
||||||
pub fn kind(&self) -> InterfaceKind {
|
pub fn kind(&self) -> StreamInterfaceKind {
|
||||||
#[allow(unreachable_patterns)]
|
#[allow(unreachable_patterns)]
|
||||||
match self {
|
match self {
|
||||||
InterfaceConfig::Ssh(_) => InterfaceKind::Ssh,
|
InterfaceConfig::Ssh(_) => StreamInterfaceKind::Ssh,
|
||||||
InterfaceConfig::RawFraming(_) => InterfaceKind::RawFraming,
|
InterfaceConfig::RawFraming(_) => StreamInterfaceKind::RawFraming,
|
||||||
_ => InterfaceKind::Ssh,
|
_ => StreamInterfaceKind::Ssh,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum StreamInterfaceConfig {
|
||||||
|
Ssh(SshInterfaceConfig),
|
||||||
|
RawFraming(RawFramingConfig),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StreamInterfaceConfig {
|
||||||
|
pub fn kind(&self) -> StreamInterfaceKind {
|
||||||
|
match self {
|
||||||
|
StreamInterfaceConfig::Ssh(_) => StreamInterfaceKind::Ssh,
|
||||||
|
StreamInterfaceConfig::RawFraming(_) => StreamInterfaceKind::RawFraming,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for StreamInterfaceConfig {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
StreamInterfaceConfig::Ssh(_) => write!(f, "ssh"),
|
||||||
|
StreamInterfaceConfig::RawFraming(_) => write!(f, "raw-framing"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum MessageInterfaceConfig {
|
||||||
|
Http(HttpInterfaceConfig),
|
||||||
|
Dns(DnsInterfaceConfig),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageInterfaceConfig {
|
||||||
|
pub fn kind(&self) -> MessageInterfaceKind {
|
||||||
|
match self {
|
||||||
|
MessageInterfaceConfig::Http(_) => MessageInterfaceKind::Http,
|
||||||
|
MessageInterfaceConfig::Dns(_) => MessageInterfaceKind::Dns,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for MessageInterfaceConfig {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
MessageInterfaceConfig::Http(_) => write!(f, "http"),
|
||||||
|
MessageInterfaceConfig::Dns(_) => write!(f, "dns"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,22 +113,53 @@ pub struct SshInterfaceConfig {
|
|||||||
|
|
||||||
pub struct RawFramingConfig {}
|
pub struct RawFramingConfig {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct HttpInterfaceConfig {
|
||||||
|
pub bind_addr: std::net::SocketAddr,
|
||||||
|
pub tls: bool,
|
||||||
|
pub stealth: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for HttpInterfaceConfig {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "http {}", self.bind_addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct DnsInterfaceConfig {
|
||||||
|
pub bind_addr: std::net::SocketAddr,
|
||||||
|
pub tls: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for DnsInterfaceConfig {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "dns {}", self.bind_addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn interface_kind_display() {
|
fn stream_interface_kind_display() {
|
||||||
assert_eq!(InterfaceKind::Ssh.to_string(), "ssh");
|
assert_eq!(StreamInterfaceKind::Ssh.to_string(), "ssh");
|
||||||
assert_eq!(InterfaceKind::RawFraming.to_string(), "raw-framing");
|
assert_eq!(StreamInterfaceKind::RawFraming.to_string(), "raw-framing");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn interface_kind_from_config() {
|
fn message_interface_kind_display() {
|
||||||
|
assert_eq!(MessageInterfaceKind::Http.to_string(), "http");
|
||||||
|
assert_eq!(MessageInterfaceKind::Dns.to_string(), "dns");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stream_interface_config_kind() {
|
||||||
let auth = Arc::new(crate::auth::ConfigIdentityProvider::new(Arc::new(
|
let auth = Arc::new(crate::auth::ConfigIdentityProvider::new(Arc::new(
|
||||||
ArcSwap::new(Arc::new(DynamicConfig::default())),
|
ArcSwap::new(Arc::new(DynamicConfig::default())),
|
||||||
)));
|
)));
|
||||||
let ssh_config = InterfaceConfig::Ssh(SshInterfaceConfig {
|
let ssh_config = StreamInterfaceConfig::Ssh(SshInterfaceConfig {
|
||||||
auth,
|
auth,
|
||||||
forwarding: Arc::new(ArcSwap::new(Arc::new(DynamicConfig::default()))),
|
forwarding: Arc::new(ArcSwap::new(Arc::new(DynamicConfig::default()))),
|
||||||
host_key: Arc::new(
|
host_key: Arc::new(
|
||||||
@@ -73,21 +170,91 @@ mod tests {
|
|||||||
.unwrap(),
|
.unwrap(),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
assert_eq!(ssh_config.kind(), InterfaceKind::Ssh);
|
assert_eq!(ssh_config.kind(), StreamInterfaceKind::Ssh);
|
||||||
|
|
||||||
let raw_config = InterfaceConfig::RawFraming(RawFramingConfig {});
|
let raw_config = StreamInterfaceConfig::RawFraming(RawFramingConfig {});
|
||||||
assert_eq!(raw_config.kind(), InterfaceKind::RawFraming);
|
assert_eq!(raw_config.kind(), StreamInterfaceKind::RawFraming);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn interface_kind_equality() {
|
fn message_interface_config_kind() {
|
||||||
assert_eq!(InterfaceKind::Ssh, InterfaceKind::Ssh);
|
let http_config = MessageInterfaceConfig::Http(HttpInterfaceConfig {
|
||||||
assert_eq!(InterfaceKind::RawFraming, InterfaceKind::RawFraming);
|
bind_addr: "127.0.0.1:8080".parse().unwrap(),
|
||||||
assert_ne!(InterfaceKind::Ssh, InterfaceKind::RawFraming);
|
tls: false,
|
||||||
|
stealth: false,
|
||||||
|
});
|
||||||
|
assert_eq!(http_config.kind(), MessageInterfaceKind::Http);
|
||||||
|
|
||||||
|
let dns_config = MessageInterfaceConfig::Dns(DnsInterfaceConfig {
|
||||||
|
bind_addr: "127.0.0.1:53".parse().unwrap(),
|
||||||
|
tls: false,
|
||||||
|
});
|
||||||
|
assert_eq!(dns_config.kind(), MessageInterfaceKind::Dns);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stream_interface_kind_equality() {
|
||||||
|
assert_eq!(StreamInterfaceKind::Ssh, StreamInterfaceKind::Ssh);
|
||||||
|
assert_eq!(
|
||||||
|
StreamInterfaceKind::RawFraming,
|
||||||
|
StreamInterfaceKind::RawFraming
|
||||||
|
);
|
||||||
|
assert_ne!(StreamInterfaceKind::Ssh, StreamInterfaceKind::RawFraming);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn message_interface_kind_equality() {
|
||||||
|
assert_eq!(MessageInterfaceKind::Http, MessageInterfaceKind::Http);
|
||||||
|
assert_eq!(MessageInterfaceKind::Dns, MessageInterfaceKind::Dns);
|
||||||
|
assert_ne!(MessageInterfaceKind::Http, MessageInterfaceKind::Dns);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn raw_framing_config_minimal() {
|
fn raw_framing_config_minimal() {
|
||||||
let _config = RawFramingConfig {};
|
let _config = RawFramingConfig {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn http_interface_config_display() {
|
||||||
|
let config = HttpInterfaceConfig {
|
||||||
|
bind_addr: "127.0.0.1:8080".parse().unwrap(),
|
||||||
|
tls: true,
|
||||||
|
stealth: true,
|
||||||
|
};
|
||||||
|
assert_eq!(config.to_string(), "http 127.0.0.1:8080");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dns_interface_config_display() {
|
||||||
|
let config = DnsInterfaceConfig {
|
||||||
|
bind_addr: "127.0.0.1:53".parse().unwrap(),
|
||||||
|
tls: false,
|
||||||
|
};
|
||||||
|
assert_eq!(config.to_string(), "dns 127.0.0.1:53");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn http_interface_config_serialization() {
|
||||||
|
let config = HttpInterfaceConfig {
|
||||||
|
bind_addr: "127.0.0.1:8080".parse().unwrap(),
|
||||||
|
tls: true,
|
||||||
|
stealth: false,
|
||||||
|
};
|
||||||
|
let serialized = serde_json::to_string(&config).unwrap();
|
||||||
|
let deserialized: HttpInterfaceConfig = serde_json::from_str(&serialized).unwrap();
|
||||||
|
assert_eq!(deserialized.bind_addr, config.bind_addr);
|
||||||
|
assert_eq!(deserialized.tls, config.tls);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dns_interface_config_serialization() {
|
||||||
|
let config = DnsInterfaceConfig {
|
||||||
|
bind_addr: "0.0.0.0:53".parse().unwrap(),
|
||||||
|
tls: true,
|
||||||
|
};
|
||||||
|
let serialized = serde_json::to_string(&config).unwrap();
|
||||||
|
let deserialized: DnsInterfaceConfig = serde_json::from_str(&serialized).unwrap();
|
||||||
|
assert_eq!(deserialized.bind_addr, config.bind_addr);
|
||||||
|
assert_eq!(deserialized.tls, config.tls);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
crates/alknet-core/src/interface/dns.rs
Normal file
47
crates/alknet-core/src/interface/dns.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::call::OperationEnv;
|
||||||
|
use crate::interface::{InterfaceRequest, InterfaceResponse, MessageInterface};
|
||||||
|
|
||||||
|
pub struct DnsInterface {
|
||||||
|
pub domain: String,
|
||||||
|
pub identity_provider: Arc<dyn crate::auth::IdentityProvider>,
|
||||||
|
pub registry: Arc<crate::call::OperationRegistry>,
|
||||||
|
pub env: OperationEnv,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl MessageInterface for DnsInterface {
|
||||||
|
async fn handle_request(&self, _request: InterfaceRequest) -> Result<InterfaceResponse> {
|
||||||
|
Ok(InterfaceResponse {
|
||||||
|
result: Err(crate::call::CallError::new(
|
||||||
|
"NOT_IMPLEMENTED",
|
||||||
|
"DnsInterface is not yet implemented",
|
||||||
|
false,
|
||||||
|
)),
|
||||||
|
status: 501,
|
||||||
|
headers: std::collections::HashMap::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dns_interface_type_exists() {
|
||||||
|
let registry = Arc::new(crate::call::OperationRegistry::new());
|
||||||
|
let _iface = DnsInterface {
|
||||||
|
domain: "alk.dev".to_string(),
|
||||||
|
identity_provider: Arc::new(crate::auth::ConfigIdentityProvider::new(Arc::new(
|
||||||
|
arc_swap::ArcSwap::new(Arc::new(crate::config::DynamicConfig::default())),
|
||||||
|
))),
|
||||||
|
env: OperationEnv::local(crate::call::OperationRegistry::new()),
|
||||||
|
registry,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
45
crates/alknet-core/src/interface/http.rs
Normal file
45
crates/alknet-core/src/interface/http.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::call::OperationEnv;
|
||||||
|
use crate::interface::{InterfaceRequest, InterfaceResponse, MessageInterface};
|
||||||
|
|
||||||
|
pub struct HttpInterface {
|
||||||
|
pub identity_provider: Arc<dyn crate::auth::IdentityProvider>,
|
||||||
|
pub registry: Arc<crate::call::OperationRegistry>,
|
||||||
|
pub env: OperationEnv,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl MessageInterface for HttpInterface {
|
||||||
|
async fn handle_request(&self, _request: InterfaceRequest) -> Result<InterfaceResponse> {
|
||||||
|
Ok(InterfaceResponse {
|
||||||
|
result: Err(crate::call::CallError::new(
|
||||||
|
"NOT_IMPLEMENTED",
|
||||||
|
"HttpInterface is not yet implemented",
|
||||||
|
false,
|
||||||
|
)),
|
||||||
|
status: 501,
|
||||||
|
headers: std::collections::HashMap::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn http_interface_type_exists() {
|
||||||
|
let registry = Arc::new(crate::call::OperationRegistry::new());
|
||||||
|
let _iface = HttpInterface {
|
||||||
|
identity_provider: Arc::new(crate::auth::ConfigIdentityProvider::new(Arc::new(
|
||||||
|
arc_swap::ArcSwap::new(Arc::new(crate::config::DynamicConfig::default())),
|
||||||
|
))),
|
||||||
|
env: OperationEnv::local(crate::call::OperationRegistry::new()),
|
||||||
|
registry,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +1,37 @@
|
|||||||
//! Interface layer (Layer 2) of the three-layer model (ADR-026).
|
//! Interface layer (Layer 2) of the three-layer model (ADR-026, ADR-035).
|
||||||
//!
|
//!
|
||||||
//! The Interface layer sits between Transport (Layer 1) and Protocol (Layer 3).
|
//! The Interface layer sits between Transport (Layer 1) and Protocol (Layer 3).
|
||||||
//! An Interface consumes a `TransportStream` and produces call protocol sessions
|
//! It has two distinct patterns:
|
||||||
//! that yield `EventEnvelope` frames. This enables the call protocol handler to be
|
|
||||||
//! interface-agnostic — it receives `InterfaceEvent` frames from any interface.
|
|
||||||
//!
|
//!
|
||||||
//! SSH is an interface, not a transport. It wraps a byte stream in session
|
//! - **StreamInterface** — consumes a `TransportStream`, produces a long-lived
|
||||||
//! semantics (handshake, auth, channel multiplexing). Raw framing (4-byte length
|
//! `Session` that yields `InterfaceEvent` frames. SSH and raw framing are
|
||||||
//! prefix + JSON `EventEnvelope`) is another interface, one without SSH overhead.
|
//! `StreamInterface` implementations.
|
||||||
//!
|
//!
|
||||||
//! # OQ-IF-01 Resolution
|
//! - **MessageInterface** — handles individual `InterfaceRequest` →
|
||||||
//!
|
//! `InterfaceResponse` pairs. Manages its own transport (HTTP server, DNS
|
||||||
//! Every Interface session implements the `InterfaceSession` trait, which provides
|
//! server). HTTP and DNS are `MessageInterface` implementations.
|
||||||
//! `recv()` and `send()` methods producing and consuming `InterfaceEvent` frames.
|
|
||||||
//! Each `InterfaceEvent` carries an `EventEnvelope` and an optional `Identity`
|
|
||||||
//! (authenticated by the interface layer, e.g., via SSH public key auth or
|
|
||||||
//! transport-level token auth).
|
|
||||||
//!
|
|
||||||
//! This means the call protocol handler (Layer 3) is completely interface-agnostic:
|
|
||||||
//! it receives `InterfaceEvent` frames and processes them uniformly, regardless
|
|
||||||
//! of whether they arrived over SSH or raw framing.
|
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod dns;
|
||||||
|
pub mod http;
|
||||||
pub mod pairs;
|
pub mod pairs;
|
||||||
pub mod raw_framing;
|
pub mod raw_framing;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod ssh;
|
pub mod ssh;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use tokio::io::{AsyncRead, AsyncWrite};
|
use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
|
|
||||||
pub use config::{InterfaceConfig, InterfaceKind, RawFramingConfig, SshInterfaceConfig};
|
pub use config::{
|
||||||
|
DnsInterfaceConfig, HttpInterfaceConfig, InterfaceConfig, MessageInterfaceConfig,
|
||||||
|
MessageInterfaceKind, RawFramingConfig, SshInterfaceConfig, StreamInterfaceConfig,
|
||||||
|
StreamInterfaceKind,
|
||||||
|
};
|
||||||
|
pub use dns::DnsInterface;
|
||||||
|
pub use http::HttpInterface;
|
||||||
pub use pairs::{is_valid_pair, TransportKindBase, VALID_TRANSPORT_INTERFACE_PAIRS};
|
pub use pairs::{is_valid_pair, TransportKindBase, VALID_TRANSPORT_INTERFACE_PAIRS};
|
||||||
pub use raw_framing::{RawFramingInterface, RawFramingSession};
|
pub use raw_framing::{RawFramingInterface, RawFramingSession};
|
||||||
pub use session::{InterfaceEvent, InterfaceSession};
|
pub use session::{InterfaceEvent, InterfaceSession};
|
||||||
@@ -42,16 +42,36 @@ pub trait TransportStream: AsyncRead + AsyncWrite + Unpin + Send + 'static {}
|
|||||||
impl<T: AsyncRead + AsyncWrite + Unpin + Send + 'static> TransportStream for T {}
|
impl<T: AsyncRead + AsyncWrite + Unpin + Send + 'static> TransportStream for T {}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Interface: Send + Sync + 'static {
|
pub trait StreamInterface: Send + Sync + 'static {
|
||||||
type Session: InterfaceSession;
|
type Session: InterfaceSession;
|
||||||
|
|
||||||
async fn accept(
|
async fn accept(
|
||||||
&self,
|
&self,
|
||||||
stream: Box<dyn TransportStream>,
|
stream: Box<dyn TransportStream>,
|
||||||
config: &InterfaceConfig,
|
config: &StreamInterfaceConfig,
|
||||||
) -> Result<Self::Session>;
|
) -> Result<Self::Session>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait MessageInterface: Send + Sync + 'static {
|
||||||
|
async fn handle_request(&self, request: InterfaceRequest) -> Result<InterfaceResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct InterfaceRequest {
|
||||||
|
pub operation_path: String,
|
||||||
|
pub input: serde_json::Value,
|
||||||
|
pub auth_token: Option<crate::auth::AuthToken>,
|
||||||
|
pub metadata: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct InterfaceResponse {
|
||||||
|
pub result: Result<serde_json::Value, crate::call::CallError>,
|
||||||
|
pub status: u16,
|
||||||
|
pub headers: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -69,4 +89,52 @@ mod tests {
|
|||||||
let _boxed: Box<dyn TransportStream> = Box::new(server);
|
let _boxed: Box<dyn TransportStream> = Box::new(server);
|
||||||
let _: Box<dyn TransportStream> = Box::new(client);
|
let _: Box<dyn TransportStream> = Box::new(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn interface_request_fields() {
|
||||||
|
let req = InterfaceRequest {
|
||||||
|
operation_path: "/v1/head/auth/verify".to_string(),
|
||||||
|
input: serde_json::json!({"key": "value"}),
|
||||||
|
auth_token: None,
|
||||||
|
metadata: HashMap::new(),
|
||||||
|
};
|
||||||
|
assert_eq!(req.operation_path, "/v1/head/auth/verify");
|
||||||
|
assert!(req.auth_token.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn interface_response_fields() {
|
||||||
|
let resp = InterfaceResponse {
|
||||||
|
result: Ok(serde_json::json!({"status": "ok"})),
|
||||||
|
status: 200,
|
||||||
|
headers: HashMap::new(),
|
||||||
|
};
|
||||||
|
assert_eq!(resp.status, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MockMessageInterface;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl MessageInterface for MockMessageInterface {
|
||||||
|
async fn handle_request(&self, _request: InterfaceRequest) -> Result<InterfaceResponse> {
|
||||||
|
Ok(InterfaceResponse {
|
||||||
|
result: Ok(serde_json::json!({})),
|
||||||
|
status: 200,
|
||||||
|
headers: HashMap::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn message_interface_trait_compiles() {
|
||||||
|
let iface = MockMessageInterface;
|
||||||
|
let req = InterfaceRequest {
|
||||||
|
operation_path: "/test".to_string(),
|
||||||
|
input: serde_json::json!({}),
|
||||||
|
auth_token: None,
|
||||||
|
metadata: HashMap::new(),
|
||||||
|
};
|
||||||
|
let resp = iface.handle_request(req).await.unwrap();
|
||||||
|
assert_eq!(resp.status, 200);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
use crate::transport::TransportKind;
|
use crate::transport::TransportKind;
|
||||||
|
|
||||||
use super::config::InterfaceKind;
|
use super::config::StreamInterfaceKind;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum TransportKindBase {
|
pub enum TransportKindBase {
|
||||||
Tcp,
|
Tcp,
|
||||||
Tls,
|
Tls,
|
||||||
Iroh,
|
Iroh,
|
||||||
Dns,
|
|
||||||
WebTransport,
|
WebTransport,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,33 +15,36 @@ fn transport_base(kind: &TransportKind) -> TransportKindBase {
|
|||||||
TransportKind::Tcp => TransportKindBase::Tcp,
|
TransportKind::Tcp => TransportKindBase::Tcp,
|
||||||
TransportKind::Tls { .. } => TransportKindBase::Tls,
|
TransportKind::Tls { .. } => TransportKindBase::Tls,
|
||||||
TransportKind::Iroh { .. } => TransportKindBase::Iroh,
|
TransportKind::Iroh { .. } => TransportKindBase::Iroh,
|
||||||
TransportKind::Dns { .. } => TransportKindBase::Dns,
|
|
||||||
TransportKind::WebTransport { .. } => TransportKindBase::WebTransport,
|
TransportKind::WebTransport { .. } => TransportKindBase::WebTransport,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_valid_pair(transport: &TransportKind, interface: InterfaceKind) -> bool {
|
pub fn is_valid_pair(transport: &TransportKind, interface: StreamInterfaceKind) -> bool {
|
||||||
let base = transport_base(transport);
|
let base = transport_base(transport);
|
||||||
matches!(
|
matches!(
|
||||||
(base, interface),
|
(base, interface),
|
||||||
(TransportKindBase::Tcp, InterfaceKind::Ssh)
|
(TransportKindBase::Tcp, StreamInterfaceKind::Ssh)
|
||||||
| (TransportKindBase::Tls, InterfaceKind::Ssh)
|
| (TransportKindBase::Tls, StreamInterfaceKind::Ssh)
|
||||||
| (TransportKindBase::Iroh, InterfaceKind::Ssh)
|
| (TransportKindBase::Iroh, StreamInterfaceKind::Ssh)
|
||||||
| (TransportKindBase::Dns, InterfaceKind::RawFraming)
|
| (TransportKindBase::WebTransport, StreamInterfaceKind::Ssh)
|
||||||
| (TransportKindBase::WebTransport, InterfaceKind::Ssh)
|
| (
|
||||||
| (TransportKindBase::WebTransport, InterfaceKind::RawFraming)
|
TransportKindBase::WebTransport,
|
||||||
| (TransportKindBase::Tcp, InterfaceKind::RawFraming)
|
StreamInterfaceKind::RawFraming
|
||||||
|
)
|
||||||
|
| (TransportKindBase::Tcp, StreamInterfaceKind::RawFraming)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const VALID_TRANSPORT_INTERFACE_PAIRS: &[(TransportKindBase, InterfaceKind)] = &[
|
pub const VALID_TRANSPORT_INTERFACE_PAIRS: &[(TransportKindBase, StreamInterfaceKind)] = &[
|
||||||
(TransportKindBase::Tcp, InterfaceKind::Ssh),
|
(TransportKindBase::Tcp, StreamInterfaceKind::Ssh),
|
||||||
(TransportKindBase::Tls, InterfaceKind::Ssh),
|
(TransportKindBase::Tls, StreamInterfaceKind::Ssh),
|
||||||
(TransportKindBase::Iroh, InterfaceKind::Ssh),
|
(TransportKindBase::Iroh, StreamInterfaceKind::Ssh),
|
||||||
(TransportKindBase::Dns, InterfaceKind::RawFraming),
|
(TransportKindBase::WebTransport, StreamInterfaceKind::Ssh),
|
||||||
(TransportKindBase::WebTransport, InterfaceKind::Ssh),
|
(
|
||||||
(TransportKindBase::WebTransport, InterfaceKind::RawFraming),
|
TransportKindBase::WebTransport,
|
||||||
(TransportKindBase::Tcp, InterfaceKind::RawFraming),
|
StreamInterfaceKind::RawFraming,
|
||||||
|
),
|
||||||
|
(TransportKindBase::Tcp, StreamInterfaceKind::RawFraming),
|
||||||
];
|
];
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -51,22 +53,20 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn valid_ssh_pairs() {
|
fn valid_ssh_pairs() {
|
||||||
assert!(is_valid_pair(&TransportKind::Tcp, InterfaceKind::Ssh));
|
assert!(is_valid_pair(&TransportKind::Tcp, StreamInterfaceKind::Ssh));
|
||||||
assert!(is_valid_pair(
|
assert!(is_valid_pair(
|
||||||
&TransportKind::Tls { server_name: None },
|
&TransportKind::Tls { server_name: None },
|
||||||
InterfaceKind::Ssh
|
StreamInterfaceKind::Ssh
|
||||||
));
|
));
|
||||||
assert!(is_valid_pair(
|
assert!(is_valid_pair(
|
||||||
&TransportKind::Iroh {
|
&TransportKind::Iroh {
|
||||||
endpoint_id: String::new()
|
endpoint_id: String::new()
|
||||||
},
|
},
|
||||||
InterfaceKind::Ssh
|
StreamInterfaceKind::Ssh
|
||||||
));
|
));
|
||||||
assert!(is_valid_pair(
|
assert!(is_valid_pair(
|
||||||
&TransportKind::WebTransport {
|
&TransportKind::WebTransport { server_name: None },
|
||||||
host: String::new()
|
StreamInterfaceKind::Ssh
|
||||||
},
|
|
||||||
InterfaceKind::Ssh
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,35 +74,21 @@ mod tests {
|
|||||||
fn valid_raw_framing_pairs() {
|
fn valid_raw_framing_pairs() {
|
||||||
assert!(is_valid_pair(
|
assert!(is_valid_pair(
|
||||||
&TransportKind::Tcp,
|
&TransportKind::Tcp,
|
||||||
InterfaceKind::RawFraming
|
StreamInterfaceKind::RawFraming
|
||||||
));
|
));
|
||||||
assert!(is_valid_pair(
|
assert!(is_valid_pair(
|
||||||
&TransportKind::Dns {
|
&TransportKind::WebTransport { server_name: None },
|
||||||
domain: String::new()
|
StreamInterfaceKind::RawFraming
|
||||||
},
|
|
||||||
InterfaceKind::RawFraming
|
|
||||||
));
|
|
||||||
assert!(is_valid_pair(
|
|
||||||
&TransportKind::WebTransport {
|
|
||||||
host: String::new()
|
|
||||||
},
|
|
||||||
InterfaceKind::RawFraming
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invalid_pairs() {
|
fn invalid_pairs() {
|
||||||
assert!(!is_valid_pair(
|
|
||||||
&TransportKind::Dns {
|
|
||||||
domain: String::new()
|
|
||||||
},
|
|
||||||
InterfaceKind::Ssh
|
|
||||||
));
|
|
||||||
assert!(!is_valid_pair(
|
assert!(!is_valid_pair(
|
||||||
&TransportKind::Iroh {
|
&TransportKind::Iroh {
|
||||||
endpoint_id: String::new()
|
endpoint_id: String::new()
|
||||||
},
|
},
|
||||||
InterfaceKind::RawFraming
|
StreamInterfaceKind::RawFraming
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,15 +107,9 @@ mod tests {
|
|||||||
}),
|
}),
|
||||||
TransportKindBase::Iroh
|
TransportKindBase::Iroh
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
transport_base(&TransportKind::Dns {
|
|
||||||
domain: "example.com".to_string()
|
|
||||||
}),
|
|
||||||
TransportKindBase::Dns
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
transport_base(&TransportKind::WebTransport {
|
transport_base(&TransportKind::WebTransport {
|
||||||
host: "example.com".to_string()
|
server_name: Some("example.com".to_string())
|
||||||
}),
|
}),
|
||||||
TransportKindBase::WebTransport
|
TransportKindBase::WebTransport
|
||||||
);
|
);
|
||||||
@@ -137,6 +117,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn valid_pairs_table_complete() {
|
fn valid_pairs_table_complete() {
|
||||||
assert_eq!(VALID_TRANSPORT_INTERFACE_PAIRS.len(), 7);
|
assert_eq!(VALID_TRANSPORT_INTERFACE_PAIRS.len(), 6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,20 @@ use anyhow::Result;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use crate::interface::session::{InterfaceEvent, InterfaceSession};
|
use crate::interface::session::{InterfaceEvent, InterfaceSession};
|
||||||
use crate::interface::{Interface, InterfaceConfig, TransportStream};
|
use crate::interface::{StreamInterface, StreamInterfaceConfig, TransportStream};
|
||||||
|
|
||||||
pub struct RawFramingInterface;
|
pub struct RawFramingInterface;
|
||||||
|
|
||||||
pub struct RawFramingSession;
|
pub struct RawFramingSession;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Interface for RawFramingInterface {
|
impl StreamInterface for RawFramingInterface {
|
||||||
type Session = RawFramingSession;
|
type Session = RawFramingSession;
|
||||||
|
|
||||||
async fn accept(
|
async fn accept(
|
||||||
&self,
|
&self,
|
||||||
_stream: Box<dyn TransportStream>,
|
_stream: Box<dyn TransportStream>,
|
||||||
_config: &InterfaceConfig,
|
_config: &StreamInterfaceConfig,
|
||||||
) -> Result<Self::Session> {
|
) -> Result<Self::Session> {
|
||||||
Err(anyhow::anyhow!(
|
Err(anyhow::anyhow!(
|
||||||
"RawFramingInterface is not yet implemented (Phase 4+)"
|
"RawFramingInterface is not yet implemented (Phase 4+)"
|
||||||
@@ -55,7 +55,7 @@ mod tests {
|
|||||||
let iface = RawFramingInterface;
|
let iface = RawFramingInterface;
|
||||||
let (_client, server) = tokio::io::duplex(1024);
|
let (_client, server) = tokio::io::duplex(1024);
|
||||||
let stream: Box<dyn TransportStream> = Box::new(server);
|
let stream: Box<dyn TransportStream> = Box::new(server);
|
||||||
let config = InterfaceConfig::RawFraming(crate::interface::RawFramingConfig {});
|
let config = StreamInterfaceConfig::RawFraming(crate::interface::RawFramingConfig {});
|
||||||
let result = iface.accept(stream, &config).await;
|
let result = iface.accept(stream, &config).await;
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use crate::auth::identity::{Identity, IdentityProvider};
|
|||||||
use crate::call::EventEnvelope;
|
use crate::call::EventEnvelope;
|
||||||
use crate::config::DynamicConfig;
|
use crate::config::DynamicConfig;
|
||||||
use crate::interface::session::{InterfaceEvent, InterfaceSession};
|
use crate::interface::session::{InterfaceEvent, InterfaceSession};
|
||||||
use crate::interface::{Interface, InterfaceConfig, TransportStream};
|
use crate::interface::{StreamInterface, StreamInterfaceConfig, TransportStream};
|
||||||
use crate::server::control_channel::{ControlChannelRouter, ALKNET_PREFIX};
|
use crate::server::control_channel::{ControlChannelRouter, ALKNET_PREFIX};
|
||||||
use crate::server::rate_limit::{AuthAttemptLimiter, ConnectionRateLimiter};
|
use crate::server::rate_limit::{AuthAttemptLimiter, ConnectionRateLimiter};
|
||||||
use crate::transport::TransportKind;
|
use crate::transport::TransportKind;
|
||||||
@@ -553,17 +553,17 @@ impl SshInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Interface for SshInterface {
|
impl StreamInterface for SshInterface {
|
||||||
type Session = SshSession;
|
type Session = SshSession;
|
||||||
|
|
||||||
async fn accept(
|
async fn accept(
|
||||||
&self,
|
&self,
|
||||||
stream: Box<dyn TransportStream>,
|
stream: Box<dyn TransportStream>,
|
||||||
config: &InterfaceConfig,
|
config: &StreamInterfaceConfig,
|
||||||
) -> Result<Self::Session> {
|
) -> Result<Self::Session> {
|
||||||
let ssh_config = match config {
|
let ssh_config = match config {
|
||||||
InterfaceConfig::Ssh(c) => c,
|
StreamInterfaceConfig::Ssh(c) => c,
|
||||||
InterfaceConfig::RawFraming(_) => {
|
StreamInterfaceConfig::RawFraming(_) => {
|
||||||
return Err(anyhow::anyhow!("SshInterface received RawFramingConfig"));
|
return Err(anyhow::anyhow!("SshInterface received RawFramingConfig"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -734,7 +734,7 @@ mod tests {
|
|||||||
let (_client, server) = tokio::io::duplex(1024);
|
let (_client, server) = tokio::io::duplex(1024);
|
||||||
let stream: Box<dyn TransportStream> = Box::new(server);
|
let stream: Box<dyn TransportStream> = Box::new(server);
|
||||||
|
|
||||||
let raw_config = InterfaceConfig::RawFraming(crate::interface::RawFramingConfig {});
|
let raw_config = StreamInterfaceConfig::RawFraming(crate::interface::RawFramingConfig {});
|
||||||
let result = iface.accept(stream, &raw_config).await;
|
let result = iface.accept(stream, &raw_config).await;
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,9 +86,15 @@ pub use config::{
|
|||||||
};
|
};
|
||||||
pub use error::{AuthError, ChannelError, ConfigError, ForwardError, TransportError};
|
pub use error::{AuthError, ChannelError, ConfigError, ForwardError, TransportError};
|
||||||
pub use interface::{
|
pub use interface::{
|
||||||
is_valid_pair, Interface, InterfaceConfig, InterfaceEvent, InterfaceKind, InterfaceSession,
|
is_valid_pair, DnsInterface, DnsInterfaceConfig, HttpInterface, HttpInterfaceConfig,
|
||||||
RawFramingConfig, RawFramingInterface, RawFramingSession, SshInterface, SshInterfaceConfig,
|
InterfaceConfig, InterfaceEvent, InterfaceRequest, InterfaceResponse, InterfaceSession,
|
||||||
SshSession, TransportKindBase, TransportStream, VALID_TRANSPORT_INTERFACE_PAIRS,
|
MessageInterface, MessageInterfaceConfig, MessageInterfaceKind, RawFramingConfig,
|
||||||
|
RawFramingInterface, RawFramingSession, SshInterface, SshInterfaceConfig, SshSession,
|
||||||
|
StreamInterface, StreamInterfaceConfig, StreamInterfaceKind, TransportKindBase,
|
||||||
|
TransportStream, VALID_TRANSPORT_INTERFACE_PAIRS,
|
||||||
|
};
|
||||||
|
pub use server::serve::{
|
||||||
|
DnsListenerConfig, HttpListenerConfig, ListenerConfig, ServeError, ServeOptions,
|
||||||
|
ServeTransportMode, Server, StreamListenerConfig,
|
||||||
};
|
};
|
||||||
pub use server::serve::{ListenerConfig, ServeError, ServeOptions, ServeTransportMode, Server};
|
|
||||||
pub use transport::{Transport, TransportAcceptor, TransportInfo, TransportKind};
|
pub use transport::{Transport, TransportAcceptor, TransportInfo, TransportKind};
|
||||||
|
|||||||
@@ -774,17 +774,7 @@ mod tests {
|
|||||||
"iroh"
|
"iroh"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
TransportKind::Dns {
|
TransportKind::WebTransport { server_name: None }.to_string(),
|
||||||
domain: String::new()
|
|
||||||
}
|
|
||||||
.to_string(),
|
|
||||||
"dns"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
TransportKind::WebTransport {
|
|
||||||
host: String::new()
|
|
||||||
}
|
|
||||||
.to_string(),
|
|
||||||
"webtransport"
|
"webtransport"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ pub use control_channel::{
|
|||||||
};
|
};
|
||||||
pub use handler::{ProxyConfig, ProxyMode, ServerHandler};
|
pub use handler::{ProxyConfig, ProxyMode, ServerHandler};
|
||||||
pub use rate_limit::{AuthAttemptLimiter, ConnectionRateLimiter};
|
pub use rate_limit::{AuthAttemptLimiter, ConnectionRateLimiter};
|
||||||
pub use serve::{ListenerConfig, ServeError, ServeOptions, ServeTransportMode, Server};
|
pub use serve::{
|
||||||
|
DnsListenerConfig, HttpListenerConfig, ListenerConfig, ServeError, ServeOptions,
|
||||||
|
ServeTransportMode, Server, StreamListenerConfig,
|
||||||
|
};
|
||||||
|
|
||||||
pub use crate::transport::TransportKind;
|
pub use crate::transport::TransportKind;
|
||||||
pub use stealth::{
|
pub use stealth::{
|
||||||
|
|||||||
@@ -5,18 +5,20 @@
|
|||||||
//! `ServeOptions` provides a builder-pattern API for programmatic configuration.
|
//! `ServeOptions` provides a builder-pattern API for programmatic configuration.
|
||||||
//! Supports multiple listeners via `ListenerConfig` for multi-transport operation.
|
//! Supports multiple listeners via `ListenerConfig` for multi-transport operation.
|
||||||
|
|
||||||
|
use std::net::SocketAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use russh::server::{self, Config};
|
use russh::server::{self, Config};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::io::{AsyncRead, AsyncWrite};
|
use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
use crate::auth::keys::KeySource;
|
use crate::auth::keys::KeySource;
|
||||||
use crate::config::{ConfigReloadHandle, DynamicConfig};
|
use crate::config::{ConfigReloadHandle, DynamicConfig};
|
||||||
use crate::error::ConfigError;
|
use crate::error::ConfigError;
|
||||||
use crate::interface::InterfaceKind;
|
use crate::interface::StreamInterfaceKind;
|
||||||
use crate::server::handler::{ProxyConfig, ServerHandler};
|
use crate::server::handler::{ProxyConfig, ServerHandler};
|
||||||
use crate::server::rate_limit::ConnectionRateLimiter;
|
use crate::server::rate_limit::ConnectionRateLimiter;
|
||||||
use crate::server::stealth::{self, ProtocolDetection};
|
use crate::server::stealth::{self, ProtocolDetection};
|
||||||
@@ -42,10 +44,10 @@ impl std::fmt::Display for ServeTransportMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ListenerConfig {
|
pub struct StreamListenerConfig {
|
||||||
pub transport_kind: TransportKind,
|
pub transport_kind: TransportKind,
|
||||||
pub interface_kind: InterfaceKind,
|
pub interface: StreamInterfaceKind,
|
||||||
pub listen_addr: String,
|
pub listen_addr: String,
|
||||||
pub tls_cert: Option<String>,
|
pub tls_cert: Option<String>,
|
||||||
pub tls_key: Option<String>,
|
pub tls_key: Option<String>,
|
||||||
@@ -54,103 +56,7 @@ pub struct ListenerConfig {
|
|||||||
pub iroh_relay: Option<String>,
|
pub iroh_relay: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListenerConfig {
|
impl StreamListenerConfig {
|
||||||
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,
|
|
||||||
acme_domain: None,
|
|
||||||
stealth: false,
|
|
||||||
iroh_relay: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tls(addr: impl Into<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
transport_kind: TransportKind::Tls { server_name: None },
|
|
||||||
interface_kind: InterfaceKind::Ssh,
|
|
||||||
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 {
|
|
||||||
endpoint_id: String::new(),
|
|
||||||
},
|
|
||||||
interface_kind: InterfaceKind::Ssh,
|
|
||||||
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 {
|
|
||||||
domain: String::new(),
|
|
||||||
},
|
|
||||||
interface_kind: InterfaceKind::RawFraming,
|
|
||||||
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 {
|
|
||||||
host: String::new(),
|
|
||||||
},
|
|
||||||
interface_kind: InterfaceKind::Ssh,
|
|
||||||
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> {
|
pub fn validate(&self) -> Result<(), ConfigError> {
|
||||||
if self.stealth && !matches!(self.transport_kind, TransportKind::Tls { .. }) {
|
if self.stealth && !matches!(self.transport_kind, TransportKind::Tls { .. }) {
|
||||||
return Err(ConfigError::InvalidFlag {
|
return Err(ConfigError::InvalidFlag {
|
||||||
@@ -178,7 +84,6 @@ impl ListenerConfig {
|
|||||||
}
|
}
|
||||||
TransportKind::Tcp
|
TransportKind::Tcp
|
||||||
| TransportKind::Iroh { .. }
|
| TransportKind::Iroh { .. }
|
||||||
| TransportKind::Dns { .. }
|
|
||||||
| TransportKind::WebTransport { .. } => {
|
| TransportKind::WebTransport { .. } => {
|
||||||
if self.tls_cert.is_some() || self.tls_key.is_some() || self.acme_domain.is_some() {
|
if self.tls_cert.is_some() || self.tls_key.is_some() || self.acme_domain.is_some() {
|
||||||
return Err(ConfigError::IncompatibleOptions);
|
return Err(ConfigError::IncompatibleOptions);
|
||||||
@@ -190,13 +95,190 @@ impl ListenerConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for ListenerConfig {
|
impl std::fmt::Display for StreamListenerConfig {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self.transport_kind {
|
match self.transport_kind {
|
||||||
TransportKind::Iroh { .. } => write!(f, "{} (iroh)", self.listen_addr),
|
TransportKind::Iroh { .. } => {
|
||||||
TransportKind::Dns { .. } => write!(f, "{} (dns)", self.listen_addr),
|
write!(f, "{} (iroh/{})", self.listen_addr, self.interface)
|
||||||
TransportKind::WebTransport { .. } => write!(f, "{} (webtransport)", self.listen_addr),
|
}
|
||||||
_ => write!(f, "{} ({})", self.listen_addr, self.transport_kind),
|
TransportKind::WebTransport { .. } => {
|
||||||
|
write!(f, "{} (webtransport/{})", self.listen_addr, self.interface)
|
||||||
|
}
|
||||||
|
_ => write!(
|
||||||
|
f,
|
||||||
|
"{} ({}/{})",
|
||||||
|
self.listen_addr, self.transport_kind, self.interface
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct HttpListenerConfig {
|
||||||
|
pub bind_addr: SocketAddr,
|
||||||
|
pub tls: bool,
|
||||||
|
pub stealth: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for HttpListenerConfig {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{} (http)", self.bind_addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct DnsListenerConfig {
|
||||||
|
pub bind_addr: SocketAddr,
|
||||||
|
pub tls: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for DnsListenerConfig {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{} (dns)", self.bind_addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum ListenerConfig {
|
||||||
|
Stream { config: StreamListenerConfig },
|
||||||
|
Http { config: HttpListenerConfig },
|
||||||
|
Dns { config: DnsListenerConfig },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ListenerConfig {
|
||||||
|
pub fn tcp(addr: impl Into<String>) -> Self {
|
||||||
|
Self::Stream {
|
||||||
|
config: StreamListenerConfig {
|
||||||
|
transport_kind: TransportKind::Tcp,
|
||||||
|
interface: StreamInterfaceKind::Ssh,
|
||||||
|
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::Stream {
|
||||||
|
config: StreamListenerConfig {
|
||||||
|
transport_kind: TransportKind::Tls { server_name: None },
|
||||||
|
interface: StreamInterfaceKind::Ssh,
|
||||||
|
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::Stream {
|
||||||
|
config: StreamListenerConfig {
|
||||||
|
transport_kind: TransportKind::Iroh {
|
||||||
|
endpoint_id: String::new(),
|
||||||
|
},
|
||||||
|
interface: StreamInterfaceKind::Ssh,
|
||||||
|
listen_addr: addr.into(),
|
||||||
|
tls_cert: None,
|
||||||
|
tls_key: None,
|
||||||
|
acme_domain: None,
|
||||||
|
stealth: false,
|
||||||
|
iroh_relay: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn webtransport(addr: impl Into<String>) -> Self {
|
||||||
|
Self::Stream {
|
||||||
|
config: StreamListenerConfig {
|
||||||
|
transport_kind: TransportKind::WebTransport { server_name: None },
|
||||||
|
interface: StreamInterfaceKind::Ssh,
|
||||||
|
listen_addr: addr.into(),
|
||||||
|
tls_cert: None,
|
||||||
|
tls_key: None,
|
||||||
|
acme_domain: None,
|
||||||
|
stealth: false,
|
||||||
|
iroh_relay: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn http(bind_addr: SocketAddr) -> Self {
|
||||||
|
Self::Http {
|
||||||
|
config: HttpListenerConfig {
|
||||||
|
bind_addr,
|
||||||
|
tls: false,
|
||||||
|
stealth: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dns(bind_addr: SocketAddr) -> Self {
|
||||||
|
Self::Dns {
|
||||||
|
config: DnsListenerConfig {
|
||||||
|
bind_addr,
|
||||||
|
tls: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tls_cert(mut self, path: impl Into<String>) -> Self {
|
||||||
|
if let ListenerConfig::Stream { ref mut config } = self {
|
||||||
|
config.tls_cert = Some(path.into());
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tls_key(mut self, path: impl Into<String>) -> Self {
|
||||||
|
if let ListenerConfig::Stream { ref mut config } = self {
|
||||||
|
config.tls_key = Some(path.into());
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn acme_domain(mut self, domain: impl Into<String>) -> Self {
|
||||||
|
if let ListenerConfig::Stream { ref mut config } = self {
|
||||||
|
config.acme_domain = Some(domain.into());
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stealth(mut self, enabled: bool) -> Self {
|
||||||
|
match &mut self {
|
||||||
|
ListenerConfig::Stream { ref mut config } => config.stealth = enabled,
|
||||||
|
ListenerConfig::Http { ref mut config } => config.stealth = enabled,
|
||||||
|
ListenerConfig::Dns { .. } => {}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iroh_relay(mut self, url: impl Into<String>) -> Self {
|
||||||
|
if let ListenerConfig::Stream { ref mut config } = self {
|
||||||
|
config.iroh_relay = Some(url.into());
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate(&self) -> Result<(), ConfigError> {
|
||||||
|
match self {
|
||||||
|
ListenerConfig::Stream { config } => config.validate(),
|
||||||
|
ListenerConfig::Http { .. } | ListenerConfig::Dns { .. } => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ListenerConfig {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ListenerConfig::Stream { config } => write!(f, "{}", config),
|
||||||
|
ListenerConfig::Http { config } => write!(f, "{}", config),
|
||||||
|
ListenerConfig::Dns { config } => write!(f, "{}", config),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -488,9 +570,21 @@ impl Server {
|
|||||||
.first()
|
.first()
|
||||||
.expect("at least one listener required");
|
.expect("at least one listener required");
|
||||||
|
|
||||||
let transport_kind = listener.transport_kind.clone();
|
let (transport_kind, stealth, listen_addr) = match listener {
|
||||||
let stealth = listener.stealth;
|
ListenerConfig::Stream { config } => (
|
||||||
let listen_addr = listener.listen_addr.clone();
|
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())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if matches!(transport_kind, TransportKind::Iroh { .. }) {
|
if matches!(transport_kind, TransportKind::Iroh { .. }) {
|
||||||
if let Some(id) = endpoint_info {
|
if let Some(id) = endpoint_info {
|
||||||
@@ -832,10 +926,15 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn listener_config_tcp_constructor() {
|
fn listener_config_tcp_constructor() {
|
||||||
let lc = ListenerConfig::tcp("0.0.0.0:22");
|
let lc = ListenerConfig::tcp("0.0.0.0:22");
|
||||||
assert_eq!(lc.transport_kind, TransportKind::Tcp);
|
match &lc {
|
||||||
assert_eq!(lc.listen_addr, "0.0.0.0:22");
|
ListenerConfig::Stream { config } => {
|
||||||
assert!(!lc.stealth);
|
assert_eq!(config.transport_kind, TransportKind::Tcp);
|
||||||
assert!(lc.tls_cert.is_none());
|
assert_eq!(config.listen_addr, "0.0.0.0:22");
|
||||||
|
assert!(!config.stealth);
|
||||||
|
assert!(config.tls_cert.is_none());
|
||||||
|
}
|
||||||
|
_ => panic!("expected Stream variant"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -844,47 +943,85 @@ mod tests {
|
|||||||
.tls_cert("/cert.pem")
|
.tls_cert("/cert.pem")
|
||||||
.tls_key("/key.pem")
|
.tls_key("/key.pem")
|
||||||
.stealth(true);
|
.stealth(true);
|
||||||
assert_eq!(lc.transport_kind, TransportKind::Tls { server_name: None });
|
match &lc {
|
||||||
assert_eq!(lc.listen_addr, "0.0.0.0:443");
|
ListenerConfig::Stream { config } => {
|
||||||
assert!(lc.stealth);
|
assert_eq!(
|
||||||
assert_eq!(lc.tls_cert.as_deref(), Some("/cert.pem"));
|
config.transport_kind,
|
||||||
assert_eq!(lc.tls_key.as_deref(), Some("/key.pem"));
|
TransportKind::Tls { server_name: None }
|
||||||
|
);
|
||||||
|
assert_eq!(config.listen_addr, "0.0.0.0:443");
|
||||||
|
assert!(config.stealth);
|
||||||
|
assert_eq!(config.tls_cert.as_deref(), Some("/cert.pem"));
|
||||||
|
assert_eq!(config.tls_key.as_deref(), Some("/key.pem"));
|
||||||
|
}
|
||||||
|
_ => panic!("expected Stream variant"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn listener_config_iroh_constructor() {
|
fn listener_config_iroh_constructor() {
|
||||||
let lc = ListenerConfig::iroh("0.0.0.0:0").iroh_relay("https://relay.example.com");
|
let lc = ListenerConfig::iroh("0.0.0.0:0").iroh_relay("https://relay.example.com");
|
||||||
assert_eq!(
|
match &lc {
|
||||||
lc.transport_kind,
|
ListenerConfig::Stream { config } => {
|
||||||
TransportKind::Iroh {
|
assert_eq!(
|
||||||
endpoint_id: String::new()
|
config.transport_kind,
|
||||||
|
TransportKind::Iroh {
|
||||||
|
endpoint_id: String::new()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
config.iroh_relay.as_deref(),
|
||||||
|
Some("https://relay.example.com")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
_ => panic!("expected Stream variant"),
|
||||||
assert_eq!(lc.iroh_relay.as_deref(), Some("https://relay.example.com"));
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn listener_config_http_constructor() {
|
||||||
|
let lc = ListenerConfig::http("127.0.0.1:8080".parse().unwrap());
|
||||||
|
match &lc {
|
||||||
|
ListenerConfig::Http { config } => {
|
||||||
|
assert_eq!(
|
||||||
|
config.bind_addr,
|
||||||
|
"127.0.0.1:8080".parse::<SocketAddr>().unwrap()
|
||||||
|
);
|
||||||
|
assert!(!config.tls);
|
||||||
|
assert!(!config.stealth);
|
||||||
|
}
|
||||||
|
_ => panic!("expected Http variant"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn listener_config_dns_constructor() {
|
fn listener_config_dns_constructor() {
|
||||||
let lc = ListenerConfig::dns("example.com");
|
let lc = ListenerConfig::dns("127.0.0.1:53".parse().unwrap());
|
||||||
assert_eq!(
|
match &lc {
|
||||||
lc.transport_kind,
|
ListenerConfig::Dns { config } => {
|
||||||
TransportKind::Dns {
|
assert_eq!(
|
||||||
domain: String::new()
|
config.bind_addr,
|
||||||
|
"127.0.0.1:53".parse::<SocketAddr>().unwrap()
|
||||||
|
);
|
||||||
|
assert!(!config.tls);
|
||||||
}
|
}
|
||||||
);
|
_ => panic!("expected Dns variant"),
|
||||||
assert_eq!(lc.listen_addr, "example.com");
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn listener_config_webtransport_constructor() {
|
fn listener_config_webtransport_constructor() {
|
||||||
let lc = ListenerConfig::webtransport("example.com");
|
let lc = ListenerConfig::webtransport("example.com");
|
||||||
assert_eq!(
|
match &lc {
|
||||||
lc.transport_kind,
|
ListenerConfig::Stream { config } => {
|
||||||
TransportKind::WebTransport {
|
assert_eq!(
|
||||||
host: String::new()
|
config.transport_kind,
|
||||||
|
TransportKind::WebTransport { server_name: None }
|
||||||
|
);
|
||||||
|
assert_eq!(config.listen_addr, "example.com");
|
||||||
}
|
}
|
||||||
);
|
_ => panic!("expected Stream variant"),
|
||||||
assert_eq!(lc.listen_addr, "example.com");
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -922,19 +1059,19 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn listener_config_display() {
|
fn listener_config_display() {
|
||||||
let tcp = ListenerConfig::tcp("0.0.0.0:22");
|
let tcp = ListenerConfig::tcp("0.0.0.0:22");
|
||||||
assert_eq!(format!("{}", tcp), "0.0.0.0:22 (tcp)");
|
assert_eq!(format!("{}", tcp), "0.0.0.0:22 (tcp/ssh)");
|
||||||
|
|
||||||
let tls = ListenerConfig::tls("0.0.0.0:443");
|
let tls = ListenerConfig::tls("0.0.0.0:443");
|
||||||
assert_eq!(format!("{}", tls), "0.0.0.0:443 (tls)");
|
assert_eq!(format!("{}", tls), "0.0.0.0:443 (tls/ssh)");
|
||||||
|
|
||||||
let iroh = ListenerConfig::iroh("0.0.0.0:0");
|
let iroh = ListenerConfig::iroh("0.0.0.0:0");
|
||||||
assert_eq!(format!("{}", iroh), "0.0.0.0:0 (iroh)");
|
assert_eq!(format!("{}", iroh), "0.0.0.0:0 (iroh/ssh)");
|
||||||
|
|
||||||
let dns = ListenerConfig::dns("example.com");
|
let http = ListenerConfig::http("0.0.0.0:8080".parse().unwrap());
|
||||||
assert_eq!(format!("{}", dns), "example.com (dns)");
|
assert_eq!(format!("{}", http), "0.0.0.0:8080 (http)");
|
||||||
|
|
||||||
let wt = ListenerConfig::webtransport("example.com");
|
let dns = ListenerConfig::dns("0.0.0.0:53".parse().unwrap());
|
||||||
assert_eq!(format!("{}", wt), "example.com (webtransport)");
|
assert_eq!(format!("{}", dns), "0.0.0.0:53 (dns)");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1011,7 +1148,6 @@ mod tests {
|
|||||||
.listeners(listeners);
|
.listeners(listeners);
|
||||||
let server = Server::new(opts).unwrap();
|
let server = Server::new(opts).unwrap();
|
||||||
assert_eq!(server.listeners.len(), 1);
|
assert_eq!(server.listeners.len(), 1);
|
||||||
assert_eq!(server.listeners[0].transport_kind, TransportKind::Tcp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1020,8 +1156,13 @@ mod tests {
|
|||||||
ServeOptions::new(make_key_source()).authorized_keys(make_authorized_keys_source());
|
ServeOptions::new(make_key_source()).authorized_keys(make_authorized_keys_source());
|
||||||
let server = Server::new(opts).unwrap();
|
let server = Server::new(opts).unwrap();
|
||||||
assert_eq!(server.listeners.len(), 1);
|
assert_eq!(server.listeners.len(), 1);
|
||||||
assert_eq!(server.listeners[0].transport_kind, TransportKind::Tcp);
|
match &server.listeners[0] {
|
||||||
assert_eq!(server.listeners[0].listen_addr, "0.0.0.0:22");
|
ListenerConfig::Stream { config } => {
|
||||||
|
assert_eq!(config.transport_kind, TransportKind::Tcp);
|
||||||
|
assert_eq!(config.listen_addr, "0.0.0.0:22");
|
||||||
|
}
|
||||||
|
_ => panic!("expected Stream variant"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1035,12 +1176,17 @@ mod tests {
|
|||||||
.stealth(true);
|
.stealth(true);
|
||||||
let server = Server::new(opts).unwrap();
|
let server = Server::new(opts).unwrap();
|
||||||
assert_eq!(server.listeners.len(), 1);
|
assert_eq!(server.listeners.len(), 1);
|
||||||
assert_eq!(
|
match &server.listeners[0] {
|
||||||
server.listeners[0].transport_kind,
|
ListenerConfig::Stream { config } => {
|
||||||
TransportKind::Tls { server_name: None }
|
assert_eq!(
|
||||||
);
|
config.transport_kind,
|
||||||
assert!(server.listeners[0].stealth);
|
TransportKind::Tls { server_name: None }
|
||||||
assert_eq!(server.listeners[0].tls_cert.as_deref(), Some("/cert.pem"));
|
);
|
||||||
|
assert!(config.stealth);
|
||||||
|
assert_eq!(config.tls_cert.as_deref(), Some("/cert.pem"));
|
||||||
|
}
|
||||||
|
_ => panic!("expected Stream variant"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1056,11 +1202,6 @@ mod tests {
|
|||||||
.listeners(listeners);
|
.listeners(listeners);
|
||||||
let server = Server::new(opts).unwrap();
|
let server = Server::new(opts).unwrap();
|
||||||
assert_eq!(server.listeners().len(), 2);
|
assert_eq!(server.listeners().len(), 2);
|
||||||
assert_eq!(server.listeners()[0].transport_kind, TransportKind::Tcp);
|
|
||||||
assert_eq!(
|
|
||||||
server.listeners()[1].transport_kind,
|
|
||||||
TransportKind::Tls { server_name: None }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1113,4 +1254,48 @@ mod tests {
|
|||||||
"server should have shut down within timeout"
|
"server should have shut down within timeout"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn http_listener_config_display() {
|
||||||
|
let config = HttpListenerConfig {
|
||||||
|
bind_addr: "127.0.0.1:8080".parse().unwrap(),
|
||||||
|
tls: true,
|
||||||
|
stealth: false,
|
||||||
|
};
|
||||||
|
assert_eq!(config.to_string(), "127.0.0.1:8080 (http)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dns_listener_config_display() {
|
||||||
|
let config = DnsListenerConfig {
|
||||||
|
bind_addr: "0.0.0.0:53".parse().unwrap(),
|
||||||
|
tls: true,
|
||||||
|
};
|
||||||
|
assert_eq!(config.to_string(), "0.0.0.0:53 (dns)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn http_listener_config_serialization() {
|
||||||
|
let config = HttpListenerConfig {
|
||||||
|
bind_addr: "127.0.0.1:8080".parse().unwrap(),
|
||||||
|
tls: true,
|
||||||
|
stealth: false,
|
||||||
|
};
|
||||||
|
let serialized = serde_json::to_string(&config).unwrap();
|
||||||
|
let deserialized: HttpListenerConfig = serde_json::from_str(&serialized).unwrap();
|
||||||
|
assert_eq!(deserialized.bind_addr, config.bind_addr);
|
||||||
|
assert_eq!(deserialized.tls, config.tls);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dns_listener_config_serialization() {
|
||||||
|
let config = DnsListenerConfig {
|
||||||
|
bind_addr: "0.0.0.0:53".parse().unwrap(),
|
||||||
|
tls: true,
|
||||||
|
};
|
||||||
|
let serialized = serde_json::to_string(&config).unwrap();
|
||||||
|
let deserialized: DnsListenerConfig = serde_json::from_str(&serialized).unwrap();
|
||||||
|
assert_eq!(deserialized.bind_addr, config.bind_addr);
|
||||||
|
assert_eq!(deserialized.tls, config.tls);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,8 +91,7 @@ pub enum TransportKind {
|
|||||||
Tcp,
|
Tcp,
|
||||||
Tls { server_name: Option<String> },
|
Tls { server_name: Option<String> },
|
||||||
Iroh { endpoint_id: String },
|
Iroh { endpoint_id: String },
|
||||||
Dns { domain: String },
|
WebTransport { server_name: Option<String> },
|
||||||
WebTransport { host: String },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for TransportKind {
|
impl std::fmt::Display for TransportKind {
|
||||||
@@ -101,7 +100,7 @@ impl std::fmt::Display for TransportKind {
|
|||||||
TransportKind::Tcp => write!(f, "tcp"),
|
TransportKind::Tcp => write!(f, "tcp"),
|
||||||
TransportKind::Tls { .. } => write!(f, "tls"),
|
TransportKind::Tls { .. } => write!(f, "tls"),
|
||||||
TransportKind::Iroh { .. } => write!(f, "iroh"),
|
TransportKind::Iroh { .. } => write!(f, "iroh"),
|
||||||
TransportKind::Dns { .. } => write!(f, "dns"),
|
|
||||||
TransportKind::WebTransport { .. } => write!(f, "webtransport"),
|
TransportKind::WebTransport { .. } => write!(f, "webtransport"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,11 +182,8 @@ mod tests {
|
|||||||
let iroh = TransportKind::Iroh {
|
let iroh = TransportKind::Iroh {
|
||||||
endpoint_id: "abc123".to_string(),
|
endpoint_id: "abc123".to_string(),
|
||||||
};
|
};
|
||||||
let dns = TransportKind::Dns {
|
|
||||||
domain: "example.com".to_string(),
|
|
||||||
};
|
|
||||||
let wt = TransportKind::WebTransport {
|
let wt = TransportKind::WebTransport {
|
||||||
host: "example.com".to_string(),
|
server_name: Some("example.com".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let TransportKind::Tcp = tcp {}
|
if let TransportKind::Tcp = tcp {}
|
||||||
@@ -200,11 +196,8 @@ mod tests {
|
|||||||
if let TransportKind::Iroh { endpoint_id } = iroh {
|
if let TransportKind::Iroh { endpoint_id } = iroh {
|
||||||
assert_eq!(endpoint_id, "abc123");
|
assert_eq!(endpoint_id, "abc123");
|
||||||
}
|
}
|
||||||
if let TransportKind::Dns { domain } = dns {
|
if let TransportKind::WebTransport { server_name } = wt {
|
||||||
assert_eq!(domain, "example.com");
|
assert_eq!(server_name, Some("example.com".to_string()));
|
||||||
}
|
|
||||||
if let TransportKind::WebTransport { host } = wt {
|
|
||||||
assert_eq!(host, "example.com");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -306,7 +306,9 @@ impl russh::server::Handler for NapiServerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let fingerprint = format!("{}", public_key.fingerprint(HashAlg::Sha256));
|
let fingerprint = format!("{}", public_key.fingerprint(HashAlg::Sha256));
|
||||||
let identity = self.identity_provider.resolve_from_fingerprint(&fingerprint);
|
let identity = self
|
||||||
|
.identity_provider
|
||||||
|
.resolve_from_fingerprint(&fingerprint);
|
||||||
|
|
||||||
match identity {
|
match identity {
|
||||||
Some(id) => {
|
Some(id) => {
|
||||||
@@ -339,11 +341,14 @@ impl russh::server::Handler for NapiServerHandler {
|
|||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
let identity = self.authenticated_identity.clone().unwrap_or_else(|| Identity {
|
let identity = self
|
||||||
id: String::new(),
|
.authenticated_identity
|
||||||
scopes: vec![],
|
.clone()
|
||||||
resources: std::collections::HashMap::new(),
|
.unwrap_or_else(|| Identity {
|
||||||
});
|
id: String::new(),
|
||||||
|
scopes: vec![],
|
||||||
|
resources: std::collections::HashMap::new(),
|
||||||
|
});
|
||||||
|
|
||||||
let policy = self.dynamic.load();
|
let policy = self.dynamic.load();
|
||||||
let allowed = policy.forwarding.check(
|
let allowed = policy.forwarding.check(
|
||||||
@@ -664,11 +669,8 @@ impl AlknetServer {
|
|||||||
let new_auth_policy = build_auth_policy_from_napi(&auth)?;
|
let new_auth_policy = build_auth_policy_from_napi(&auth)?;
|
||||||
let new_forwarding = build_forwarding_policy(&forwarding)?;
|
let new_forwarding = build_forwarding_policy(&forwarding)?;
|
||||||
let current = self.reload_handle.dynamic();
|
let current = self.reload_handle.dynamic();
|
||||||
let new_config = DynamicConfig::from_parts(
|
let new_config =
|
||||||
new_auth_policy,
|
DynamicConfig::from_parts(new_auth_policy, new_forwarding, current.rate_limits.clone());
|
||||||
new_forwarding,
|
|
||||||
current.rate_limits.clone(),
|
|
||||||
);
|
|
||||||
self.reload_handle.reload(new_config);
|
self.reload_handle.reload(new_config);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user