use crate::auth_lys::session::LysSession;
use crate::auth_lys::{AccessDecisionError, HotAction, SubjectBinding};
use crate::link_handshake::MachineIdentity;
use liminal::protocol::{Frame, FrameType, codec, validate_stream};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex, mpsc};
type Device = [u8; 32];
type Pair = (Device, Device);
const STREAM_BOUND: usize = 256;
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RelayConfig {
pub enabled: bool,
}
#[derive(Debug, thiserror::Error)]
pub enum RelayRefusal {
#[error(transparent)]
Decision(#[from] AccessDecisionError),
#[error("{0}")]
Named(&'static str),
}
pub trait RelaySink: Send + Sync + std::fmt::Debug {
fn send(&self, frame: Frame) -> Result<(), RelayRefusal>;
}
impl RelaySink for mpsc::SyncSender<Frame> {
fn send(&self, frame: Frame) -> Result<(), RelayRefusal> {
self.try_send(frame).map_err(|error| match error {
mpsc::TrySendError::Full(_) => RelayRefusal::Named("relay_outbound_full"),
mpsc::TrySendError::Disconnected(_) => RelayRefusal::Named("peer_not_connected"),
})
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
pub struct RelayCounters {
pub frames_relayed: u64,
pub bytes_relayed: u64,
pub refused: u64,
}
#[derive(Debug)]
struct Endpoint {
generation: u64,
sink: Arc<dyn RelaySink>,
streams: BTreeMap<Device, u32>,
}
#[derive(Debug, Default)]
struct State {
generation: u64,
endpoints: BTreeMap<Device, Endpoint>,
pending: BTreeMap<Pair, u32>,
counters: BTreeMap<Pair, RelayCounters>,
}
#[derive(Debug)]
pub struct Relay {
network: String,
recorded: BTreeMap<String, MachineIdentity>,
state: Mutex<State>,
}
impl Relay {
#[must_use]
pub fn new(network: String, recorded: BTreeMap<String, MachineIdentity>) -> Arc<Self> {
Arc::new(Self {
network,
recorded,
state: Mutex::new(State::default()),
})
}
pub fn connect(
self: &Arc<Self>,
session: Arc<dyn LysSession>,
sink: Arc<dyn RelaySink>,
) -> Result<RelayConnection, RelayRefusal> {
let device = *self
.recorded
.get(session.holder())
.ok_or(RelayRefusal::Named("machine_not_recorded"))?
.ed25519_public();
let mut state = self.lock()?;
if state.endpoints.contains_key(&device) {
return Err(RelayRefusal::Named("device_already_connected"));
}
state.generation = state
.generation
.checked_add(1)
.ok_or(RelayRefusal::Named("relay_ids_exhausted"))?;
let generation = state.generation;
state.endpoints.insert(
device,
Endpoint {
generation,
sink,
streams: BTreeMap::new(),
},
);
drop(state);
Ok(RelayConnection {
relay: Arc::clone(self),
device,
generation,
session,
})
}
pub fn counters(
&self,
from: MachineIdentity,
to: MachineIdentity,
) -> Result<RelayCounters, RelayRefusal> {
Ok(self
.lock()?
.counters
.get(&(*from.ed25519_public(), *to.ed25519_public()))
.copied()
.unwrap_or_default())
}
fn lock(&self) -> Result<std::sync::MutexGuard<'_, State>, RelayRefusal> {
self.state
.lock()
.map_err(|_| RelayRefusal::Named("relay_state_poisoned"))
}
}
pub struct RelayConnection {
relay: Arc<Relay>,
device: Device,
generation: u64,
session: Arc<dyn LysSession>,
}
impl std::fmt::Debug for RelayConnection {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("RelayConnection")
.field("generation", &self.generation)
.finish_non_exhaustive()
}
}
impl RelayConnection {
pub fn mark_stream(&self, stream: u32, peer: &str, now: u64) -> Result<(), RelayRefusal> {
validate_stream(FrameType::LinkMessage, stream)
.map_err(|_| RelayRefusal::Named("invalid_relay_stream"))?;
let mut device = [0; 32];
hex::decode_to_slice(peer, &mut device)
.map_err(|_| RelayRefusal::Named("invalid_peer_device"))?;
if hex::encode(device) != peer || device == self.device {
return Err(RelayRefusal::Named("invalid_peer_device"));
}
self.decide(device, now)?;
let mut state = self.relay.lock()?;
let endpoint = self.endpoint(&state)?;
if endpoint
.streams
.iter()
.any(|(to, id)| (*id == stream && *to != device) || (*to == device && *id != stream))
{
return Err(RelayRefusal::Named("relay_stream_in_use"));
}
if !endpoint.streams.contains_key(&device) && endpoint.streams.len() >= STREAM_BOUND {
return Err(RelayRefusal::Named("relay_stream_limit"));
}
state
.endpoints
.get_mut(&self.device)
.ok_or(RelayRefusal::Named("peer_not_connected"))?
.streams
.insert(device, stream);
drop(state);
Ok(())
}
pub fn receive(&self, mut frame: Frame, now: u64) -> Result<(), RelayRefusal> {
let stream = frame.stream_id();
let mut state = self.relay.lock()?;
let endpoint = self.endpoint(&state)?;
let peer = *endpoint
.streams
.iter()
.find(|(_, id)| **id == stream)
.map(|(peer, _)| peer)
.ok_or(RelayRefusal::Named("relay_stream_not_marked"))?;
let pair = (self.device, peer);
if matches!(frame, Frame::LinkMessage { .. }) {
if let Err(error) = self.decide(peer, now) {
return refuse(&mut state, pair, stream, &error.to_string());
}
let Some(target) = state.endpoints.get(&peer) else {
return refuse(&mut state, pair, stream, "peer_not_connected");
};
let Some(&target_stream) = target.streams.get(&self.device) else {
return refuse(&mut state, pair, stream, "peer_relay_stream_not_marked");
};
if state.pending.contains_key(&pair) {
return refuse(&mut state, pair, stream, "relay_in_flight");
}
remap(&mut frame, target_stream)?;
let bytes = codec::encoded_len(&frame)
.map_err(|_| RelayRefusal::Named("invalid_relay_frame"))?;
if bytes > crate::link_carrier::WIRE_FRAME_BOUND {
return refuse(&mut state, pair, stream, "relay_frame_too_large");
}
let previous = state.counters.get(&pair).copied().unwrap_or_default();
let frames_relayed = increment(previous.frames_relayed)?;
let bytes_relayed = previous
.bytes_relayed
.checked_add(
u64::try_from(bytes)
.map_err(|_| RelayRefusal::Named("relay_counter_overflow"))?,
)
.ok_or(RelayRefusal::Named("relay_counter_overflow"))?;
if let Err(error) = target.sink.send(frame) {
return refuse(&mut state, pair, stream, &error.to_string());
}
state.counters.insert(
pair,
RelayCounters {
frames_relayed,
bytes_relayed,
..previous
},
);
state.pending.insert(pair, stream);
drop(state);
return Ok(());
}
let original = (peer, self.device);
let sender_stream = *state
.pending
.get(&original)
.ok_or(RelayRefusal::Named("unexpected_relay_reply"))?;
let refused = matches!(frame, Frame::PublishError { .. });
remap(&mut frame, sender_stream)?;
let count = if refused {
Some(increment(
state.counters.entry(original).or_default().refused,
)?)
} else {
None
};
state
.endpoints
.get(&peer)
.ok_or(RelayRefusal::Named("peer_not_connected"))?
.sink
.send(frame)?;
state.pending.remove(&original);
if let Some(count) = count {
state.counters.entry(original).or_default().refused = count;
}
drop(state);
Ok(())
}
pub fn disconnect(&self) -> Result<(), RelayRefusal> {
let mut state = self.relay.lock()?;
if state
.endpoints
.get(&self.device)
.is_none_or(|endpoint| endpoint.generation != self.generation)
{
return Ok(());
}
state.endpoints.remove(&self.device);
let affected: Vec<_> = state
.pending
.iter()
.filter(|((from, to), _)| *from == self.device || *to == self.device)
.map(|(pair, stream)| (*pair, *stream))
.collect();
let mut failed = None;
for (pair, stream) in affected {
state.pending.remove(&pair);
if pair.1 == self.device
&& let Err(error) = refuse(&mut state, pair, stream, "peer_not_connected")
{
tracing::error!(%error, "relay disconnect notification refused");
failed = Some(error);
}
}
drop(state);
failed.map_or(Ok(()), Err)
}
fn endpoint<'a>(&self, state: &'a State) -> Result<&'a Endpoint, RelayRefusal> {
state
.endpoints
.get(&self.device)
.filter(|endpoint| endpoint.generation == self.generation)
.ok_or(RelayRefusal::Named("peer_not_connected"))
}
fn decide(&self, peer: Device, now: u64) -> Result<(), RelayRefusal> {
let subject = format!("net.relay.{}", hex::encode(peer));
let binding = SubjectBinding::new(self.relay.network.clone(), subject.clone(), subject)
.map_err(|_| RelayRefusal::Named("invalid_relay_subject"))?;
self.session.decide(&binding, HotAction::Call, now)?;
Ok(())
}
}
impl Drop for RelayConnection {
fn drop(&mut self) {
if let Err(error) = self.disconnect() {
tracing::error!(%error, "relay disconnect refused");
}
}
}
fn increment(value: u64) -> Result<u64, RelayRefusal> {
value
.checked_add(1)
.ok_or(RelayRefusal::Named("relay_counter_overflow"))
}
fn refuse(state: &mut State, pair: Pair, stream: u32, reason: &str) -> Result<(), RelayRefusal> {
let counters = state.counters.entry(pair).or_default();
counters.refused = increment(counters.refused)?;
state
.endpoints
.get(&pair.0)
.ok_or(RelayRefusal::Named("peer_not_connected"))?
.sink
.send(Frame::PublishError {
flags: 0,
stream_id: stream,
reason_code: 0x0302,
message: Some(reason.to_owned()),
})
}
const fn remap(frame: &mut Frame, stream: u32) -> Result<(), RelayRefusal> {
match frame {
Frame::LinkMessage { stream_id, .. }
| Frame::PublishAck { stream_id, .. }
| Frame::PublishError { stream_id, .. } => {
*stream_id = stream;
Ok(())
}
_ => Err(RelayRefusal::Named("invalid_relay_frame")),
}
}