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}