use std::{
collections::hash_map::Entry,
net::{IpAddr, SocketAddr},
};
use rustc_hash::{FxHashMap, FxHashSet};
use tracing::trace;
use crate::{
PathId, Side, VarInt,
frame::{AddAddress, ReachOut, RemoveAddress},
};
type IpPort = (IpAddr, u16);
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Tried to add too many addresses to their advertised set")]
TooManyAddresses,
#[error("Not allowed for this endpoint's connection side")]
WrongConnectionSide,
#[error("Iroh's nat traversal was not negotiated")]
ExtensionNotNegotiated,
#[error("Not enough addresses")]
NotEnoughAddresses,
#[error("Failed to establish paths {0}")]
Multipath(super::PathError),
#[error("The connection is already closed")]
Closed,
}
pub(crate) struct NatTraversalRound {
pub(crate) new_round: VarInt,
pub(crate) reach_out_at: Vec<IpPort>,
pub(crate) addresses_to_probe: Vec<(VarInt, IpPort)>,
pub(crate) prev_round_path_ids: Vec<PathId>,
}
#[derive(Debug, Clone)]
pub enum Event {
AddressAdded(SocketAddr),
AddressRemoved(SocketAddr),
}
#[derive(Debug, Default)]
pub(crate) enum State {
#[default]
NotNegotiated,
ClientSide(ClientState),
ServerSide(ServerState),
}
#[derive(Debug)]
pub(crate) struct ClientState {
max_remote_addresses: usize,
max_local_addresses: usize,
remote_addresses: FxHashMap<VarInt, (IpPort, bool)>,
local_addresses: FxHashSet<IpPort>,
round: VarInt,
round_path_ids: Vec<PathId>,
}
impl ClientState {
fn new(max_remote_addresses: usize, max_local_addresses: usize) -> Self {
Self {
max_remote_addresses,
max_local_addresses,
remote_addresses: Default::default(),
local_addresses: Default::default(),
round: Default::default(),
round_path_ids: Default::default(),
}
}
fn add_local_address(&mut self, address: IpPort) -> Result<(), Error> {
if self.local_addresses.len() < self.max_local_addresses {
self.local_addresses.insert(address);
Ok(())
} else if self.local_addresses.contains(&address) {
Ok(())
} else {
Err(Error::TooManyAddresses)
}
}
fn remove_local_address(&mut self, address: &IpPort) {
self.local_addresses.remove(address);
}
pub(crate) fn initiate_nat_traversal_round(
&mut self,
ipv6: bool,
) -> Result<NatTraversalRound, Error> {
if self.local_addresses.is_empty() {
return Err(Error::NotEnoughAddresses);
}
let prev_round_path_ids = std::mem::take(&mut self.round_path_ids);
self.round = self.round.saturating_add(1u8);
let mut addresses_to_probe = Vec::with_capacity(self.remote_addresses.len());
for (id, ((ip, port), report_in_continuation)) in self.remote_addresses.iter_mut() {
*report_in_continuation = false;
if let Some(ip) = map_to_local_socket_family(*ip, ipv6) {
addresses_to_probe.push((*id, (ip, *port)));
} else {
trace!(?ip, "not using IPv6 nat candidate for IPv4 socket");
}
}
Ok(NatTraversalRound {
new_round: self.round,
reach_out_at: self.local_addresses.iter().copied().collect(),
addresses_to_probe,
prev_round_path_ids,
})
}
pub(crate) fn report_in_continuation(&mut self, id: VarInt, e: crate::PathError) {
match e {
crate::PathError::MaxPathIdReached | crate::PathError::RemoteCidsExhausted => {
if let Some((_address, report_in_continuation)) = self.remote_addresses.get_mut(&id)
{
*report_in_continuation = true;
}
}
_ => {}
}
}
pub(crate) fn continue_nat_traversal_round(&mut self, ipv6: bool) -> Option<(VarInt, IpPort)> {
let (id, (address, report_in_continuation)) = self
.remote_addresses
.iter_mut()
.filter(|(_id, (_addr, report))| *report)
.filter_map(|(id, ((ip, port), report))| {
let Some(ip) = map_to_local_socket_family(*ip, ipv6) else {
trace!(?ip, "not using IPv6 nat candidate for IPv4 socket");
return None;
};
Some((*id, ((ip, *port), report)))
})
.next()?;
*report_in_continuation = false;
Some((id, address))
}
pub(crate) fn set_round_path_ids(&mut self, path_ids: Vec<PathId>) {
self.round_path_ids = path_ids;
}
pub(crate) fn add_round_path_id(&mut self, path_id: PathId) {
self.round_path_ids.push(path_id);
}
pub(crate) fn add_remote_address(
&mut self,
add_addr: AddAddress,
) -> Result<Option<SocketAddr>, Error> {
let AddAddress { seq_no, ip, port } = add_addr;
let address = (ip, port);
let allow_new = self.remote_addresses.len() < self.max_remote_addresses;
match self.remote_addresses.entry(seq_no) {
Entry::Occupied(mut occupied_entry) => {
let is_update = occupied_entry.get().0 != address;
if is_update {
occupied_entry.insert((address, false));
}
Ok(is_update.then_some(address.into()))
}
Entry::Vacant(vacant_entry) if allow_new => {
vacant_entry.insert((address, false));
Ok(Some(address.into()))
}
_ => Err(Error::TooManyAddresses),
}
}
pub(crate) fn remove_remote_address(
&mut self,
remove_addr: RemoveAddress,
) -> Option<SocketAddr> {
self.remote_addresses
.remove(&remove_addr.seq_no)
.map(|(address, _report_in_continuation)| address.into())
}
pub(crate) fn check_remote_address(&self, add_addr: &AddAddress) -> bool {
match self.remote_addresses.get(&add_addr.seq_no) {
None => true,
Some((existing, _)) => existing == &add_addr.ip_port(),
}
}
pub(crate) fn get_remote_nat_traversal_addresses(&self) -> Vec<SocketAddr> {
self.remote_addresses
.values()
.map(|(address, _report_in_continuation)| (*address).into())
.collect()
}
}
#[derive(Debug)]
pub(crate) struct ServerState {
max_remote_addresses: usize,
max_local_addresses: usize,
local_addresses: FxHashMap<IpPort, VarInt>,
next_local_addr_id: VarInt,
round: VarInt,
pending_probes: FxHashSet<IpPort>,
}
impl ServerState {
fn new(max_remote_addresses: usize, max_local_addresses: usize) -> Self {
Self {
max_remote_addresses,
max_local_addresses,
local_addresses: Default::default(),
next_local_addr_id: Default::default(),
round: Default::default(),
pending_probes: Default::default(),
}
}
fn add_local_address(&mut self, address: IpPort) -> Result<Option<AddAddress>, Error> {
let allow_new = self.local_addresses.len() < self.max_local_addresses;
match self.local_addresses.entry(address) {
Entry::Occupied(_) => Ok(None),
Entry::Vacant(vacant_entry) if allow_new => {
let id = self.next_local_addr_id;
self.next_local_addr_id = self.next_local_addr_id.saturating_add(1u8);
vacant_entry.insert(id);
Ok(Some(AddAddress::new(address, id)))
}
_ => Err(Error::TooManyAddresses),
}
}
fn remove_local_address(&mut self, address: &IpPort) -> Option<RemoveAddress> {
self.local_addresses.remove(address).map(RemoveAddress::new)
}
pub(crate) fn handle_reach_out(
&mut self,
reach_out: ReachOut,
ipv6: bool,
) -> Result<(), Error> {
let ReachOut { round, ip, port } = reach_out;
if round < self.round {
trace!(current_round=%self.round, "ignoring REACH_OUT for previous round");
return Ok(());
}
let Some(ip) = map_to_local_socket_family(ip, ipv6) else {
trace!("Ignoring IPv6 REACH_OUT frame due to not supporting IPv6 locally");
return Ok(());
};
if round > self.round {
self.round = round;
self.pending_probes.clear();
} else if self.pending_probes.len() >= self.max_remote_addresses {
return Err(Error::TooManyAddresses);
}
self.pending_probes.insert((ip, port));
Ok(())
}
pub(crate) fn next_probe(&mut self) -> Option<ServerProbing<'_>> {
self.pending_probes
.iter()
.next()
.copied()
.map(|remote| ServerProbing {
remote,
pending_probes: &mut self.pending_probes,
})
}
}
pub(crate) struct ServerProbing<'a> {
remote: IpPort,
pending_probes: &'a mut FxHashSet<IpPort>,
}
impl<'a> ServerProbing<'a> {
pub(crate) fn mark_as_sent(self) {
self.pending_probes.remove(&self.remote);
}
pub(crate) fn remote(&self) -> SocketAddr {
self.remote.into()
}
}
impl State {
pub(crate) fn new(max_remote_addresses: u8, max_local_addresses: u8, side: Side) -> Self {
match side {
Side::Client => Self::ClientSide(ClientState::new(
max_remote_addresses.into(),
max_local_addresses.into(),
)),
Side::Server => Self::ServerSide(ServerState::new(
max_remote_addresses.into(),
max_local_addresses.into(),
)),
}
}
pub(crate) fn client_side(&self) -> Result<&ClientState, Error> {
match self {
Self::NotNegotiated => Err(Error::ExtensionNotNegotiated),
Self::ClientSide(client_side) => Ok(client_side),
Self::ServerSide(_) => Err(Error::WrongConnectionSide),
}
}
pub(crate) fn client_side_mut(&mut self) -> Result<&mut ClientState, Error> {
match self {
Self::NotNegotiated => Err(Error::ExtensionNotNegotiated),
Self::ClientSide(client_side) => Ok(client_side),
Self::ServerSide(_) => Err(Error::WrongConnectionSide),
}
}
pub(crate) fn server_side_mut(&mut self) -> Result<&mut ServerState, Error> {
match self {
Self::NotNegotiated => Err(Error::ExtensionNotNegotiated),
Self::ClientSide(_) => Err(Error::WrongConnectionSide),
Self::ServerSide(server_side) => Ok(server_side),
}
}
pub(crate) fn add_local_address(
&mut self,
address: SocketAddr,
) -> Result<Option<AddAddress>, Error> {
let ip_port = IpPort::from((address.ip(), address.port()));
match self {
Self::NotNegotiated => Err(Error::ExtensionNotNegotiated),
Self::ClientSide(client_state) => {
client_state.add_local_address(ip_port)?;
Ok(None)
}
Self::ServerSide(server_state) => server_state.add_local_address(ip_port),
}
}
pub(crate) fn remove_local_address(
&mut self,
address: SocketAddr,
) -> Result<Option<RemoveAddress>, Error> {
let address = IpPort::from((address.ip(), address.port()));
match self {
Self::NotNegotiated => Err(Error::ExtensionNotNegotiated),
Self::ClientSide(client_state) => {
client_state.remove_local_address(&address);
Ok(None)
}
Self::ServerSide(server_state) => Ok(server_state.remove_local_address(&address)),
}
}
pub(crate) fn get_local_nat_traversal_addresses(&self) -> Result<Vec<SocketAddr>, Error> {
match self {
Self::NotNegotiated => Err(Error::ExtensionNotNegotiated),
Self::ClientSide(client_state) => Ok(client_state
.local_addresses
.iter()
.copied()
.map(Into::into)
.collect()),
Self::ServerSide(server_state) => Ok(server_state
.local_addresses
.keys()
.copied()
.map(Into::into)
.collect()),
}
}
}
pub(crate) fn map_to_local_socket_family(address: IpAddr, ipv6: bool) -> Option<IpAddr> {
let ip = match address {
IpAddr::V4(addr) if ipv6 => IpAddr::V6(addr.to_ipv6_mapped()),
IpAddr::V4(_) => address,
IpAddr::V6(_) if ipv6 => address,
IpAddr::V6(addr) => IpAddr::V4(addr.to_ipv4_mapped()?),
};
Some(ip)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_server_state() {
let mut state = ServerState::new(2, 2);
state
.handle_reach_out(
ReachOut {
round: 1u32.into(),
ip: std::net::Ipv4Addr::LOCALHOST.into(),
port: 1,
},
true,
)
.unwrap();
state
.handle_reach_out(
ReachOut {
round: 1u32.into(),
ip: "1.1.1.1".parse().unwrap(), port: 2,
},
true,
)
.unwrap();
dbg!(&state);
assert_eq!(state.pending_probes.len(), 2);
let probe = state.next_probe().unwrap();
probe.mark_as_sent();
let probe = state.next_probe().unwrap();
probe.mark_as_sent();
assert!(state.next_probe().is_none());
assert_eq!(state.pending_probes.len(), 0);
}
#[test]
fn test_map_to_local_socket() {
assert_eq!(
map_to_local_socket_family("1.1.1.1".parse().unwrap(), false),
Some("1.1.1.1".parse().unwrap())
);
assert_eq!(
map_to_local_socket_family("1.1.1.1".parse().unwrap(), true),
Some("::ffff:1.1.1.1".parse().unwrap())
);
assert_eq!(
map_to_local_socket_family("::1".parse().unwrap(), true),
Some("::1".parse().unwrap())
);
assert_eq!(
map_to_local_socket_family("::1".parse().unwrap(), false),
None
);
assert_eq!(
map_to_local_socket_family("::ffff:1.1.1.1".parse().unwrap(), false),
Some("1.1.1.1".parse().unwrap())
)
}
}