summaryrefslogtreecommitdiffstats
path: root/src/transport
diff options
context:
space:
mode:
Diffstat (limited to 'src/transport')
-rw-r--r--src/transport/libp2p.rs285
1 files changed, 228 insertions, 57 deletions
diff --git a/src/transport/libp2p.rs b/src/transport/libp2p.rs
index d621c8e..9217c79 100644
--- a/src/transport/libp2p.rs
+++ b/src/transport/libp2p.rs
@@ -21,7 +21,9 @@
//! (`<relay>/p2p-circuit/p2p/<it>`) is published with the others. Over a relayed connection, both sides then try to
//! connect directly (libp2p's DCUtR, hole punching); the relay carries the traffic until
//! that works, or for good if it doesn't, within its limits (`RELAY_CIRCUIT_DURATION`,
-//! `RELAY_CIRCUIT_BYTES`; a circuit that ends is made again on the next send).
+//! `RELAY_CIRCUIT_BYTES`; a circuit that ends is made again on the next send). Hole
+//! punching needs both sides to listen; where only one does, the other dials it
+//! directly once it knows where (`upgrade`).
//!
//! Of several connections to one peer (both dialled at once, or AutoNAT dialled back),
//! all but one are closed a few seconds later (`DUPLICATE_GRACE`, `Book::duplicates`),
@@ -103,9 +105,13 @@ const MAX_PEERS: usize = 4096;
/// the punching only).
const RELAY_CIRCUIT_DURATION: Duration = Duration::from_secs(3600);
const RELAY_CIRCUIT_BYTES: u64 = 1 << 30;
-/// Circuits through this node at once, and per peer.
-const RELAY_CIRCUITS: usize = 64;
-const RELAY_CIRCUITS_PER_PEER: usize = 8;
+/// Circuits through this node at once, and per peer (either end: a node behind the
+/// relay is an end of every circuit to it). libp2p's rate limits on circuits and slots
+/// (a burst, then one every minute or two, per peer and per IP address: every node
+/// behind one NAT shares one) are left out: they're for public relays serving
+/// strangers, and refused circuits used them up too.
+const RELAY_CIRCUITS: usize = 1024;
+const RELAY_CIRCUITS_PER_PEER: usize = 64;
/// A configured relay that couldn't be reached to find out its peer ID is tried again
/// after this.
const RELAY_RETRY: Duration = Duration::from_secs(30);
@@ -113,10 +119,16 @@ const RELAY_RETRY: Duration = Duration::from_secs(30);
/// needs them.
const AUTO_RELAYS: usize = 2;
const RELAY_CHECK: Duration = Duration::from_secs(30);
-/// A relay that lost the slot before granting it (it doesn't answer, or another peer
-/// answers at its address: a relay restarted without its libp2p key) isn't picked again
-/// for this long.
+/// A relay that refused a slot (`relay_refused`: none left, not allowed, not a relay,
+/// another peer at its address: a relay restarted without its libp2p key) isn't picked
+/// again for this long. One that didn't grant it for another reason (it doesn't answer,
+/// the connection closed) isn't for `RELAY_RETRY`; a slot lost is reserved again at
+/// once.
const RELAY_PAUSE: Duration = Duration::from_secs(600);
+/// A peer connected only through a relay is dialled directly where it can be reached
+/// (`upgrade`): first at once, then after a failure no sooner than this, doubling up to
+/// `RELAY_PAUSE`.
+const UPGRADE_RETRY: Duration = Duration::from_secs(30);
/// A peer nobody mentioned for this long (the member list, identify, mDNS, a
/// connection) is forgotten, unless connected: mostly old peer IDs of nodes restarted
/// without their libp2p key, whose new ones the member list has since. Members are
@@ -248,12 +260,15 @@ struct Book {
/// With `Libp2pConfig::public`: other peers that relay (identify says so), and
/// where.
public_relays: HashMap<PeerId, Multiaddr>,
- /// Relays picked that never granted a slot, and when (`RELAY_PAUSE`).
+ /// Relays not to be picked, until when (`RELAY_PAUSE`, `RELAY_RETRY`).
failed_relays: HashMap<PeerId, tokio::time::Instant>,
/// Relays that granted us a slot we hold.
slots: std::collections::HashSet<PeerId>,
/// Peers holding a slot on this node, as a relay.
slots_here: std::collections::HashSet<PeerId>,
+ /// Peers connected only through a relay, dialled directly (`upgrade`): when to again
+ /// at the soonest, and how long to wait after that.
+ upgrades: HashMap<PeerId, (tokio::time::Instant, Duration)>,
/// Open connections: to whom, whether through a relay, and the remote address.
connections: HashMap<ConnectionId, (PeerId, bool, Multiaddr)>,
/// What AutoNAT says, and the address it confirmed while public.
@@ -347,6 +362,22 @@ fn relay_failure_text(debug: &str, innermost: &str) -> String {
innermost.to_owned()
}
+/// Whether a slot ended, or wasn't granted, because the relay refused it (its error's
+/// debug form and innermost cause, as for `relay_failure_text`), rather than because it
+/// didn't answer or the connection closed: only then is it worth not asking again for a
+/// while.
+fn relay_refused(debug: &str, innermost: &str) -> bool {
+ let refusals = [
+ "NoAddressesInReservation",
+ "ResourceLimitExceeded",
+ "PermissionDenied",
+ "connection gated",
+ "UnsupportedProtocol",
+ "WrongPeerId",
+ ];
+ refusals.iter().any(|pattern| debug.contains(pattern)) || innermost == "Failed"
+}
+
/// The innermost of `error`'s causes, each distinct one once: those libp2p lists in a
/// "Multiple dial errors occurred:" text (one per line, ` - <cause>: <its cause>...`),
/// or the last of the source chain.
@@ -459,7 +490,7 @@ impl Book {
paused: self
.failed_relays
.values()
- .filter(|at| at.elapsed() < RELAY_PAUSE)
+ .filter(|until| tokio::time::Instant::now() < **until)
.count(),
granted: self.counts.slots_granted,
lost: self.counts.slots_lost,
@@ -503,6 +534,7 @@ impl Book {
self.continuum.remove(peer);
self.lan.remove(peer);
self.failed_relays.remove(peer);
+ self.upgrades.remove(peer);
self.seen.remove(peer);
}
stale.len()
@@ -924,6 +956,8 @@ impl Behaviour {
max_circuits_per_peer: RELAY_CIRCUITS_PER_PEER,
max_circuit_duration: RELAY_CIRCUIT_DURATION,
max_circuit_bytes: RELAY_CIRCUIT_BYTES,
+ reservation_rate_limiters: Vec::new(),
+ circuit_src_rate_limiters: Vec::new(),
..Default::default()
},
)
@@ -1080,8 +1114,8 @@ fn pick_relays(
}
let (direct, configured, listed, members, public) = {
let mut book = lock(book);
- book.failed_relays
- .retain(|_, at| at.elapsed() < RELAY_PAUSE);
+ let now = tokio::time::Instant::now();
+ book.failed_relays.retain(|_, until| now < *until);
let failed = book.failed_relays.clone();
let usable = |offers: &HashMap<PeerId, Multiaddr>| -> HashMap<PeerId, Multiaddr> {
offers
@@ -1126,6 +1160,50 @@ fn pick_relays(
}
}
+/// Dials `peer` directly if it's connected only through a relay and is known elsewhere
+/// too (the member list, identify, mDNS), since the first connection, made through a
+/// relay (a relayed seed), would otherwise be kept for good: a peer connected isn't
+/// dialled again, and hole punching needs both sides to listen. The direct connection,
+/// once open, wins over the relayed one (`Book::duplicates`).
+fn upgrade(swarm: &mut Swarm<Behaviour>, book: &Mutex<Book>, peer: PeerId, local: PeerId) {
+ let direct: Vec<Multiaddr> = {
+ let mut book = lock(book);
+ if book.connection(&peer) != Some(Connection::Relayed) {
+ return;
+ }
+ let direct: Vec<Multiaddr> = book
+ .addresses
+ .get(&peer)
+ .map(|known| dialable(known, local))
+ .unwrap_or_default()
+ .into_iter()
+ .filter(|address| !is_relayed(address))
+ .collect();
+ if direct.is_empty() {
+ return;
+ }
+ let now = tokio::time::Instant::now();
+ let (due, wait) = book
+ .upgrades
+ .get(&peer)
+ .copied()
+ .unwrap_or((now, UPGRADE_RETRY));
+ if now < due {
+ return;
+ }
+ book.upgrades
+ .insert(peer, (now + wait, (wait * 2).min(RELAY_PAUSE)));
+ direct
+ };
+ debug!(peer = %Addressed(SCHEME, &peer), addresses = ?direct, "Connected only through a relay; dialling directly");
+ let _ = swarm.dial(
+ DialOpts::peer_id(peer)
+ .addresses(direct)
+ .condition(PeerCondition::NotDialing)
+ .build(),
+ );
+}
+
/// Asks the DHT (with `--libp2p-public`) where `peer` is, for the next attempt.
fn find_on_dht(swarm: &mut Swarm<Behaviour>, peer: PeerId) {
if let Some(dht) = swarm.behaviour_mut().kad.as_mut() {
@@ -1210,6 +1288,7 @@ async fn run(
Some(Command::Connect { peer, addresses, done }) => {
if swarm.is_connected(&peer) {
let _ = done.send(Ok(()));
+ upgrade(&mut swarm, &book, peer, relaying.local);
continue;
}
let waiting = connecting.entry(peer).or_default();
@@ -1285,6 +1364,9 @@ async fn run(
connection_id,
(peer_id, relayed, endpoint.get_remote_address().clone()),
);
+ if !relayed {
+ book.upgrades.remove(&peer_id);
+ }
if relaying.local < peer_id
&& book.connections.values().filter(|(connected, _, _)| *connected == peer_id).count() > 1
{
@@ -1363,31 +1445,50 @@ async fn run(
// A relay picked: the next check picks again, another one if this one
// never granted the slot.
if let Some(relay) = picked.remove(&listener_id) {
- let lost = {
+ let refused = match &reason {
+ Ok(()) => false,
+ Err(error) => relay_refused(&format!("{error:?}"), &innermost_causes(error)),
+ };
+ let (lost, pause) = {
let mut book = lock(&book);
let lost = book.slots.remove(&relay);
+ let pause = match (refused, lost) {
+ (true, _) => Some(RELAY_PAUSE),
+ (false, true) => None,
+ (false, false) => Some(RELAY_RETRY),
+ };
if lost {
book.counts.slots_lost += 1;
} else {
book.counts.slots_not_granted += 1;
- book.failed_relays.insert(relay, tokio::time::Instant::now());
// Named without its peer ID: maybe another one now.
if let Some(address) = book.find_again(relay) {
let _ = again.send(Command::FindRelay { address });
}
}
- lost
+ if let Some(pause) = pause {
+ book.failed_relays.insert(relay, tokio::time::Instant::now() + pause);
+ }
+ (lost, pause)
};
+ // Again at once, or others meanwhile.
+ let _ = again.send(Command::CheckRelays);
let reason = relay_failure(&reason);
- if lost {
- debug!(relay = %Addressed(SCHEME, &relay), reason = %reason, "Lost the slot on a relay");
- } else {
- info!(
- relay = %Addressed(SCHEME, &relay),
- pause = ?RELAY_PAUSE,
- reason = %reason,
- "A relay didn't grant a slot; picking others for a while"
- );
+ match (lost, pause) {
+ (true, None) => {
+ info!(relay = %Addressed(SCHEME, &relay), reason = %reason, "Lost the slot on a relay; reserving again");
+ }
+ (true, Some(pause)) => {
+ info!(relay = %Addressed(SCHEME, &relay), pause = ?pause, reason = %reason, "Lost the slot on a relay; picking others for a while");
+ }
+ (false, pause) => {
+ info!(
+ relay = %Addressed(SCHEME, &relay),
+ pause = ?pause.unwrap_or_default(),
+ reason = %reason,
+ "A relay didn't grant a slot; picking others for a while"
+ );
+ }
}
}
}
@@ -1456,43 +1557,46 @@ async fn run(
SwarmEvent::Behaviour(BehaviourEvent::Identify(
::libp2p::identify::Event::Received { connection_id, peer_id, info },
)) => {
- let mut book = lock(&book);
- let listen = published(&info.listen_addrs);
- // Every new connection identifies again: logged when it says something new.
- if book.knows(peer_id, &listen) {
- trace!(peer = %Addressed(SCHEME, &peer_id), connection = %connection_id, "Peer identified again");
- } else {
- debug!(
- peer = %Addressed(SCHEME, &peer_id),
- connection = %connection_id,
- listen = ?info.listen_addrs,
- observed = %info.observed_addr,
- agent = %info.agent_version,
- "Peer identified"
- );
- }
- // Last first: `remember` puts each in front.
- for address in listen.into_iter().rev() {
- book.remember(peer_id, address);
- }
- if relaying.public
- && info.protocols.contains(&::libp2p::relay::HOP_PROTOCOL_NAME)
- && (book.public_relays.len() < MAX_PEERS
- || book.public_relays.contains_key(&peer_id))
- && let Some(address) = book
- .addresses
- .get(&peer_id)
- .and_then(|known| known.first())
- .and_then(|address| address.clone().with_p2p(peer_id).ok())
{
- book.public_relays.insert(peer_id, address);
- }
- if info.protocols.contains(&PROTOCOL) {
- book.continuum.insert(peer_id);
- if book.lan.remove(&peer_id) {
- book.discover(peer_id);
+ let mut book = lock(&book);
+ let listen = published(&info.listen_addrs);
+ // Every new connection identifies again: logged when it says something new.
+ if book.knows(peer_id, &listen) {
+ trace!(peer = %Addressed(SCHEME, &peer_id), connection = %connection_id, "Peer identified again");
+ } else {
+ debug!(
+ peer = %Addressed(SCHEME, &peer_id),
+ connection = %connection_id,
+ listen = ?info.listen_addrs,
+ observed = %info.observed_addr,
+ agent = %info.agent_version,
+ "Peer identified"
+ );
+ }
+ // Last first: `remember` puts each in front.
+ for address in listen.into_iter().rev() {
+ book.remember(peer_id, address);
+ }
+ if relaying.public
+ && info.protocols.contains(&::libp2p::relay::HOP_PROTOCOL_NAME)
+ && (book.public_relays.len() < MAX_PEERS
+ || book.public_relays.contains_key(&peer_id))
+ && let Some(address) = book
+ .addresses
+ .get(&peer_id)
+ .and_then(|known| known.first())
+ .and_then(|address| address.clone().with_p2p(peer_id).ok())
+ {
+ book.public_relays.insert(peer_id, address);
+ }
+ if info.protocols.contains(&PROTOCOL) {
+ book.continuum.insert(peer_id);
+ if book.lan.remove(&peer_id) {
+ book.discover(peer_id);
+ }
}
}
+ upgrade(&mut swarm, &book, peer_id, relaying.local);
}
SwarmEvent::Behaviour(BehaviourEvent::Mdns(
::libp2p::mdns::Event::Discovered(found),
@@ -1507,6 +1611,7 @@ async fn run(
book.remember(peer, address.clone());
book.continuum.contains(&peer)
};
+ upgrade(&mut swarm, &book, peer, relaying.local);
if continuum {
lock(&book).discover(peer);
} else if lock(&book).lan.len() < MAX_PEERS {
@@ -2066,6 +2171,41 @@ mod tests {
assert!(a.connection(b.local_id()).is_some());
}
+ /// b listens nowhere; a, which can be dialled, reaches it through the relay r: b
+ /// then dials a directly (identify told it where), and both keep that.
+ #[tokio::test]
+ async fn a_relayed_connection_is_upgraded_where_one_side_can_be_dialled() {
+ let (_r, r_address) = memory_node_with(true, Vec::new(), true);
+ let (b, _) = memory_node_with(false, vec![r_address.clone()], false);
+ let b = Arc::new(b);
+ let mut at_b = received(Arc::clone(&b));
+ assert!(
+ eventually(|| !b.local_addresses().is_empty()).await,
+ "b got no slot on the relay"
+ );
+ let (a, _) = memory_node();
+ let relayed = b.local_addresses()[0].clone();
+ assert_eq!(a.resolve_seed(&relayed).await.unwrap(), b.local_id());
+ a.send_message(b.local_id(), &status()).await.unwrap();
+ at_b.recv().await.unwrap();
+ assert!(
+ eventually(|| b.connection(a.local_id()) == Some(Connection::Direct)).await,
+ "b didn't dial a directly"
+ );
+ assert_eq!(a.connection(b.local_id()), Some(Connection::Direct));
+ // The relayed one goes (`DUPLICATE_GRACE`); the direct one stays.
+ let deadline = tokio::time::Instant::now() + DUPLICATE_GRACE * 2;
+ while a.stats().libp2p.unwrap().connections.relayed > 0 {
+ assert!(
+ tokio::time::Instant::now() < deadline,
+ "the relayed connection stayed"
+ );
+ tokio::time::sleep(Duration::from_millis(100)).await;
+ }
+ b.send_message(a.local_id(), &status()).await.unwrap();
+ assert_eq!(a.connection(b.local_id()), Some(Connection::Direct));
+ }
+
/// A relay that is no node (`--relay-only`) relays, and takes no messages itself.
#[tokio::test]
async fn a_relay_that_is_no_node() {
@@ -2308,6 +2448,37 @@ mod tests {
assert_eq!(relay_failure(&Ok(())), "closed");
}
+ /// Only refusals pause a relay; a slot that ended otherwise is asked for again.
+ #[test]
+ fn only_refusals_pause_relays() {
+ let refused = [
+ ("Reservation(Protocol(NoAddressesInReservation))", "x"),
+ ("Reservation(Status(ResourceLimitExceeded))", "x"),
+ ("Reservation(Status(PermissionDenied))", "x"),
+ ("Denied { cause: \"connection gated\" }", "x"),
+ ("Upgrade(UnsupportedProtocol)", "x"),
+ ("Dial(WrongPeerId { obtained: … })", "x"),
+ (
+ "Codec(Error(Custom { kind: Other, error: Failed }))",
+ "Failed",
+ ),
+ ];
+ for (debug, innermost) in refused {
+ assert!(relay_refused(debug, innermost), "{debug}");
+ }
+ let not = [
+ ("Io(Custom { kind: Other, error: Timeout })", "Timeout"),
+ (
+ "Io(Custom { kind: Other, error: Right(Closed) })",
+ "connection is closed",
+ ),
+ ("Io(…)", "connection reset"),
+ ];
+ for (debug, innermost) in not {
+ assert!(!relay_refused(debug, innermost), "{debug}");
+ }
+ }
+
/// libp2p's nested dial errors come out as each address's cause, once.
#[test]
fn dial_errors_are_short() {