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
82impl PeerAddress {
83 /// Create a new peer address.
84 pub fn new(transport: impl Into<String>, addr: impl Into<String>) -> Self {
85 Self {
86 transport: transport.into(),
87 addr: addr.into(),
88 priority: default_priority(),
89 seen_at_ms: None,
90 }
91 }
92
93 /// Create a new peer address with priority.
94 pub fn with_priority(
95 transport: impl Into<String>,
96 addr: impl Into<String>,
97 priority: u8,
98 ) -> Self {
99 Self {
100 transport: transport.into(),
101 addr: addr.into(),
102 priority,
103 seen_at_ms: None,
104 }
105 }
106
107 /// Tag this address with a freshness timestamp. Used by the dialer to
108 /// rank candidates from multiple sources (overlay advert, recent-peers
109 /// cache, operator hints) by recency without caring where they came
110 /// from. See [`crate::config::PeerAddress::seen_at_ms`].
111 pub fn with_seen_at_ms(mut self, seen_at_ms: u64) -> Self {
112 self.seen_at_ms = Some(seen_at_ms);
113 self
114 }
115}
116
117/// Configuration for a known peer.
118///
119/// Peers are identified by their Nostr public key (npub) and can have
120/// multiple transport addresses for reaching them.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122#[serde(deny_unknown_fields)]
123pub struct PeerConfig {
124 /// The peer's Nostr public key in npub (bech32) or hex format.
125 pub npub: String,
126
127 /// Human-readable alias for the peer (optional).
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub alias: Option<String>,
130
131 /// Transport addresses for reaching this peer.
132 ///
133 /// At least one address is required unless Nostr discovery is enabled,
134 /// in which case the address list may be empty and endpoints are
135 /// resolved from the peer's Nostr advert at dial time.
136 #[serde(default)]
137 pub addresses: Vec<PeerAddress>,
138
139 /// Connection policy for this peer.
140 #[serde(default)]
141 pub connect_policy: ConnectPolicy,
142
143 /// Whether to automatically reconnect after link-dead removal.
144 /// When true (default), the node will retry connecting with exponential
145 /// backoff after MMP removes this peer due to liveness timeout.
146 #[serde(default = "default_auto_reconnect")]
147 pub auto_reconnect: bool,
148}
149
150impl Default for PeerConfig {
151 fn default() -> Self {
152 Self {
153 npub: String::new(),
154 alias: None,
155 addresses: Vec::new(),
156 connect_policy: ConnectPolicy::default(),
157 auto_reconnect: default_auto_reconnect(),
158 }
159 }
160}
161
162impl PeerConfig {
163 /// Create a new peer config with a single address.
164 pub fn new(
165 npub: impl Into<String>,
166 transport: impl Into<String>,
167 addr: impl Into<String>,
168 ) -> Self {
169 Self {
170 npub: npub.into(),
171 alias: None,
172 addresses: vec![PeerAddress::new(transport, addr)],
173 connect_policy: ConnectPolicy::default(),
174 auto_reconnect: default_auto_reconnect(),
175 }
176 }
177
178 /// Set an alias for the peer.
179 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
180 self.alias = Some(alias.into());
181 self
182 }
183
184 /// Add an additional address for the peer.
185 pub fn with_address(mut self, addr: PeerAddress) -> Self {
186 self.addresses.push(addr);
187 self
188 }
189
190 /// Get addresses sorted by priority (lowest first).
191 pub fn addresses_by_priority(&self) -> Vec<&PeerAddress> {
192 let mut addrs: Vec<_> = self.addresses.iter().collect();
193 addrs.sort_by_key(|a| a.priority);
194 addrs
195 }
196
197 /// Check if this peer should auto-connect on startup.
198 pub fn is_auto_connect(&self) -> bool {
199 matches!(self.connect_policy, ConnectPolicy::AutoConnect)
200 }
201}