Files
fips/src/config/peer.rs
T
Martti MalmiandJohnathan Corgan da0d9d39a0 node: refresh active peer paths without dropping links
Add Node::update_peers for runtime peer-list refresh. It re-derives the
active peer connections from a new peer configuration, adding newly
configured peers and removing those no longer present, while keeping
links to peers that remain in the set rather than tearing every
connection down. The call returns an UpdatePeersOutcome summarizing the
added, removed, and retained peers.

PeerAddress gains a seen_at_ms recency field (with_seen_at_ms). Active
path selection now sorts address candidates by recency so the most
recently observed address wins when concurrent path probes race.

complete_rekey_msg2 now returns the remote peer's startup epoch
alongside the new Noise session, letting the rekey path detect a peer
restart and clear stale session state. A stale FSP session is cleared
when a peer restart is detected during FMP rekey or cross-connection
promotion, so the session-layer map no longer lingers out of sync with
the freshly promoted peer.

Per-tick work budgets bound the connection churn in a single node tick
(MAX_DISCOVERY_CONNECTS_PER_TICK, MAX_RETRY_CONNECTIONS_PER_TICK,
MAX_PARALLEL_PATH_CANDIDATES_PER_PEER); work beyond a tick's budget is
deferred to the next tick rather than discarded.

Co-authored-by: Johnathan Corgan <johnathan@corganlabs.com>
2026-05-30 00:46:38 +00:00

205 lines
6.3 KiB
Rust

//! Peer configuration types.
//!
//! Known peer definitions with transport addresses and connection policies.
use serde::{Deserialize, Serialize};
/// Connection policy for a peer.
///
/// Determines when and how to establish a connection to a peer.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConnectPolicy {
/// Connect to this peer automatically on node startup.
/// This is the only policy supported in the initial implementation.
#[default]
AutoConnect,
/// Connect only when traffic needs to be routed through this peer (future).
OnDemand,
/// Wait for explicit API call to connect (future).
Manual,
}
/// A transport-specific address for reaching a peer.
///
/// Each peer can have multiple addresses across different transports,
/// allowing fallback if one transport is unavailable.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PeerAddress {
/// Transport type (e.g., "udp", "tor", "ethernet").
pub transport: String,
/// Transport-specific address string.
///
/// Format depends on transport type:
/// - UDP/TCP: "host:port" — IP address or DNS hostname
/// (e.g., "192.168.1.1:2121" or "peer1.example.com:2121")
/// - Ethernet: "interface/mac" (e.g., "eth0/aa:bb:cc:dd:ee:ff")
pub addr: String,
/// Priority for address selection (lower = preferred).
/// When multiple addresses are available, lower priority addresses
/// are tried first.
#[serde(default = "default_priority")]
pub priority: u8,
/// Wall-clock observation timestamp (Unix ms) for ranking by recency.
///
/// `None` means "no freshness signal", typically an operator-edited
/// static config. The dialer sorts candidates by this field descending
/// so freshly observed overlay or runtime hints can be tried before stale
/// static addresses. This field is runtime-only and is ignored when
/// comparing peer-address lists for config changes.
#[serde(default, skip_serializing_if = "Option::is_none", skip_deserializing)]
pub seen_at_ms: Option<u64>,
}
impl PartialEq for PeerAddress {
fn eq(&self, other: &Self) -> bool {
self.transport == other.transport
&& self.addr == other.addr
&& self.priority == other.priority
}
}
impl Eq for PeerAddress {}
fn default_priority() -> u8 {
100
}
fn default_auto_reconnect() -> bool {
true
}
impl PeerAddress {
/// Create a new peer address.
pub fn new(transport: impl Into<String>, addr: impl Into<String>) -> Self {
Self {
transport: transport.into(),
addr: addr.into(),
priority: default_priority(),
seen_at_ms: None,
}
}
/// Create a new peer address with priority.
pub fn with_priority(
transport: impl Into<String>,
addr: impl Into<String>,
priority: u8,
) -> Self {
Self {
transport: transport.into(),
addr: addr.into(),
priority,
seen_at_ms: None,
}
}
/// Tag this address with a freshness timestamp for source-agnostic
/// candidate ranking.
pub fn with_seen_at_ms(mut self, seen_at_ms: u64) -> Self {
self.seen_at_ms = Some(seen_at_ms);
self
}
}
/// Configuration for a known peer.
///
/// Peers are identified by their Nostr public key (npub) and can have
/// multiple transport addresses for reaching them.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PeerConfig {
/// The peer's Nostr public key in npub (bech32) or hex format.
pub npub: String,
/// Human-readable alias for the peer (optional).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alias: Option<String>,
/// Transport addresses for reaching this peer.
///
/// At least one address is required unless `via_nostr` is `true`,
/// in which case the address list may be empty and endpoints are
/// resolved from the peer's Nostr advert at dial time.
#[serde(default)]
pub addresses: Vec<PeerAddress>,
/// Connection policy for this peer.
#[serde(default)]
pub connect_policy: ConnectPolicy,
/// Whether to automatically reconnect after link-dead removal.
/// When true (default), the node will retry connecting with exponential
/// backoff after MMP removes this peer due to liveness timeout.
#[serde(default = "default_auto_reconnect")]
pub auto_reconnect: bool,
/// Whether to append Nostr-advertised endpoints when dialing this peer.
///
/// Static addresses are still attempted first; advert-derived endpoints are
/// appended as fallback candidates.
#[serde(default)]
pub via_nostr: bool,
}
impl Default for PeerConfig {
fn default() -> Self {
Self {
npub: String::new(),
alias: None,
addresses: Vec::new(),
connect_policy: ConnectPolicy::default(),
auto_reconnect: default_auto_reconnect(),
via_nostr: false,
}
}
}
impl PeerConfig {
/// Create a new peer config with a single address.
pub fn new(
npub: impl Into<String>,
transport: impl Into<String>,
addr: impl Into<String>,
) -> Self {
Self {
npub: npub.into(),
alias: None,
addresses: vec![PeerAddress::new(transport, addr)],
connect_policy: ConnectPolicy::default(),
auto_reconnect: default_auto_reconnect(),
via_nostr: false,
}
}
/// Set an alias for the peer.
pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
self.alias = Some(alias.into());
self
}
/// Add an additional address for the peer.
pub fn with_address(mut self, addr: PeerAddress) -> Self {
self.addresses.push(addr);
self
}
/// Get addresses sorted by priority (lowest first).
pub fn addresses_by_priority(&self) -> Vec<&PeerAddress> {
let mut addrs: Vec<_> = self.addresses.iter().collect();
addrs.sort_by_key(|a| a.priority);
addrs
}
/// Check if this peer should auto-connect on startup.
pub fn is_auto_connect(&self) -> bool {
matches!(self.connect_policy, ConnectPolicy::AutoConnect)
}
}