use std::io::{self, IoSliceMut};
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use bytes::Bytes;
use iroh::PublicKey;
use iroh::endpoint::transports::{CustomEndpoint, CustomSender, CustomTransport, RecvInfo, Transmit};
use iroh_base::CustomAddr;
use noq_udp::RecvMeta;
use parking_lot::RwLock;
use tokio::sync::mpsc as tokio_mpsc;
use tokio_rustls::TlsConnector;
use tokio_util::sync::CancellationToken;
use tracing::warn;
use super::certs::RelayCerts;
use super::manager::start_peer_dial;
use super::protocol::Message;
pub const BEP_TRANSPORT_ID: u64 = 0x5357_5342;
pub const RELAY_ID_LEN: usize = 32;
const ADDR_VERSION: u8 = 1;
pub const ADDR_LEN: usize = 1 + 32 + RELAY_ID_LEN;
const NODE_LEN: usize = 32;
const RELAY_START: usize = 1 + NODE_LEN;
const RELAY_LEN: usize = 32;
#[derive(Debug)]
pub struct IncomingPacket {
pub remote: CustomAddr,
pub local: CustomAddr,
pub data: Bytes,
}
#[derive(Debug)]
pub struct Session {
pub peer: [u8; 32],
pub peer_node: [u8; 32],
pub queue: tokio_mpsc::Sender<Bytes>,
pub cancel: CancellationToken,
}
pub struct Backend {
pub sessions: RwLock<std::collections::HashMap<[u8; 32], Arc<Session>>>,
pub node_ids: RwLock<std::collections::HashMap<[u8; 32], [u8; 32]>>,
pub ingress: parking_lot::Mutex<Option<tokio_mpsc::UnboundedSender<IncomingPacket>>>,
pub local_addrs: n0_watcher::Watchable<Vec<CustomAddr>>,
pub dials: parking_lot::Mutex<std::collections::HashSet<[u8; 32]>>,
pub relay_index: parking_lot::Mutex<usize>,
pub relays: RwLock<Vec<String>>,
pub control: RwLock<std::collections::HashMap<String, tokio_mpsc::UnboundedSender<Message>>>,
pub certs: Arc<RelayCerts>,
pub connector: TlsConnector,
pub timeout: Duration,
pub cancel: CancellationToken,
}
impl std::fmt::Debug for Backend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Backend").finish_non_exhaustive()
}
}
impl Backend {
#[must_use]
pub(crate) fn new(certs: Arc<RelayCerts>, connector: TlsConnector, timeout: Duration) -> Self {
Self {
sessions: RwLock::new(std::collections::HashMap::new()),
node_ids: RwLock::new(std::collections::HashMap::new()),
ingress: parking_lot::Mutex::new(None),
local_addrs: n0_watcher::Watchable::new(Vec::new()),
dials: parking_lot::Mutex::new(std::collections::HashSet::new()),
relay_index: parking_lot::Mutex::new(0),
relays: RwLock::new(Vec::new()),
control: RwLock::new(std::collections::HashMap::new()),
certs,
connector,
timeout,
cancel: CancellationToken::new(),
}
}
#[must_use]
pub(crate) fn local_addr(&self) -> CustomAddr {
self.local_addrs
.get()
.first()
.cloned()
.unwrap_or_else(|| CustomAddr::from_parts(BEP_TRANSPORT_ID, &[0_u8; ADDR_LEN]))
}
pub(crate) fn update_local_addr(&self, addr: CustomAddr) {
let _ = self.local_addrs.set(vec![addr]);
}
pub(crate) fn remember_node(&self, relay_id: [u8; 32], node: [u8; 32]) {
self.node_ids.write().insert(relay_id, node);
}
#[must_use]
pub(crate) fn next_relay(&self) -> Option<String> {
if self.relays.read().is_empty() {
return None;
}
let count = self.relays.read().len();
let slot = {
let mut index = self.relay_index.lock();
let slot = *index;
*index = if slot.saturating_add(1) >= count {
0
} else {
slot.saturating_add(1)
};
slot
};
self.relays.read().get(slot).cloned()
}
}
#[derive(Clone, Debug)]
pub struct SyncthingRelayTransport(Arc<Backend>);
impl SyncthingRelayTransport {
#[must_use]
pub(crate) const fn new(backend: Arc<Backend>) -> Self {
Self(backend)
}
#[must_use]
pub fn local_addr(&self) -> CustomAddr {
self.0.local_addr()
}
pub fn remember_node(&self, relay_id: &[u8; 32], node: PublicKey) {
self.0.remember_node(*relay_id, *node.as_bytes());
}
}
impl CustomTransport for SyncthingRelayTransport {
fn bind(&self) -> io::Result<Box<dyn CustomEndpoint>> {
let (tx, rx) = tokio_mpsc::unbounded_channel();
*self.0.ingress.lock() = Some(tx);
Ok(Box::new(RelayEndpoint {
backend: self.0.clone(),
recv: rx,
}))
}
}
#[derive(Debug)]
struct RelayEndpoint {
backend: Arc<Backend>,
recv: tokio_mpsc::UnboundedReceiver<IncomingPacket>,
}
impl CustomEndpoint for RelayEndpoint {
fn watch_local_addrs(&self) -> n0_watcher::Direct<Vec<CustomAddr>> {
self.backend.local_addrs.watch()
}
fn create_sender(&self) -> Arc<dyn CustomSender> {
Arc::new(RelaySender(self.backend.clone()))
}
fn poll_recv(
&mut self,
cx: &mut Context<'_>,
bufs: &mut [IoSliceMut<'_>],
metas: &mut [RecvMeta],
recv_infos: &mut [RecvInfo],
) -> Poll<io::Result<usize>> {
let mut count = 0;
while count < bufs.len() {
match self.recv.poll_recv(cx) {
Poll::Pending | Poll::Ready(None) => break,
Poll::Ready(Some(packet)) => {
let len = packet.data.len();
let Some(buffer) = bufs.get_mut(count) else {
break;
};
let Some(dest) = buffer.get_mut(..len) else {
warn!(
bytes = len,
capacity = buffer.len(),
"datagram larger than consumer buffer, dropping"
);
continue;
};
dest.copy_from_slice(&packet.data);
let Some(meta) = metas.get_mut(count) else {
break;
};
*meta = RecvMeta::default();
meta.len = len;
meta.stride = len;
let Some(recv_info) = recv_infos.get_mut(count) else {
break;
};
*recv_info = RecvInfo::new(packet.remote, Some(packet.local));
count = count.saturating_add(1);
}
}
}
if count == 0 {
return Poll::Pending;
}
Poll::Ready(Ok(count))
}
}
#[derive(Debug)]
struct RelaySender(Arc<Backend>);
impl CustomSender for RelaySender {
fn is_valid_send_addr(&self, addr: &CustomAddr) -> bool {
decode_peer_addr(addr).is_some()
}
fn poll_send(
&self,
_cx: &mut Context<'_>,
dst: &CustomAddr,
_src: Option<&CustomAddr>,
transmit: &Transmit<'_>,
) -> Poll<io::Result<()>> {
let Some((peer, node)) = decode_peer_addr(dst) else {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"custom address is not a syncweb relay address",
)));
};
let candidate = self.0.sessions.read().get(&peer).cloned();
if let Some(session) = candidate {
if session
.queue
.try_send(Bytes::copy_from_slice(transmit.contents))
.is_err()
{
start_peer_dial(self.0.clone(), peer, node);
}
return Poll::Ready(Ok(()));
}
start_peer_dial(self.0.clone(), peer, node);
Poll::Ready(Ok(()))
}
}
#[must_use]
pub fn encode_peer_addr(node: &PublicKey, relay_id: &[u8; 32]) -> Vec<u8> {
let mut payload = Vec::with_capacity(ADDR_LEN);
payload.push(ADDR_VERSION);
payload.extend_from_slice(node.as_bytes());
payload.extend_from_slice(relay_id);
payload
}
#[must_use]
pub fn own_addr(node: &PublicKey, relay_id: &[u8; 32]) -> CustomAddr {
CustomAddr::from_parts(BEP_TRANSPORT_ID, &encode_peer_addr(node, relay_id))
}
#[must_use]
pub fn decode_peer_addr(addr: &CustomAddr) -> Option<([u8; 32], [u8; 32])> {
if addr.id() != BEP_TRANSPORT_ID {
return None;
}
let data = addr.data();
if data.len() != ADDR_LEN || data.first() != Some(&ADDR_VERSION) {
return None;
}
let mut node = [0_u8; NODE_LEN];
node.copy_from_slice(data.get(1..=NODE_LEN)?);
let mut relay = [0_u8; RELAY_LEN];
relay.copy_from_slice(data.get(RELAY_START..)?);
Some((relay, node))
}
#[must_use]
pub fn peer_addr(relay: [u8; 32], node: [u8; 32]) -> CustomAddr {
let mut payload = Vec::with_capacity(ADDR_LEN);
payload.push(ADDR_VERSION);
payload.extend_from_slice(&node);
payload.extend_from_slice(&relay);
CustomAddr::from_parts(BEP_TRANSPORT_ID, &payload)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn peer_addr_round_trips() {
let node_id = iroh::SecretKey::generate().public();
let node = *node_id.as_bytes();
let relay = [9_u8; 32];
let payload = encode_peer_addr(&node_id, &relay);
let addr = CustomAddr::from((BEP_TRANSPORT_ID, payload.as_ref()));
let (decoded_relay, decoded_node) = decode_peer_addr(&addr).expect("decode");
assert_eq!(decoded_relay, relay);
assert_eq!(decoded_node, node);
}
#[test]
fn foreign_addrs_are_rejected() {
let addr = CustomAddr::from((42, b"junk".as_slice()));
assert!(decode_peer_addr(&addr).is_none());
}
#[test]
fn short_payloads_are_rejected() {
let addr = CustomAddr::from((BEP_TRANSPORT_ID, b"1".as_slice()));
assert!(decode_peer_addr(&addr).is_none());
}
}