pub(crate) mod guard;
pub(crate) mod handshake;
pub(crate) mod intro_queue;
pub(crate) mod routing;
pub(crate) mod staged;
pub(crate) mod tables;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_safe_arithmetic;
use std::collections::{BTreeMap, VecDeque};
use std::net::SocketAddr;
use std::num::NonZeroU64;
use std::time::{Duration, Instant};
use rand_chacha::ChaCha20Rng;
use rand_core::{Rng, SeedableRng};
use crate::config::Config;
use crate::constants;
use crate::core::{
ConnSeed, Connection, ConnectionId, Deadline, Disposition, EndpointOutput, EstablishedSession,
FlowWindows, Install, Role, Timestamp, ToEndpoint, Transmit,
};
use crate::error::ConnectError;
use crate::identity::{Identity, PublicKeyOf};
use crate::packet::{Handshake, Inbound, Mac1Key, classify};
use self::guard::TimestampGuard;
use self::intro_queue::{Arrival, IntroEntry, IntroQueue};
use self::staged::InitiatorSent;
use self::tables::{IndexTables, StaticEntry, StaticMap, StaticState};
pub use self::staged::IntroId;
struct Pending<I: Identity> {
conn: ConnectionId,
remote: SocketAddr,
remote_static: PublicKeyOf<I>,
remote_static_bytes: Vec<u8>,
peer_mac1: Mac1Key,
sender_index: Option<u32>,
state: Option<Box<InitiatorSent<I>>>,
next_retransmit: Deadline,
give_up_at: Deadline,
attempt_spent: bool,
attempted: bool,
psk: crate::identity::PskOf<I>,
guard_pinned: bool,
}
pub(crate) struct Endpoint<I: Identity> {
config: Config,
identity: I,
our_static_bytes: Vec<u8>,
our_mac1: Mac1Key,
rng: ChaCha20Rng,
outputs: VecDeque<EndpointOutput<I::Suite>>,
intros: IntroQueue<I>,
guard: TimestampGuard,
indices: IndexTables,
statics: StaticMap,
pendings: BTreeMap<ConnectionId, Pending<I>>,
last_init_timestamp: Option<Timestamp>,
next_connection: u64,
}
impl<I: Identity> Endpoint<I> {
pub(crate) fn new(_now: Instant, config: Config, identity: I, rng_seed: [u8; 32]) -> Self {
let our_static_bytes = identity.public_static().as_ref().to_vec();
let our_mac1 = Mac1Key::derive(&our_static_bytes);
let intros = IntroQueue::new(config.intro_queue_cap(), config.intro_max_per_source());
Self {
config,
identity,
our_static_bytes,
our_mac1,
rng: ChaCha20Rng::from_seed(rng_seed),
outputs: VecDeque::new(),
intros,
guard: TimestampGuard::default(),
indices: IndexTables::default(),
statics: StaticMap::default(),
pendings: BTreeMap::new(),
last_init_timestamp: None,
next_connection: 0,
}
}
pub(crate) fn our_static(&self) -> &[u8] {
&self.our_static_bytes
}
pub(crate) fn greatest(&self, peer_static: &[u8]) -> Option<Timestamp> {
self.guard.greatest(peer_static)
}
pub(crate) fn guard_pins(&self, peer_static: &[u8]) -> u32 {
self.guard.pins(peer_static)
}
pub(crate) fn replacement_basis(&self, peer_static: &[u8]) -> Option<Option<Timestamp>> {
self.statics
.get(peer_static)
.map(|entry| entry.replacement_basis)
}
pub(crate) fn hints(&self) -> Vec<SocketAddr> {
let mut hints: Vec<SocketAddr> = self.statics.hints().collect();
hints.sort_unstable();
hints
}
pub(crate) fn poll_output(&mut self) -> EndpointOutput<I::Suite> {
match self.outputs.pop_front() {
Some(output) => output,
None => EndpointOutput::Timeout(self.deadline()),
}
}
pub(crate) fn next_deadline(&self) -> Option<Instant> {
self.deadline()
}
fn deadline(&self) -> Option<Instant> {
let pendings = self
.pendings
.values()
.flat_map(|p| [p.next_retransmit, p.give_up_at])
.filter_map(Deadline::as_instant)
.min();
let intros = self.intros.next_deadline();
let orphans = self.guard.next_orphan_deadline();
[pendings, intros, orphans].into_iter().flatten().min()
}
fn emit(&mut self, output: EndpointOutput<I::Suite>) {
self.outputs.push_back(output);
}
fn mint_conn_seed(&mut self) -> ConnSeed {
let mut sub_seed = [0u8; 32];
self.rng.fill_bytes(&mut sub_seed);
ConnSeed {
sub_seed,
windows: FlowWindows {
stream: self.config.stream_window(),
connection: self.config.connection_window(),
},
timing_profile: self.config.timing_profile(),
}
}
fn draw_retransmit_delay(&mut self) -> Option<Duration> {
let draw = u128::from(self.rng.next_u32());
let span = constants::RETRANSMIT_JITTER_MAX.as_nanos().checked_add(1)?;
let jitter = u64::try_from(draw % span).ok()?;
constants::RETRANSMIT_BASE.checked_add(Duration::from_nanos(jitter))
}
fn next_connection_id(&mut self) -> ConnectionId {
let id = ConnectionId::from_raw(self.next_connection);
self.next_connection += 1;
id
}
fn draw_timestamp(&mut self) -> Timestamp {
let wall = self.config.clock().now();
let forced = match self.last_init_timestamp {
Some(previous) if wall <= previous => previous.succ(),
_ => wall,
};
self.last_init_timestamp = Some(forced);
forced
}
pub(crate) fn mint_pending(
&mut self,
now: Instant,
remote: SocketAddr,
remote_static: PublicKeyOf<I>,
psk: crate::identity::PskOf<I>,
) -> Result<(ConnectionId, Connection<I::Suite>), ConnectError> {
let key = remote_static.as_ref().to_vec();
if self.statics.get(&key).is_some() {
return Err(ConnectError::AlreadyConnected);
}
let conn = self.next_connection_id();
let sub_seed = self.mint_conn_seed();
let peer_mac1 = Mac1Key::derive(&key);
let mut pending = Pending {
conn,
remote,
remote_static,
remote_static_bytes: key.clone(),
peer_mac1,
psk,
sender_index: None,
state: None,
next_retransmit: Deadline::at(now),
give_up_at: Deadline::after(now, constants::HANDSHAKE_GIVEUP),
attempt_spent: false,
attempted: false,
guard_pinned: false,
};
self.statics.insert(
key.clone(),
StaticEntry {
conn,
state: StaticState::Pending,
dialled: Some(remote),
replacement_basis: None,
guard_exempt: false,
},
);
pending.guard_pinned = self.guard.pin(&key, guard::PinKind::KeyHolder);
self.pendings.insert(conn, pending);
Ok((conn, Connection::connecting(sub_seed)))
}
pub(crate) fn start_attempt(&mut self, now: Instant, conn: ConnectionId) {
let Some(mut pending) = self.pendings.remove(&conn) else {
return;
};
self.build_attempt(now, &mut pending);
self.pendings.insert(conn, pending);
}
fn build_attempt(&mut self, now: Instant, pending: &mut Pending<I>) {
if let Some(previous) = pending.sender_index.take() {
self.indices.remove_pending(previous);
}
pending.state = None;
pending.attempt_spent = false;
pending.next_retransmit = match self.draw_retransmit_delay() {
Some(delay) => Deadline::after(now, delay),
None => Deadline::Unreachable,
};
let sender_index = self.indices.mint(&mut self.rng);
let timestamp = self.draw_timestamp();
let (provider, our_key) = match self.identity.open() {
Ok(opened) => opened,
Err(error) => {
tracing::warn!(
target: "slither::io",
verb = "connect",
stage = "Identity::open",
conn = ?pending.conn,
%error,
"the identity provider failed to open"
);
return;
}
};
let state = <I::Suite as Handshake>::initiator(
provider,
constants::PROLOGUE,
pending.remote_static.clone(),
);
let (msg1, sent) = match <I::Suite as Handshake>::write_msg1(
state,
our_key,
&pending.psk,
×tamp.encode(),
) {
Ok(written) => written,
Err(error) => {
tracing::warn!(
target: "slither::io",
verb = "connect",
stage = "Handshake::write_msg1",
conn = ?pending.conn,
%error,
"msg1 would not write on our static"
);
return;
}
};
let data = handshake::frame_init(sender_index, &msg1, &pending.peer_mac1);
pending.attempted = true;
self.indices.insert_pending(sender_index, pending.conn);
pending.sender_index = Some(sender_index);
pending.state = Some(Box::new(sent));
self.emit(EndpointOutput::Transmit(Transmit {
to: pending.remote,
data,
}));
}
fn drop_pending(&mut self, now: Instant, conn: ConnectionId) -> Option<Pending<I>> {
let pending = self.pendings.remove(&conn)?;
if let Some(index) = pending.sender_index {
self.indices.remove_pending(index);
}
let exempt = self
.statics
.remove(&pending.remote_static_bytes)
.is_some_and(|entry| entry.guard_exempt);
if exempt {
self.extend_guard_exemption(now, &pending.remote_static_bytes);
}
if pending.guard_pinned {
self.guard
.unpin(&pending.remote_static_bytes, guard::PinKind::KeyHolder, now);
}
Some(pending)
}
pub(crate) fn handle_datagram(
&mut self,
now: Instant,
src: SocketAddr,
datagram: &[u8],
) -> Disposition {
let Some(inbound) = classify::<I::Suite>(datagram) else {
return Disposition::Done;
};
match inbound {
Inbound::Init {
header,
msg1,
preimage,
mac1,
} => {
if !self.our_mac1.verify(preimage, mac1) {
return Disposition::Done;
}
self.route_initiation(now, src, header.sender_index, msg1);
Disposition::Done
}
Inbound::Resp {
header,
msg2,
preimage,
mac1,
} => {
self.complete_initiation(
header.receiver_index,
header.sender_index,
msg2,
preimage,
mac1,
);
Disposition::Done
}
Inbound::Data { header, .. } => match self.indices.session(header.receiver_index) {
Some(conn) => Disposition::ForConnection(conn),
None => Disposition::Done,
},
}
}
fn park_initiation(&mut self, now: Instant, src: SocketAddr, sender_index: u32, msg1: &[u8]) {
let outcome = self.intros.arrive(now, src, sender_index, msg1);
if let Some(evicted) = outcome.evicted {
self.release_evicted_chain(now, src, evicted);
}
match outcome.arrival {
Arrival::Parked(id) => self.emit(EndpointOutput::IntroReady(id, src)),
Arrival::Refreshed(_) | Arrival::Dropped => {}
}
}
fn complete_initiation(
&mut self,
receiver_index: u32,
peer_index: u32,
msg2: &[u8],
preimage: &[u8],
mac1: &[u8],
) {
let Some(conn) = self.indices.pending(receiver_index) else {
return;
};
if !self.our_mac1.verify(preimage, mac1) {
return;
}
let (state, our_index, anchor, key) = {
let Some(pending) = self.pendings.get_mut(&conn) else {
return;
};
if pending.attempt_spent || pending.sender_index != Some(receiver_index) {
return;
}
pending.attempt_spent = true;
let Some(state) = pending.state.take() else {
return;
};
(
state,
receiver_index,
pending.remote,
pending.remote_static_bytes.clone(),
)
};
let Ok(transport) = <I::Suite as Handshake>::read_msg2(*state, msg2) else {
return;
};
let (seal, open) = <I::Suite as Handshake>::into_datagram(transport, self.epoch_size());
let session = EstablishedSession {
seal,
open,
our_index,
peer_index,
anchor,
};
self.pendings.remove(&conn);
self.indices.remove_pending(our_index);
self.indices.insert_session(our_index, conn);
self.statics.promote(&key, None);
self.emit(EndpointOutput::ToConnection(
conn,
Install {
session,
role: Role::Initiator,
anchor_from_msg1: false,
},
));
}
fn epoch_size(&self) -> NonZeroU64 {
self.config.epoch_size()
}
pub(crate) fn handle_timeout(&mut self, now: Instant) {
self.expire_pendings(now);
for expired in self.intros.expire(now) {
self.release_chain_guard_state(now, expired.guard_undo, expired.guard_pin);
}
self.guard.age_orphans(now);
self.retransmit_pendings(now);
}
fn expire_pendings(&mut self, now: Instant) {
let due: Vec<ConnectionId> = self
.pendings
.iter()
.filter(|(_, p)| p.give_up_at.is_due(now))
.map(|(id, _)| *id)
.collect();
for conn in due {
let attempted = self.pendings.get(&conn).is_some_and(|p| p.attempted);
let _ = self.drop_pending(now, conn);
let why = if attempted {
ConnectError::TimedOut
} else {
ConnectError::Local
};
self.emit(EndpointOutput::HandshakeFailed(conn, why));
}
}
fn retransmit_pendings(&mut self, now: Instant) {
let due: Vec<ConnectionId> = self
.pendings
.iter()
.filter(|(_, p)| p.next_retransmit.is_due(now))
.map(|(id, _)| *id)
.collect();
for conn in due {
let Some(mut pending) = self.pendings.remove(&conn) else {
continue;
};
self.build_attempt(now, &mut pending);
self.pendings.insert(conn, pending);
}
}
pub(crate) fn handle_connection_event(
&mut self,
now: Instant,
id: ConnectionId,
ev: ToEndpoint,
) {
match ev {
ToEndpoint::Retired { our_index } => {
self.indices.remove_session(our_index);
self.indices.remove_pending(our_index);
if self.drop_pending(now, id).is_none()
&& let Some((key, entry)) = self.statics.remove_by_connection(id)
{
if entry.guard_exempt {
self.extend_guard_exemption(now, &key);
}
self.guard.unpin(&key, guard::PinKind::KeyHolder, now);
}
}
}
}
fn release_chain_guard_state(
&mut self,
now: Instant,
undo: Option<guard::GuardUndo>,
pin: Option<guard::ChainPin>,
) {
if let Some(pin) = pin {
self.guard.unpin(&pin.key, pin.kind, now);
}
if let Some(undo) = undo {
self.guard.revert(undo);
}
}
fn release_evicted_chain(
&mut self,
now: Instant,
arriving: SocketAddr,
evicted: IntroEntry<I>,
) {
tracing::debug!(
target: "slither::policy",
event = "intro_evicted",
id = ?evicted.id,
evicted = %evicted.src,
%arriving,
"an intro-queue cap displaced a parked introduction before its TTL"
);
self.release_chain_guard_state(now, evicted.guard_undo, evicted.guard_pin);
}
}