Skip to main content

fips_core/config/
peer.rs

1//! Peer configuration types.
2//!
3//! Known peer definitions with transport addresses and connection policies.
4
5use serde::{Deserialize, Serialize};
6
7/// Connection policy for a peer.
8///
9/// Determines when and how to establish a connection to a peer.
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum ConnectPolicy {
13    /// Connect to this peer automatically on node startup.
14    /// This is the only policy supported in the initial implementation.
15    #[default]
16    AutoConnect,
17
18    /// Connect only when traffic needs to be routed through this peer (future).
19    OnDemand,
20
21    /// Wait for explicit API call to connect (future).
22    Manual,
23}
24
25/// Whether a peer address came from operator configuration or discovery.
26#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum PeerAddressProvenance {
29    /// The operator explicitly configured this address.
30    #[default]
31    Configured,
32    /// The address was learned from a peer, advert, or active socket.
33    Learned,
34}
35
36impl PeerAddressProvenance {
37    fn is_configured(&self) -> bool {
38        matches!(self, Self::Configured)
39    }
40}
41
42/// A transport-specific address for reaching a peer.
43///
44/// Each peer can have multiple addresses across different transports,
45/// allowing fallback if one transport is unavailable.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47#[serde(deny_unknown_fields)]
48pub struct PeerAddress {
49    /// Transport type (e.g., "udp", "tor", "ethernet").
50    pub transport: String,
51
52    /// Transport-specific address string.
53    ///
54    /// Format depends on transport type:
55    /// - UDP/TCP: "host:port" — IP address or DNS hostname
56    ///   (e.g., "192.168.1.1:2121" or "peer1.example.com:2121")
57    /// - Ethernet: "interface/mac" (e.g., "eth0/aa:bb:cc:dd:ee:ff")
58    pub addr: String,
59
60    /// Priority for address selection (lower = preferred).
61    /// When multiple addresses are available, lower priority addresses
62    /// are tried first.
63    #[serde(default = "default_priority")]
64    pub priority: u8,
65
66    /// Trust provenance for policies that distinguish operator routes from
67    /// learned address hints.
68    ///
69    /// Omission means configured for compatibility with existing config files.
70    /// Learned runtime values retain their provenance when serialized.
71    #[serde(default, skip_serializing_if = "PeerAddressProvenance::is_configured")]
72    pub provenance: PeerAddressProvenance,
73
74    /// Wall-clock observation timestamp (Unix ms) for ranking by recency
75    /// within the same priority.
76    ///
77    /// `None` means "no freshness signal". The dialer primarily honors
78    /// explicit address priority and only uses this field to order
79    /// otherwise-equal candidates. Skipped from serde so that round-tripping
80    /// a config file doesn't produce noisy empty fields.
81    ///
82    /// Excluded from `PartialEq`: refreshing the timestamp on a peer that's
83    /// otherwise unchanged should not flag it as "updated" in
84    /// [`crate::endpoint::FipsEndpoint::update_peers`]'s diff.
85    #[serde(default, skip_serializing_if = "Option::is_none", skip_deserializing)]
86    pub seen_at_ms: Option<u64>,
87}
88
89impl PartialEq for PeerAddress {
90    fn eq(&self, other: &Self) -> bool {
91        self.transport == other.transport
92            && self.addr == other.addr
93            && self.priority == other.priority
94            && self.provenance == other.provenance
95    }
96}
97
98impl Eq for PeerAddress {}
99
100fn default_priority() -> u8 {
101    100
102}
103
104fn default_auto_reconnect() -> bool {
105    true
106}
107
108fn default_discovery_fallback_transit() -> bool {
109    true
110}
111
112impl PeerAddress {
113    /// Create an explicitly configured peer address.
114    pub fn new(transport: impl Into<String>, addr: impl Into<String>) -> Self {
115        Self {
116            transport: transport.into(),
117            addr: addr.into(),
118            priority: default_priority(),
119            provenance: PeerAddressProvenance::Configured,
120            seen_at_ms: None,
121        }
122    }
123
124    /// Create an explicitly configured peer address with priority.
125    pub fn with_priority(
126        transport: impl Into<String>,
127        addr: impl Into<String>,
128        priority: u8,
129    ) -> Self {
130        Self {
131            transport: transport.into(),
132            addr: addr.into(),
133            priority,
134            provenance: PeerAddressProvenance::Configured,
135            seen_at_ms: None,
136        }
137    }
138
139    /// Mark this address as learned from discovery or an active path.
140    pub fn learned(mut self) -> Self {
141        self.provenance = PeerAddressProvenance::Learned;
142        self
143    }
144
145    /// Whether this address was explicitly operator-configured.
146    pub fn is_configured(&self) -> bool {
147        self.provenance == PeerAddressProvenance::Configured
148    }
149
150    /// Tag this address with a freshness timestamp. Used by the dialer to
151    /// rank candidates from multiple sources (overlay advert, recent-peers
152    /// cache, operator hints) by recency without caring where they came
153    /// from. See [`crate::config::PeerAddress::seen_at_ms`].
154    pub fn with_seen_at_ms(mut self, seen_at_ms: u64) -> Self {
155        self.seen_at_ms = Some(seen_at_ms);
156        self
157    }
158}
159
160/// Configuration for a known peer.
161///
162/// Peers are identified by their Nostr public key (npub) and can have
163/// multiple transport addresses for reaching them.
164#[derive(Debug, Clone, Serialize, Deserialize)]
165#[serde(deny_unknown_fields)]
166pub struct PeerConfig {
167    /// The peer's Nostr public key in npub (bech32) or hex format.
168    pub npub: String,
169
170    /// Human-readable alias for the peer (optional).
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    pub alias: Option<String>,
173
174    /// Transport addresses for reaching this peer.
175    ///
176    /// At least one address is required unless Nostr discovery is enabled,
177    /// in which case the address list may be empty and endpoints are
178    /// resolved from the peer's Nostr advert at dial time.
179    #[serde(default)]
180    pub addresses: Vec<PeerAddress>,
181
182    /// Connection policy for this peer.
183    #[serde(default)]
184    pub connect_policy: ConnectPolicy,
185
186    /// Whether to automatically reconnect after link-dead removal.
187    /// When true (default), the node will retry connecting with exponential
188    /// backoff after MMP removes this peer due to liveness timeout.
189    #[serde(default = "default_auto_reconnect")]
190    pub auto_reconnect: bool,
191
192    /// Whether this peer may be used as an extra reply-learned lookup hop.
193    ///
194    /// Direct lookups to this peer are still allowed when false. This only
195    /// controls opportunistic fallback fanout through the peer for other
196    /// destinations.
197    #[serde(default = "default_discovery_fallback_transit")]
198    pub discovery_fallback_transit: bool,
199}
200
201impl Default for PeerConfig {
202    fn default() -> Self {
203        Self {
204            npub: String::new(),
205            alias: None,
206            addresses: Vec::new(),
207            connect_policy: ConnectPolicy::default(),
208            auto_reconnect: default_auto_reconnect(),
209            discovery_fallback_transit: default_discovery_fallback_transit(),
210        }
211    }
212}
213
214impl PeerConfig {
215    /// Create a new peer config with a single address.
216    pub fn new(
217        npub: impl Into<String>,
218        transport: impl Into<String>,
219        addr: impl Into<String>,
220    ) -> Self {
221        Self {
222            npub: npub.into(),
223            alias: None,
224            addresses: vec![PeerAddress::new(transport, addr)],
225            connect_policy: ConnectPolicy::default(),
226            auto_reconnect: default_auto_reconnect(),
227            discovery_fallback_transit: default_discovery_fallback_transit(),
228        }
229    }
230
231    /// Set an alias for the peer.
232    pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
233        self.alias = Some(alias.into());
234        self
235    }
236
237    /// Add an additional address for the peer.
238    pub fn with_address(mut self, addr: PeerAddress) -> Self {
239        self.addresses.push(addr);
240        self
241    }
242
243    /// Get addresses sorted by priority (lowest first).
244    pub fn addresses_by_priority(&self) -> Vec<&PeerAddress> {
245        let mut addrs: Vec<_> = self.addresses.iter().collect();
246        addrs.sort_by_key(|a| a.priority);
247        addrs
248    }
249
250    /// Check if this peer should auto-connect on startup.
251    pub fn is_auto_connect(&self) -> bool {
252        matches!(self.connect_policy, ConnectPolicy::AutoConnect)
253    }
254}