summaryrefslogtreecommitdiffstats
path: root/src/transport
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-28 05:00:01 +0100
committertv <tv@krebsco.de>2026-10-28 05:00:01 +0100
commitb4a01ca69526510f3446edbf18672be37d70f79c (patch)
tree8605ce47001cb98e9bb4456a1776974c063324dc /src/transport
parent81515a196b527b79a5d2230a1aeca1ff4769d69d (diff)
peers connect over all available transports
Diffstat (limited to 'src/transport')
-rw-r--r--src/transport/identity.rs90
1 files changed, 83 insertions, 7 deletions
diff --git a/src/transport/identity.rs b/src/transport/identity.rs
index 0c4cda4..94faded 100644
--- a/src/transport/identity.rs
+++ b/src/transport/identity.rs
@@ -20,7 +20,9 @@
//! (`learn_addresses`): hints, not proofs. A message to a node goes to the addresses
//! it proved itself at, best first (`Paths::rank`, `Paths::send_first`: the next
//! when a send fails); with only hints, those are asked WHO, and the send is refused
-//! until one answers (the node retries refused sends).
+//! until one answers (the node retries refused sends). A node proven on some
+//! transports is asked WHO at its hints on the others too, now and then
+//! (`OTHER_PATHS_ASK_INTERVAL`), so that each transport both have becomes a path.
//!
//! Addresses and the transports behind them are the paths layer's
//! (`transport::paths`): the books keep addresses as paths keep them.
@@ -50,6 +52,10 @@ pub(crate) const WHO_LABEL: &[u8] = b"continuum/who/1";
pub(crate) const ME_LABEL: &[u8] = b"continuum/me/1";
/// An address is asked WHO at most this often, unless a seed is being resolved.
const ASK_INTERVAL: Duration = Duration::from_secs(5);
+/// A node's addresses on transports it hasn't proven itself on are asked WHO at most
+/// this often (`Book::unproven_hints`): over Tor, a WHO is a dial that takes seconds,
+/// and an address that doesn't answer shouldn't be pressed.
+const OTHER_PATHS_ASK_INTERVAL: Duration = Duration::from_secs(60);
/// How long resolving a seed waits for its ME.
const RESOLVE_TIMEOUT: Duration = Duration::from_secs(10);
/// Addresses kept per node from hints: about one per transport.
@@ -190,19 +196,44 @@ impl Book {
}
}
+ /// Where `node` is said to be on transports it hasn't proven itself on yet.
+ fn unproven_hints(&self, node: &str) -> Vec<String> {
+ let proven = self.proven.get(node);
+ self.hints
+ .get(node)
+ .into_iter()
+ .flatten()
+ .filter(|hint| {
+ proven.is_none_or(|proven| {
+ !proven
+ .iter()
+ .any(|address| scheme_of(address) == scheme_of(hint))
+ })
+ })
+ .cloned()
+ .collect()
+ }
+
/// Whether to ask `address` WHO now (always, if `force`), noting that it was asked.
fn ask(&mut self, address: &str, now: Instant, force: bool) -> bool {
- if !force
- && self
- .asked
- .get(address)
- .is_some_and(|at| now.duration_since(*at) < ASK_INTERVAL)
+ let interval = if force { Duration::ZERO } else { ASK_INTERVAL };
+ self.ask_every(address, now, interval)
+ }
+
+ /// Whether to ask `address` WHO now, if it wasn't within `interval`, noting that it
+ /// was asked.
+ fn ask_every(&mut self, address: &str, now: Instant, interval: Duration) -> bool {
+ if self
+ .asked
+ .get(address)
+ .is_some_and(|at| now.duration_since(*at) < interval)
{
return false;
}
if self.asked.len() >= MAX_ENTRIES {
+ // The longest interval: other paths' asks are kept until they are due again.
self.asked
- .retain(|_, at| now.duration_since(*at) < ASK_INTERVAL);
+ .retain(|_, at| now.duration_since(*at) < OTHER_PATHS_ASK_INTERVAL);
}
self.asked.insert(address.to_owned(), now);
true
@@ -301,6 +332,27 @@ impl Shared {
if !self.lock().ask(address, Instant::now(), force) {
return;
}
+ self.send_who(address);
+ }
+
+ /// Asks `node`'s addresses on transports it hasn't proven itself on WHO, now and then
+ /// (`OTHER_PATHS_ASK_INTERVAL`): its ME there proves another path to it, which the
+ /// paths layer then measures, and fails over to.
+ fn ask_other_paths(self: &Arc<Self>, node: &str) {
+ let due: Vec<String> = {
+ let mut book = self.lock();
+ let now = Instant::now();
+ book.unproven_hints(node)
+ .into_iter()
+ .filter(|address| book.ask_every(address, now, OTHER_PATHS_ASK_INTERVAL))
+ .collect()
+ };
+ for address in due {
+ self.send_who(&address);
+ }
+ }
+
+ fn send_who(self: &Arc<Self>, address: &str) {
let shared = Arc::clone(self);
let address = address.to_owned();
tokio::spawn(async move { shared.send_proof(MessageKind::Who, &address).await });
@@ -571,6 +623,9 @@ impl Transport for IdentityTransport {
/// fails (`Paths::send_first`); refused if it proved itself nowhere yet.
async fn send_encoded(&self, message: &EncodedMessage) -> Result<()> {
let proven = self.shared.proven_at(&message.peer);
+ if !proven.is_empty() {
+ self.shared.ask_other_paths(&message.peer);
+ }
if proven.is_empty() {
let hints = {
let mut book = self.shared.lock();
@@ -652,6 +707,27 @@ mod tests {
}
#[test]
+ fn other_paths_are_the_hints_on_transports_not_proven() {
+ let now = Instant::now();
+ let mut book = Book::default();
+ book.hints
+ .insert("n1".into(), vec!["s:x".into(), "t:x".into(), "u:x".into()]);
+ // Proven nowhere: every hint.
+ assert_eq!(book.unproven_hints("n1"), ["s:x", "t:x", "u:x"]);
+ // Proven on s (even at another address): the others.
+ book.bind("s:y", "n1", now);
+ assert_eq!(book.unproven_hints("n1"), ["t:x", "u:x"]);
+ assert!(book.ask_every("t:x", now, OTHER_PATHS_ASK_INTERVAL));
+ assert!(!book.ask_every("t:x", now + ASK_INTERVAL, OTHER_PATHS_ASK_INTERVAL));
+ assert!(book.ask_every(
+ "t:x",
+ now + OTHER_PATHS_ASK_INTERVAL,
+ OTHER_PATHS_ASK_INTERVAL
+ ));
+ assert!(book.unproven_hints("n2").is_empty());
+ }
+
+ #[test]
fn asking_is_rate_limited() {
let now = Instant::now();
let mut book = Book::default();