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