use crate::config::{EthernetConfig, NostrDiscoveryPolicy, TransportInstances, UdpConfig};
#[cfg(test)]
use crate::node::ENDPOINT_EVENT_TEST_PAYLOAD_LEN;
use crate::node::{
EndpointDataBatchTx, EndpointDataPayload, EndpointDirectSink, EndpointEventSender,
NodeEndpointControlCommand, NodeEndpointDataBatch, NodeEndpointEvent,
};
use crate::upper::tun::TunOutboundTx;
use crate::{
Config, FipsAddress, IdentityConfig, Node, NodeAddr, NodeDeliveredPacket, NodeError,
PeerIdentity,
};
use std::sync::{Arc, Mutex as StdMutex};
use thiserror::Error;
use tokio::sync::{Mutex, mpsc, oneshot};
use tokio::task::JoinHandle;
const ENDPOINT_DATA_BATCH_MAX: usize = 128;
const ENDPOINT_RECV_BATCH_MAX: usize = 128;
mod builder;
mod receive;
mod status;
#[cfg(test)]
mod tests;
pub use crate::node::{
FipsEndpointDirectDeliveryError, FipsEndpointDirectPacketBatch, FipsEndpointDirectPacketRun,
FipsEndpointDirectSink,
};
pub use builder::FipsEndpointBuilder;
use receive::EndpointReceiveState;
pub use status::{FipsEndpointPeer, FipsEndpointRelayStatus};
pub type FipsEndpointData = crate::transport::PacketBuffer;
#[derive(Debug, Error)]
pub enum FipsEndpointError {
#[error("node error: {0}")]
Node(#[from] NodeError),
#[error("endpoint task failed: {0}")]
TaskJoin(#[from] tokio::task::JoinError),
#[error("endpoint is closed")]
Closed,
#[error("endpoint data payload is too large: {len} bytes exceeds max {max} bytes")]
EndpointDataTooLarge { len: usize, max: usize },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FipsEndpointMessage {
pub source_peer: PeerIdentity,
pub data: FipsEndpointData,
pub enqueued_at_ms: u64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct UpdatePeersOutcome {
pub added: usize,
pub removed: usize,
pub updated: usize,
pub unchanged: usize,
}
impl From<crate::node::UpdatePeersOutcome> for UpdatePeersOutcome {
fn from(value: crate::node::UpdatePeersOutcome) -> Self {
Self {
added: value.added,
removed: value.removed,
updated: value.updated,
unchanged: value.unchanged,
}
}
}
fn apply_default_scoped_discovery(config: &mut Config, scope: &str) {
if config.node.discovery.nostr.enabled || !config.transports.is_empty() {
return;
}
config.node.discovery.nostr.enabled = true;
config.node.discovery.nostr.advertise = true;
config.node.discovery.nostr.policy = NostrDiscoveryPolicy::Open;
config.node.discovery.nostr.share_local_candidates = true;
config.node.discovery.nostr.app = scope.to_string();
config.node.discovery.lan.scope = Some(scope.to_string());
config.node.discovery.local.enabled = true;
config.transports.udp = TransportInstances::Single(UdpConfig {
bind_addr: Some("0.0.0.0:0".to_string()),
advertise_on_nostr: Some(true),
public: Some(false),
outbound_only: Some(false),
accept_connections: Some(true),
..UdpConfig::default()
});
}
fn endpoint_ethernet_config(interface: &str, scope: Option<&str>) -> EthernetConfig {
EthernetConfig {
interface: interface.to_string(),
discovery: Some(true),
announce: Some(true),
auto_connect: Some(true),
accept_connections: Some(true),
discovery_scope: scope
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string),
..EthernetConfig::default()
}
}
fn add_endpoint_ethernet_transport(config: &mut Config, interface: &str, scope: Option<&str>) {
let eth = endpoint_ethernet_config(interface, scope);
if config.transports.ethernet.is_empty() {
config.transports.ethernet = TransportInstances::Single(eth);
return;
}
let existing = std::mem::take(&mut config.transports.ethernet);
let mut named = match existing {
TransportInstances::Single(config) => {
let mut map = std::collections::HashMap::new();
map.insert("default".to_string(), config);
map
}
TransportInstances::Named(map) => map,
};
let base_name = endpoint_ethernet_instance_name(interface);
let mut name = base_name.clone();
let mut suffix = 2usize;
while named.contains_key(&name) {
name = format!("{base_name}-{suffix}");
suffix += 1;
}
named.insert(name, eth);
config.transports.ethernet = TransportInstances::Named(named);
}
fn endpoint_ethernet_instance_name(interface: &str) -> String {
let suffix: String = interface
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() {
c.to_ascii_lowercase()
} else {
'-'
}
})
.collect();
let suffix = suffix.trim_matches('-');
if suffix.is_empty() {
"local-ethernet".to_string()
} else {
format!("local-ethernet-{suffix}")
}
}
fn endpoint_data_payloads_from_vecs(
payloads: Vec<Vec<u8>>,
) -> Result<Vec<EndpointDataPayload>, FipsEndpointError> {
let mut converted = Vec::with_capacity(payloads.len());
for payload in payloads {
let len = payload.len();
let Some(payload) = EndpointDataPayload::from_packet_payload(payload) else {
let max = crate::node::session_wire::fsp_endpoint_data_max_body_len();
return Err(FipsEndpointError::EndpointDataTooLarge { len, max });
};
converted.push(payload);
}
Ok(converted)
}
fn spawn_node_task(
mut node: Node,
shutdown_rx: oneshot::Receiver<()>,
) -> JoinHandle<Result<(), NodeError>> {
tokio::spawn(async move {
tokio::pin!(shutdown_rx);
let loop_result = tokio::select! {
result = node.run_rx_loop() => result,
_ = &mut shutdown_rx => Ok(()),
};
let stop_result = if node.state().can_stop() {
node.stop().await
} else {
Ok(())
};
loop_result?;
stop_result
})
}
pub struct FipsEndpoint {
identity: PeerIdentity,
npub: String,
node_addr: NodeAddr,
address: FipsAddress,
discovery_scope: Option<String>,
outbound_packets: TunOutboundTx,
delivered_packets: Arc<Mutex<mpsc::Receiver<NodeDeliveredPacket>>>,
endpoint_control_tx: mpsc::Sender<NodeEndpointControlCommand>,
endpoint_data_batches: EndpointDataBatchTx,
inbound_endpoint_tx: EndpointEventSender,
inbound_endpoint_rx: Arc<Mutex<EndpointReceiveState>>,
shutdown_tx: StdMutex<Option<oneshot::Sender<()>>>,
task: StdMutex<Option<JoinHandle<Result<(), NodeError>>>>,
}
impl FipsEndpoint {
pub fn builder() -> FipsEndpointBuilder {
FipsEndpointBuilder::default()
}
pub fn npub(&self) -> &str {
&self.npub
}
pub fn node_addr(&self) -> &NodeAddr {
&self.node_addr
}
pub fn address(&self) -> FipsAddress {
self.address
}
pub fn discovery_scope(&self) -> Option<&str> {
self.discovery_scope.as_deref()
}
pub async fn send_batch_to_peer(
&self,
remote: PeerIdentity,
payloads: Vec<Vec<u8>>,
) -> Result<(), FipsEndpointError> {
self.send_payloads_to_peer(remote, payloads)
}
fn send_payloads_to_peer(
&self,
remote: PeerIdentity,
payloads: Vec<Vec<u8>>,
) -> Result<(), FipsEndpointError> {
let payloads = endpoint_data_payloads_from_vecs(payloads)?;
if *remote.node_addr() == self.node_addr {
for payload in payloads {
self.send_loopback(payload)?;
}
return Ok(());
}
self.send_endpoint_data_batch(remote, payloads)
}
fn send_endpoint_data_batch(
&self,
remote: PeerIdentity,
payloads: Vec<EndpointDataPayload>,
) -> Result<(), FipsEndpointError> {
if payloads.is_empty() {
return Ok(());
}
if payloads.len() <= ENDPOINT_DATA_BATCH_MAX {
self.enqueue_endpoint_data_batch(remote, payloads)?;
return Ok(());
}
let mut payloads = payloads.into_iter();
loop {
let payload_batch: Vec<_> = payloads.by_ref().take(ENDPOINT_DATA_BATCH_MAX).collect();
if payload_batch.is_empty() {
break;
}
self.enqueue_endpoint_data_batch(remote, payload_batch)?;
}
Ok(())
}
fn enqueue_endpoint_data_batch(
&self,
remote: PeerIdentity,
payload_batch: Vec<EndpointDataPayload>,
) -> Result<(), FipsEndpointError> {
if let Some(batch) = NodeEndpointDataBatch::from_payloads(
remote,
payload_batch,
crate::perf_profile::stamp(),
) {
self.endpoint_data_batches
.send_or_drop(batch)
.map_err(|_| FipsEndpointError::Closed)?;
}
Ok(())
}
fn send_loopback(&self, payload: EndpointDataPayload) -> Result<(), FipsEndpointError> {
self.inbound_endpoint_tx
.send(NodeEndpointEvent {
messages: vec![crate::node::EndpointDataDelivery::new(
self.identity,
payload.into_body(),
)],
queued_at: crate::perf_profile::stamp(),
})
.map_err(|_| FipsEndpointError::Closed)
}
pub async fn recv_batch_into(
&self,
messages: &mut Vec<FipsEndpointMessage>,
max: usize,
) -> Option<usize> {
let max = max.clamp(1, ENDPOINT_RECV_BATCH_MAX);
messages.clear();
let mut state = self.inbound_endpoint_rx.lock().await;
state.drain_pending_into(messages, max);
while messages.len() < max {
let event = if messages.is_empty() {
state.rx.recv().await?
} else {
match state.rx.try_recv() {
Ok(event) => event,
Err(_) => break,
}
};
state.push_event_into(event, messages, max);
}
Some(messages.len())
}
pub fn blocking_send_batch_to_peer(
&self,
remote: PeerIdentity,
payloads: Vec<Vec<u8>>,
) -> Result<(), FipsEndpointError> {
self.send_payloads_to_peer(remote, payloads)
}
pub fn blocking_recv_batch_into(
&self,
messages: &mut Vec<FipsEndpointMessage>,
max: usize,
) -> Option<usize> {
let max = max.clamp(1, ENDPOINT_RECV_BATCH_MAX);
messages.clear();
let mut state = self.inbound_endpoint_rx.blocking_lock();
state.drain_pending_into(messages, max);
while messages.len() < max {
let event = if messages.is_empty() {
state.rx.blocking_recv()?
} else {
match state.rx.try_recv() {
Ok(event) => event,
Err(_) => break,
}
};
state.push_event_into(event, messages, max);
}
Some(messages.len())
}
pub async fn update_peers(
&self,
peers: Vec<crate::config::PeerConfig>,
) -> Result<UpdatePeersOutcome, FipsEndpointError> {
let (response_tx, response_rx) = oneshot::channel();
self.endpoint_control_tx
.send(NodeEndpointControlCommand::UpdatePeers { peers, response_tx })
.await
.map_err(|_| FipsEndpointError::Closed)?;
match response_rx.await.map_err(|_| FipsEndpointError::Closed)? {
Ok(outcome) => Ok(UpdatePeersOutcome::from(outcome)),
Err(error) => Err(FipsEndpointError::Node(error)),
}
}
pub async fn refresh_peer_paths(
&self,
peers: Vec<PeerIdentity>,
) -> Result<usize, FipsEndpointError> {
let (response_tx, response_rx) = oneshot::channel();
let npubs = peers.into_iter().map(|peer| peer.npub()).collect();
self.endpoint_control_tx
.send(NodeEndpointControlCommand::RefreshPeerPaths { npubs, response_tx })
.await
.map_err(|_| FipsEndpointError::Closed)?;
match response_rx.await.map_err(|_| FipsEndpointError::Closed)? {
Ok(refreshed) => Ok(refreshed),
Err(error) => Err(FipsEndpointError::Node(error)),
}
}
pub async fn peers(&self) -> Result<Vec<FipsEndpointPeer>, FipsEndpointError> {
let (response_tx, response_rx) = oneshot::channel();
self.endpoint_control_tx
.send(NodeEndpointControlCommand::PeerSnapshot { response_tx })
.await
.map_err(|_| FipsEndpointError::Closed)?;
response_rx
.await
.map(|peers| peers.into_iter().map(FipsEndpointPeer::from).collect())
.map_err(|_| FipsEndpointError::Closed)
}
pub async fn local_advertised_endpoints(
&self,
) -> Result<Vec<crate::discovery::nostr::OverlayEndpointAdvert>, FipsEndpointError> {
let (response_tx, response_rx) = oneshot::channel();
self.endpoint_control_tx
.send(NodeEndpointControlCommand::LocalAdvertSnapshot { response_tx })
.await
.map_err(|_| FipsEndpointError::Closed)?;
response_rx.await.map_err(|_| FipsEndpointError::Closed)
}
pub async fn relay_statuses(&self) -> Result<Vec<FipsEndpointRelayStatus>, FipsEndpointError> {
let (response_tx, response_rx) = oneshot::channel();
self.endpoint_control_tx
.send(NodeEndpointControlCommand::RelaySnapshot { response_tx })
.await
.map_err(|_| FipsEndpointError::Closed)?;
response_rx
.await
.map(|relays| {
relays
.into_iter()
.map(FipsEndpointRelayStatus::from)
.collect()
})
.map_err(|_| FipsEndpointError::Closed)
}
pub async fn update_relays(
&self,
advert_relays: Vec<String>,
dm_relays: Vec<String>,
) -> Result<(), FipsEndpointError> {
let (response_tx, response_rx) = oneshot::channel();
self.endpoint_control_tx
.send(NodeEndpointControlCommand::UpdateRelays {
advert_relays,
dm_relays,
response_tx,
})
.await
.map_err(|_| FipsEndpointError::Closed)?;
response_rx
.await
.map_err(|_| FipsEndpointError::Closed)?
.map_err(FipsEndpointError::Node)
}
pub async fn send_ip_packet(
&self,
packet: impl Into<Vec<u8>>,
) -> Result<(), FipsEndpointError> {
self.outbound_packets
.send(packet.into())
.await
.map_err(|_| FipsEndpointError::Closed)
}
pub async fn recv_ip_packet(&self) -> Option<NodeDeliveredPacket> {
self.delivered_packets.lock().await.recv().await
}
pub async fn shutdown(&self) -> Result<(), FipsEndpointError> {
let shutdown_tx = self
.shutdown_tx
.lock()
.map_err(|_| FipsEndpointError::Closed)?
.take();
if let Some(shutdown_tx) = shutdown_tx {
let _ = shutdown_tx.send(());
}
let task = self
.task
.lock()
.map_err(|_| FipsEndpointError::Closed)?
.take();
if let Some(task) = task {
task.await??;
}
Ok(())
}
}
impl Drop for FipsEndpoint {
fn drop(&mut self) {
if let Ok(mut shutdown_tx) = self.shutdown_tx.lock()
&& let Some(shutdown_tx) = shutdown_tx.take()
{
let _ = shutdown_tx.send(());
}
if let Ok(mut task) = self.task.lock()
&& let Some(task) = task.take()
{
task.abort();
}
}
}