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