use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use alkcall::channels::client::ChannelClient;
use alkcall::channels::operations::ChannelCore;
use alkcall::channels::pump::pump_bidi;
use alkcall::core::auth::AuthContext;
use alkcall::core::types::BiStream;
use alktunnels::params::{Substrate, SubstrateAddr, TunnelForwardedParams};
use alktunnels::{TunnelListener, TunnelSession};
use bytes::Bytes;
use futures::future::BoxFuture;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::sync::oneshot;
use crate::egress::{
BindAccept, BindAcceptError, BindAcceptFactory, BindAcceptFactoryError, BindTargetHandle,
Egress, EgressError, EgressFactory,
};
pub use crate::rfc1928::util::target_addr::TargetAddr;
use crate::wire::MAX_INNER_LEN;
pub const DEFAULT_MAX_TUNNELS: usize = 256;
pub fn substrate_addr_of(addr: &TargetAddr) -> SubstrateAddr {
match addr {
TargetAddr::Ip(sa) => SubstrateAddr::HostPort {
host: sa.ip().to_string(),
port: sa.port(),
},
TargetAddr::Domain(domain, port) => SubstrateAddr::HostPort {
host: domain.clone(),
port: *port,
},
}
}
pub fn key_of(addr: &TargetAddr) -> String {
match substrate_addr_of(addr) {
SubstrateAddr::HostPort { host, port } => format!("{host}:{port}"),
SubstrateAddr::Path { path } => path,
}
}
pub struct SessionOwner<H> {
half: H,
session: TunnelSession,
}
impl<H: AsyncRead + Send + Unpin> AsyncRead for SessionOwner<H> {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
let this = std::pin::Pin::into_inner(self);
std::pin::Pin::new(&mut this.half).poll_read(cx, buf)
}
}
impl<H> SessionOwner<H> {
pub fn new(half: H, session: TunnelSession) -> Self {
Self { half, session }
}
pub fn half(&self) -> &H {
&self.half
}
pub fn half_mut(&mut self) -> &mut H {
&mut self.half
}
pub fn into_parts(self) -> (H, TunnelSession) {
(self.half, self.session)
}
}
pub struct ComposedEgress {
client: Arc<ChannelClient>,
max_tunnels: usize,
entries: HashMap<String, TableEntry>,
order: std::collections::VecDeque<(String, TargetAddr)>,
inbound: tokio::sync::mpsc::Receiver<Inbound>,
inbound_tx: tokio::sync::mpsc::Sender<Inbound>,
live_relays: usize,
ever_opened: bool,
}
#[derive(Debug)]
enum Inbound {
Datagram(String, Bytes),
Ended,
}
struct TableEntry {
_session: TunnelSession,
write: Box<dyn AsyncWrite + Send + Unpin>,
reply_addr: TargetAddr,
}
async fn relay_read(
mut read: impl AsyncRead + Send + Unpin,
tx: tokio::sync::mpsc::Sender<Inbound>,
key: String,
) {
use tokio::io::AsyncReadExt;
let mut reader = alktunnels::wire::DatagramReader::new();
let mut chunk = vec![0u8; 16 * 1024];
loop {
match read.read(&mut chunk).await {
Ok(0) | Err(_) => break,
Ok(n) => {
for dg in reader.feed(&chunk[..n]) {
if tx.send(Inbound::Datagram(key.clone(), dg)).await.is_err() {
return;
}
}
}
}
}
let _ = tx.send(Inbound::Ended).await;
}
fn inner_bytes(addr: &TargetAddr, payload: &[u8]) -> Result<Bytes, EgressError> {
let mut inner = addr
.to_be_bytes()
.map_err(|e| EgressError::General(format!("addr encode: {e}")))?;
inner.extend_from_slice(payload);
if inner.len() > MAX_INNER_LEN {
return Err(EgressError::General(format!(
"reply oversize: {} > {MAX_INNER_LEN}",
inner.len()
)));
}
Ok(Bytes::from(inner))
}
impl ComposedEgress {
pub fn new(client: Arc<ChannelClient>, max_tunnels: usize) -> Self {
let (tx, rx) = tokio::sync::mpsc::channel(64);
Self {
client,
max_tunnels,
entries: HashMap::new(),
order: std::collections::VecDeque::new(),
inbound: rx,
inbound_tx: tx,
live_relays: 0,
ever_opened: false,
}
}
async fn entry_for(&mut self, addr: &TargetAddr) -> Result<&mut TableEntry, EgressError> {
let key = key_of(addr);
if !self.entries.contains_key(&key) {
while self.entries.len() >= self.max_tunnels {
let Some((evict, _)) = self.order.pop_front() else {
break;
};
self.entries.remove(&evict);
}
let target = substrate_addr_of(addr);
let session = TunnelSession::open_direct(self.client.as_ref(), target, Substrate::Udp)
.await
.map_err(|e| EgressError::Unreachable(format!("tunnel open `{key}`: {e}")))?;
let taken = session.take_halves();
tokio::spawn(relay_read(taken.read, self.inbound_tx.clone(), key.clone()));
self.live_relays += 1;
self.ever_opened = true;
self.entries.insert(
key.clone(),
TableEntry {
_session: taken.session,
write: taken.write,
reply_addr: addr.clone(),
},
);
}
self.order.retain(|(k, _)| k != &key);
self.order.push_back((key.clone(), addr.clone()));
self.entries.get_mut(&key).ok_or_else(|| {
EgressError::General(format!(
"tunnel table invariant broken: `{key}` absent after insert"
))
})
}
}
impl Egress for ComposedEgress {
fn send(&mut self, addr: TargetAddr, payload: Bytes) -> BoxFuture<'_, Result<(), EgressError>> {
Box::pin(async move {
let entry = self.entry_for(&addr).await?;
use tokio::io::AsyncWriteExt;
let framed = alktunnels::wire::frame_datagram(&payload)
.map_err(|e| EgressError::General(format!("tunnel frame: {e}")))?;
let write: &mut (dyn AsyncWrite + Send + Unpin) = &mut *entry.write;
write
.write_all(&framed)
.await
.map_err(|e| EgressError::General(format!("tunnel write: {e}")))?;
write
.flush()
.await
.map_err(|e| EgressError::General(format!("tunnel flush: {e}")))?;
Ok(())
})
}
fn next_reply(&mut self) -> BoxFuture<'_, Result<Option<Bytes>, EgressError>> {
Box::pin(async move {
loop {
loop {
match self.inbound.try_recv() {
Ok(Inbound::Datagram(key, dg)) => {
let addr = match self.entries.get(&key) {
Some(entry) => entry.reply_addr.clone(),
None => continue,
};
return Ok(Some(inner_bytes(&addr, &dg)?));
}
Ok(Inbound::Ended) => {
self.live_relays = self.live_relays.saturating_sub(1);
continue;
}
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break,
}
}
if self.ever_opened && self.live_relays == 0 {
return Err(EgressError::General("all tunnel relays ended".into()));
}
match self.inbound.recv().await {
Some(Inbound::Datagram(key, dg)) => {
let addr = match self.entries.get(&key) {
Some(entry) => entry.reply_addr.clone(),
None => continue,
};
return Ok(Some(inner_bytes(&addr, &dg)?));
}
Some(Inbound::Ended) => {
self.live_relays = self.live_relays.saturating_sub(1);
continue;
}
None => {
return Err(EgressError::General("all tunnel relays ended".into()));
}
}
}
})
}
}
pub fn direct_egress(client: Arc<ChannelClient>, max_tunnels: usize) -> EgressFactory {
Arc::new(move || {
let client = Arc::clone(&client);
Box::pin(async move {
Ok(Box::new(ComposedEgress::new(client, max_tunnels)) as Box<dyn Egress + Send>)
})
})
}
pub type CorrelationRegistry =
Arc<tokio::sync::Mutex<HashMap<u32, oneshot::Sender<ForwardedSession>>>>;
pub struct ForwardedSession {
pub read: tokio::io::ReadHalf<tokio::io::DuplexStream>,
pub write: tokio::io::WriteHalf<tokio::io::DuplexStream>,
pub peer: SubstrateAddr,
}
fn make_forwarded_handler(registry: CorrelationRegistry) -> alktunnels::ForwardedHandler {
Arc::new(move |forwarded: TunnelForwardedParams, bidi: BiStream| {
let registry = Arc::clone(®istry);
tracing::debug!(
listen = forwarded.listen,
"composed accept: forwarded open arrived"
);
tokio::spawn(async move {
let tx = registry.lock().await.remove(&forwarded.listen);
let Some(tx) = tx else {
tracing::warn!(
listen = forwarded.listen,
"composed accept: forwarded open with no waiting session; dropping"
);
return;
};
let (near, far) = tokio::io::duplex(64 * 1024);
let (far_read, far_write) = tokio::io::split(far);
if tx
.send(ForwardedSession {
read: far_read,
write: far_write,
peer: forwarded.peer,
})
.is_err()
{
tracing::warn!(
listen = forwarded.listen,
"composed accept: waiting session vanished; dropping the forwarded channel"
);
return;
}
let (near_read, near_write) = tokio::io::split(near);
pump_bidi(bidi, near_read, near_write).await;
})
})
}
struct ForwardedOwner {
_listener: TunnelListener,
}
struct SyncHalves {
inner: std::sync::Mutex<tokio::io::ReadHalf<tokio::io::DuplexStream>>,
}
impl AsyncRead for SyncHalves {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
let this = self.get_mut();
let mut inner = match this.inner.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
std::pin::Pin::new(&mut *inner).poll_read(cx, buf)
}
}
struct OwnedWrite {
inner: std::sync::Mutex<OwnedInner>,
}
struct OwnedInner {
_owner: ForwardedOwner,
write: tokio::io::WriteHalf<tokio::io::DuplexStream>,
}
impl AsyncWrite for OwnedWrite {
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
let this = self.get_mut();
let mut inner = match this.inner.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
std::pin::Pin::new(&mut inner.write).poll_write(cx, buf)
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
let this = self.get_mut();
let mut inner = match this.inner.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
std::pin::Pin::new(&mut inner.write).poll_flush(cx)
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
let this = self.get_mut();
let mut inner = match this.inner.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
std::pin::Pin::new(&mut inner.write).poll_shutdown(cx)
}
}
pub fn register_forwarded_serving(
core: &ChannelCore,
on_registry: &Arc<alkcall::registry::registration::OperationRegistry>,
auth: AuthContext,
) -> Result<CorrelationRegistry, String> {
let registry: CorrelationRegistry = Arc::new(tokio::sync::Mutex::new(HashMap::new()));
alktunnels::producer::register_tunnel_forwarded_openable(
core,
on_registry,
auth,
make_forwarded_handler(Arc::clone(®istry)),
)?;
Ok(registry)
}
pub fn bindfirst_accept(
client: Arc<ChannelClient>,
registry: CorrelationRegistry,
resource: impl Into<String>,
) -> BindAcceptFactory {
let resource: Arc<str> = resource.into().into();
Arc::new(move |_target: TargetAddr| {
let client = Arc::clone(&client);
let registry = Arc::clone(®istry);
let resource = Arc::clone(&resource);
Box::pin(async move {
let params = alktunnels::TunnelParams {
resource: resource.to_string(),
substrate: Substrate::Tcp,
};
let listener = TunnelListener::open(client.as_ref(), params)
.await
.map_err(|e| BindAcceptFactoryError::Refused(format!("listen open: {e}")))?;
let bind_addr = match listener.bound() {
Some(SubstrateAddr::HostPort { host, port }) => resolve_bound(host, *port)
.map_err(|e| BindAcceptFactoryError::General(format!("bound render: {e}")))?,
Some(SubstrateAddr::Path { path }) => {
return Err(BindAcceptFactoryError::NotAllowed(format!(
"non-tcp bound address `{path}`: this provider serves TCP listeners only"
)));
}
None => {
return Err(BindAcceptFactoryError::Shortage(
"the producer supplied no `bound` reply field, so this is not a bind-first \
resource; no stand-in on this variant"
.into(),
));
}
};
let token = listener.listen().ok_or_else(|| {
BindAcceptFactoryError::Shortage(
"the producer supplied no `listen` correlation token, so this is not a \
bind-first resource; no stand-in on this variant"
.into(),
)
})?;
let (tx, rx) = oneshot::channel();
registry.lock().await.insert(token, tx);
tracing::debug!(token, "composed accept: registry entry inserted");
let fut = async move {
let listener = listener;
let ForwardedSession { read, write, peer } = match rx.await {
Ok(session) => session,
Err(_) => {
tracing::warn!(
token,
"composed accept: oneshot sender dropped; listener closing"
);
return Err(BindAcceptError::General(
"the forwarded-op serving half went away; the listener closed".into(),
));
}
};
let peer_addr = peer_to_socket_addr(&peer);
Ok(BindTargetHandle {
peer: TargetAddr::Ip(peer_addr),
read: Box::new(SyncHalves {
inner: std::sync::Mutex::new(read),
}) as Box<dyn AsyncRead + Send + Sync + Unpin>,
write: Box::new(OwnedWrite {
inner: std::sync::Mutex::new(OwnedInner {
_owner: ForwardedOwner {
_listener: listener,
},
write,
}),
}),
})
};
Ok(BindAccept::new(bind_addr, fut))
})
})
}
fn resolve_bound(host: &str, port: u16) -> Result<SocketAddr, String> {
host.parse::<std::net::IpAddr>()
.map(|ip| SocketAddr::new(ip, port))
.map_err(|e| format!("`{host}`: {e}"))
}
fn peer_to_socket_addr(peer: &SubstrateAddr) -> SocketAddr {
match peer {
SubstrateAddr::HostPort { host, port } => host
.parse::<std::net::IpAddr>()
.map(|ip| SocketAddr::new(ip, *port))
.unwrap_or_else(|_| sentinel_peer()),
SubstrateAddr::Path { .. } => sentinel_peer(),
}
}
fn sentinel_peer() -> SocketAddr {
SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), 0)
}
#[cfg(test)]
mod egress_unit_tests {
use super::*;
use crate::rfc1928::util::target_addr::ToTargetAddr;
use tokio::io::AsyncWriteExt;
#[test]
fn inner_bytes_oversize_guard_fires() {
let addr = ("10.0.0.1", 53u16).to_target_addr().unwrap();
let payload = vec![0u8; MAX_INNER_LEN];
let err = inner_bytes(&addr, &payload).expect_err("the joint bound trips");
assert!(
matches!(err, EgressError::General(ref m) if m.contains("oversize")),
"the oversize guard: {err}"
);
let ok = inner_bytes(
&addr,
&payload[..MAX_INNER_LEN - addr.to_be_bytes().unwrap().len()],
);
assert!(ok.is_ok(), "the bound-edge reply passes");
}
#[tokio::test]
async fn relay_read_exits_silently_when_the_queue_is_closed() {
let (tx, rx) = tokio::sync::mpsc::channel::<Inbound>(4);
drop(rx);
let (client_side, mut server_side) = tokio::io::duplex(1024);
server_side
.write_all(&alktunnels::wire::frame_datagram(b"queued").unwrap())
.await
.unwrap();
server_side.shutdown().await.unwrap();
drop(server_side);
let handle = tokio::spawn(relay_read(client_side, tx, "k".into()));
tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("the relay exits (the closed queue ends the pump)")
.expect("no panic");
}
#[tokio::test]
async fn relay_read_reports_ended_on_eof() {
let (tx, mut rx) = tokio::sync::mpsc::channel::<Inbound>(4);
let (client_side, mut server_side) = tokio::io::duplex(1024);
server_side
.write_all(&alktunnels::wire::frame_datagram(b"one").unwrap())
.await
.unwrap();
server_side
.write_all(&alktunnels::wire::frame_datagram(b"two").unwrap())
.await
.unwrap();
server_side.shutdown().await.unwrap();
drop(server_side);
let handle = tokio::spawn(relay_read(client_side, tx, "k".into()));
let first = rx.recv().await.expect("a datagram arrives");
assert!(
matches!(first, Inbound::Datagram(ref key, ref dg) if key == "k" && (&dg[..] == b"one" || &dg[..] == b"two")),
"a queued datagram: {first:?}"
);
let mut saw_end = false;
for _ in 0..3 {
match rx.recv().await.expect("the queue delivers") {
Inbound::Datagram(key, dg) => {
assert_eq!(key, "k");
assert!(
&dg[..] == b"one" || &dg[..] == b"two",
"a queued datagram: {dg:?}"
);
}
Inbound::Ended => {
saw_end = true;
break;
}
}
}
assert!(saw_end, "the EOF exit reports Ended");
tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("the relay exits after the EOF")
.expect("no panic");
}
}