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}