diff options
| author | tv <tv@krebsco.de> | 2026-10-27 01:01:51 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-10-27 01:01:51 +0100 |
| commit | 5fd686dcd8ddfbdf276c044e5acf03f744e03193 (patch) | |
| tree | a4f04328c8a2de0a50d3af3c9a8a92bfe2b118a2 | |
| parent | b90cfd1fde4e1c00d03e44976753d0044b273bda (diff) | |
introduce addresses
| -rw-r--r-- | src/swarm/members.rs | 66 | ||||
| -rw-r--r-- | src/swarm/peer/handshake.rs | 28 | ||||
| -rw-r--r-- | src/swarm/state_file.rs | 21 | ||||
| -rw-r--r-- | src/swarm/topology.rs | 1 | ||||
| -rw-r--r-- | src/transport/firewall.rs | 12 | ||||
| -rw-r--r-- | src/transport/mod.rs | 30 | ||||
| -rw-r--r-- | src/transport/traffic.rs | 12 | ||||
| -rw-r--r-- | src/transport/wire.rs | 28 |
8 files changed, 173 insertions, 25 deletions
diff --git a/src/swarm/members.rs b/src/swarm/members.rs index cb8a558..c2edd01 100644 --- a/src/swarm/members.rs +++ b/src/swarm/members.rs @@ -10,8 +10,10 @@ //! //! Besides its tier, an entry holds the alias a node gives itself (`--alias`), a name //! for people reading /peers and metrics (not unique and never an identity: the public -//! key stays that), and an `alive` stamp the node renews now and then: the members -//! renewed within `--electorate-window` are the electorate of a remake. +//! key stays that), where it can be reached (`addresses`, as its transport gives them: +//! hints for the others' transports), and an `alive` stamp the node renews now and +//! then: the members renewed within `--electorate-window` are the electorate of a +//! remake. use std::collections::HashMap; use std::time::Duration; @@ -108,6 +110,42 @@ fn set_alias(doc: &mut AutoCommit, pk: &str, alias: Option<&str>) -> Result<(), } } +fn addresses_path(pk: &str) -> Vec<String> { + let mut path = member_path(pk); + path.push("addresses".to_owned()); + path +} + +/// Addresses a member most likely has, at most: one per transport. +const MAX_ADDRESSES: usize = 8; + +/// Where `pk` says it can be reached (at most `MAX_ADDRESSES`); none if its entry +/// doesn't say. +pub(crate) fn addresses(doc: &AutoCommit, pk: &str) -> Vec<String> { + match store::read_path(doc, &addresses_path(pk)) { + Ok(Some(JsonValue::Array(items))) => items + .into_iter() + .filter_map(|item| match item { + JsonValue::String(address) => Some(address), + _ => None, + }) + .take(MAX_ADDRESSES) + .collect(), + _ => Vec::new(), + } +} + +/// Hands every other member's addresses to the transport (`Transport::learn_addresses`): +/// the member list is how a node finds where members it never met are. +pub(crate) fn learn_addresses(state: &AppState, doc: &AutoCommit) { + let me = state.local_id(); + for pk in members(doc) { + if pk != me { + state.transport.learn_addresses(&pk, &addresses(doc, &pk)); + } + } +} + fn alive_path(pk: &str) -> Vec<String> { let mut path = member_path(pk); path.push("alive".to_owned()); @@ -146,15 +184,16 @@ fn alive_renewal(window: Duration) -> Duration { } /// Registers this node if it is ready and its entry is missing, keeps its alias as -/// configured (set, changed or removed after a restart with another `--alias`), and -/// renews its `alive` stamp now and then (see `electorate`). +/// configured (set, changed or removed after a restart with another `--alias`) and its +/// addresses as the transport gives them, and renews its `alive` stamp now and then +/// (see `electorate`). pub(crate) async fn ensure_registered(state: &AppState) { if !state.is_ready().await { return; } let local = state.local_id().to_owned(); let own = member_path(&local); - let (registered, recorded_alias, alive) = { + let (registered, recorded_alias, recorded_addresses, alive) = { let replica = state.doc.read().await; let registered = store::read_path(&replica.doc, &own) .ok() @@ -163,14 +202,17 @@ pub(crate) async fn ensure_registered(state: &AppState) { ( registered, alias(&replica.doc, &local), + addresses(&replica.doc, &local), millis_at(&replica.doc, &alive_path(&local)), ) }; let wanted = state.alias.clone(); + let mut own_addresses = state.transport.local_addresses(); + own_addresses.truncate(MAX_ADDRESSES); let now = state.clock.now_millis().as_u64(); if !registered { - let mut entry = json!({ "since": now, "alive": now }); + let mut entry = json!({ "since": now, "alive": now, "addresses": own_addresses }); if let Some(alias) = &wanted { entry["alias"] = json!(alias); } @@ -199,6 +241,18 @@ pub(crate) async fn ensure_registered(state: &AppState) { } } + if recorded_addresses != own_addresses { + let path = addresses_path(&local); + let value = json!(own_addresses); + match state + .mutate_doc(move |doc| store::write_path(doc, &path, &value)) + .await + { + Ok(()) => info!(addresses = ?own_addresses, "Addresses updated in the member list"), + Err(error) => warn!(error = %error, "Failed to record our addresses"), + } + } + if recorded_alias == wanted { return; } diff --git a/src/swarm/peer/handshake.rs b/src/swarm/peer/handshake.rs index 4c657ad..4a7f451 100644 --- a/src/swarm/peer/handshake.rs +++ b/src/swarm/peer/handshake.rs @@ -19,6 +19,9 @@ use crate::transport::{self, ParsedMessage}; /// costs the recipient as much as it is long. A joining node needs only a few to find /// the swarm; the member list gives it the rest once it is ready. const MAX_LISTED_PEERS: usize = 64; +/// Addresses of one peer listed in a HELLO_REPLY, at most (a node has one per transport, +/// so a few). +const MAX_LISTED_ADDRESSES: usize = 8; pub(crate) async fn remember_peer(state: &AppState, peer: &str) { if peer.is_empty() { @@ -202,6 +205,17 @@ pub(super) async fn send_hello_reply( peers.sort_by_cached_key(|known| topology::pair_score(peer, known)); peers.truncate(MAX_LISTED_PEERS); } + let peers = peers + .into_iter() + .map(|known| { + let mut addresses = state.transport.addresses_of(&known); + addresses.truncate(MAX_LISTED_ADDRESSES); + transport::ListedPeer { + id: known, + addresses, + } + }) + .collect(); let auth = state .swarm_key @@ -237,11 +251,17 @@ pub(super) async fn send_hello_reply( /// Greets the peers a HELLO_REPLY listed: at most `MAX_LISTED_PEERS`, only valid /// identities, and, once the member list is known, only those that are our neighbours /// (it says whom we greet; a listed peer that isn't one would get nothing from us, so -/// it isn't remembered either). -pub(super) async fn discover_peers(state: &AppState, from_peer: &str, advertised_peers: &[String]) { +/// it isn't remembered either). Where the sender reaches them is a hint to the +/// transport (at most `MAX_LISTED_ADDRESSES` each). +pub(super) async fn discover_peers( + state: &AppState, + from_peer: &str, + advertised_peers: &[transport::ListedPeer], +) { let view_known = topology::view_known(state); let mut taken = 0; - for discovered_peer in advertised_peers { + for listed in advertised_peers { + let discovered_peer = &listed.id; if discovered_peer == from_peer || !state.transport.is_peer_id(discovered_peer) { continue; } @@ -258,6 +278,8 @@ pub(super) async fn discover_peers(state: &AppState, from_peer: &str, advertised } taken += 1; + let addresses = &listed.addresses[..listed.addresses.len().min(MAX_LISTED_ADDRESSES)]; + state.transport.learn_addresses(discovered_peer, addresses); remember_peer(state, discovered_peer).await; if let Err(error) = send_hello_once(state, discovered_peer).await { warn!( diff --git a/src/swarm/state_file.rs b/src/swarm/state_file.rs index 73b80a2..3693d89 100644 --- a/src/swarm/state_file.rs +++ b/src/swarm/state_file.rs @@ -31,7 +31,8 @@ const MIN_WRITE_INTERVAL: Duration = Duration::from_secs(3600); #[serde(rename_all = "camelCase")] pub(crate) struct Remembered { pub(crate) swarm_id: String, - /// Public keys of members to join through (sorted). + /// Addresses of members to join through (sorted), as their member list entries give + /// them (`swarm::members::addresses`). #[serde(default)] pub(crate) seeds: Vec<String>, } @@ -173,16 +174,22 @@ pub(crate) async fn update(state: &AppState, now: bool) { } } -/// Members to rejoin through: those alive within the electorate window, stable ones -/// first, at most `MAX_SEEDS`, sorted. Changes only when members join, leave or change -/// tier, so the file rarely does. +/// Where to rejoin through: the addresses of members alive within the electorate +/// window, stable ones first, of at most `MAX_SEEDS` members, sorted. Changes only when +/// members join, leave, move or change tier, so the file rarely does. fn seeds(doc: &automerge::AutoCommit, me: &str, window: Duration, now: u64) -> Vec<String> { let (mut stable, transient): (Vec<String>, Vec<String>) = members::electorate(doc, now, window) .into_iter() .filter(|pk| pk != me) .partition(|pk| members::tier(doc, pk) == members::Tier::Stable); stable.extend(transient); - stable.truncate(MAX_SEEDS); - stable.sort(); - stable + let mut seeds: Vec<String> = stable + .iter() + .filter(|pk| !members::addresses(doc, pk).is_empty()) + .take(MAX_SEEDS) + .flat_map(|pk| members::addresses(doc, pk)) + .collect(); + seeds.sort(); + seeds.dedup(); + seeds } diff --git a/src/swarm/topology.rs b/src/swarm/topology.rs index f8a1c30..bdfeb53 100644 --- a/src/swarm/topology.rs +++ b/src/swarm/topology.rs @@ -134,6 +134,7 @@ pub(crate) async fn refresh(state: &AppState) -> Option<NeighbourChange> { } let (current, stable) = { let replica = state.doc.read().await; + members::learn_addresses(state, &replica.doc); let current = members::members(&replica.doc); let stable: BTreeSet<String> = current .iter() diff --git a/src/transport/firewall.rs b/src/transport/firewall.rs index df82aa7..3036b11 100644 --- a/src/transport/firewall.rs +++ b/src/transport/firewall.rs @@ -72,6 +72,18 @@ impl Transport for FirewallTransport { self.inner.delivery_limit() } + fn local_addresses(&self) -> Vec<String> { + self.inner.local_addresses() + } + + fn addresses_of(&self, peer: &str) -> Vec<String> { + self.inner.addresses_of(peer) + } + + fn learn_addresses(&self, peer: &str, addresses: &[String]) { + self.inner.learn_addresses(peer, addresses) + } + async fn resolve_seed(&self, seed: &str) -> Result<String> { self.inner.resolve_seed(seed).await } diff --git a/src/transport/mod.rs b/src/transport/mod.rs index 61b1f4b..29c48d6 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -297,10 +297,18 @@ pub(crate) struct HelloRequest { pub(crate) handshake: u32, } +/// A peer a HELLO_REPLY lists: who it is, and where the sender reaches it. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct ListedPeer { + pub(crate) id: String, + /// As `Transport::addresses_of` gives them; hints for the recipient. + pub(crate) addresses: Vec<String>, +} + #[derive(Clone, Serialize, Deserialize)] pub(crate) struct HelloReply { pub(crate) swarm_id: String, - pub(crate) peers: Vec<String>, + pub(crate) peers: Vec<ListedPeer>, #[serde(with = "crate::transport::wire::codec::epoch")] pub(crate) epoch: Epoch, /// See `SyncEnvelope::epoch_seq`. @@ -490,6 +498,26 @@ pub(crate) trait Transport: Send + Sync { /// a message sent is delivered or lost for good. fn delivery_limit(&self) -> std::time::Duration; + /// Where this node can be reached, as addresses other nodes can use: published in + /// the member list (`swarm::members`). By default its identity, for a transport + /// whose identities are addresses (Mycelium: a public key). + fn local_addresses(&self) -> Vec<String> { + vec![self.local_id().to_owned()] + } + + /// Where `peer` can be reached, as far as this node knows: passed on to others (in + /// a HELLO_REPLY). By default its identity, as for `local_addresses`. + fn addresses_of(&self, peer: &str) -> Vec<String> { + vec![peer.to_owned()] + } + + /// Where others say `peer` can be reached (the member list, a HELLO_REPLY): hints, + /// to be checked before they are relied on. By default nothing to learn, as for + /// `local_addresses`. + fn learn_addresses(&self, peer: &str, addresses: &[String]) { + let _ = (peer, addresses); + } + /// Resolves a seed (anything that leads to a node, e.g. an address) to that node's /// identity, as `local_id` gives it on that node. async fn resolve_seed(&self, seed: &str) -> Result<String>; diff --git a/src/transport/traffic.rs b/src/transport/traffic.rs index f9b8289..c27f0d7 100644 --- a/src/transport/traffic.rs +++ b/src/transport/traffic.rs @@ -145,6 +145,18 @@ impl Transport for CountingTransport { self.inner.delivery_limit() } + fn local_addresses(&self) -> Vec<String> { + self.inner.local_addresses() + } + + fn addresses_of(&self, peer: &str) -> Vec<String> { + self.inner.addresses_of(peer) + } + + fn learn_addresses(&self, peer: &str, addresses: &[String]) { + self.inner.learn_addresses(peer, addresses) + } + async fn resolve_seed(&self, seed: &str) -> Result<String> { self.inner.resolve_seed(seed).await } diff --git a/src/transport/wire.rs b/src/transport/wire.rs index 009380f..ceeaf87 100644 --- a/src/transport/wire.rs +++ b/src/transport/wire.rs @@ -14,7 +14,7 @@ use super::{MessageKind, ParsedMessage}; /// The wire format's version, the first byte of every payload. Bump it with every change /// to a message type or a codec (`golden_bytes` fails then). -pub(crate) const VERSION: u8 = 3; +pub(crate) const VERSION: u8 = 4; /// What the second byte of a payload says. fn tag(kind: MessageKind) -> u8 { @@ -300,7 +300,7 @@ mod tests { use crate::document::replica::Epoch; use crate::transport::{ BlobBody, BlobId, BlobMessage, HeadsSummary, HelloReply, HelloRequest, Keepalive, - MAX_LISTED_HEADS, + ListedPeer, MAX_LISTED_HEADS, }; use automerge::ChangeHash; @@ -330,7 +330,10 @@ mod tests { let reply = ParsedMessage::HelloReply(HelloReply { swarm_id: "swarm".into(), - peers: vec!["p".into()], + peers: vec![ListedPeer { + id: "p".into(), + addresses: vec!["a".into()], + }], epoch, epoch_seq: 1, auth: Vec::new(), @@ -342,7 +345,13 @@ mod tests { let ParsedMessage::HelloReply(decoded) = decode(&encode(&reply).unwrap()).unwrap() else { panic!("not a HELLO_REPLY"); }; - assert_eq!(decoded.peers, vec!["p".to_owned()]); + assert_eq!( + decoded.peers, + vec![ListedPeer { + id: "p".into(), + addresses: vec!["a".into()], + }] + ); assert_eq!(decoded.away_ms, Some(5)); assert_eq!(decoded.sender_run_id, [6; 16]); } @@ -438,12 +447,15 @@ mod tests { }); assert_eq!( hex(&encode(&hello).unwrap()), - format!("030097a5737761726d{epoch_hex}01c403010203c3{}07", run("04")) + format!("040097a5737761726d{epoch_hex}01c403010203c3{}07", run("04")) ); let reply = ParsedMessage::HelloReply(HelloReply { swarm_id: "swarm".into(), - peers: vec!["p".into()], + peers: vec![ListedPeer { + id: "p".into(), + addresses: vec!["a".into()], + }], epoch, epoch_seq: 1, auth: Vec::new(), @@ -455,7 +467,7 @@ mod tests { assert_eq!( hex(&encode(&reply).unwrap()), format!( - "030199a5737761726d91a170{epoch_hex}01c400c2{}05{}", + "040199a5737761726d9192a17091a161{epoch_hex}01c400c2{}05{}", run("04"), run("06") ) @@ -470,7 +482,7 @@ mod tests { }); assert_eq!( hex(&encode(&ping).unwrap()), - format!("0303959290c400{epoch_hex}01c3c2") + format!("0403959290c400{epoch_hex}01c3c2") ); } } |
