style: apply cargo fmt across the crate (A20)

The repo never enforced rustfmt, so formatting had drifted broadly. This is a
single mechanical `cargo fmt` pass over the whole crate (no behavioral change;
lib suite green, 493 passed). Going forward fmt should be enforced (planned CI
fmt --check step). Part of the 0.6.1 hygiene pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 02:11:44 -04:00
co-authored by Claude Opus 4.8
parent e0325d4590
commit d0a16cb8b9
54 changed files with 3761 additions and 1789 deletions
+274 -95
View File
@@ -1,14 +1,14 @@
use crate::network::{RoomState, NetError, PeerState, RoomEvent, PeerSpeakTicket};
use crate::network::{NetError, PeerSpeakTicket, PeerState, RoomEvent, RoomState};
use async_trait::async_trait;
use iroh::{Endpoint, EndpointAddr, EndpointId, SecretKey, Signature, TransportAddr};
use iroh_gossip::net::Gossip;
use iroh_gossip::proto::TopicId;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeSet, HashMap, HashSet};
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc;
use tokio::sync::mpsc::Receiver;
use std::sync::{Arc, Mutex};
use std::collections::{BTreeSet, HashMap, HashSet};
use async_trait::async_trait;
use tokio_stream::StreamExt;
use serde::{Serialize, Deserialize};
/// Domain-separation tag mixed into every signed gossip payload so a signature
/// can never be lifted out of this protocol/version into another context.
@@ -78,7 +78,12 @@ fn signable_bytes(topic: &[u8; 32], author: &EndpointId, ts: u64, msg: &GossipMe
fn sign_gossip(secret: &SecretKey, topic: &[u8; 32], ts: u64, msg: GossipMessage) -> GossipPayload {
let author = secret.public();
let sig = secret.sign(&signable_bytes(topic, &author, ts, &msg));
GossipPayload { author, ts, msg, sig }
GossipPayload {
author,
ts,
msg,
sig,
}
}
/// Why a received gossip payload was rejected (logging + tests).
@@ -233,7 +238,10 @@ impl ClockSkewMonitor {
}
fn drop_oldest_authors(&mut self) {
let remove_count = self.authors.len().saturating_sub(CLOCK_SKEW_AUTHORS_SOFT_CAP);
let remove_count = self
.authors
.len()
.saturating_sub(CLOCK_SKEW_AUTHORS_SOFT_CAP);
let mut by_age: Vec<_> = self
.authors
.iter()
@@ -339,7 +347,8 @@ fn verify_gossip(
return Err(GossipReject::OutOfWindow);
}
if let GossipMessage::Announce(state) = &payload.msg
&& state.addr.id != payload.author {
&& state.addr.id != payload.author
{
return Err(GossipReject::AnnounceAddressMismatch);
}
Ok(())
@@ -466,7 +475,8 @@ impl RoomState for IrohGossipState {
// Make every dial target resolvable: the ticket host plus any retained
// peers handed in (their addresses may have aged out of a fresh endpoint's
// book even though the persistent lookup usually still holds them).
self.address_lookup.add_endpoint_info(ticket.host_addr.clone());
self.address_lookup
.add_endpoint_info(ticket.host_addr.clone());
for addr in &extra_bootstrap {
self.address_lookup.add_endpoint_info(addr.clone());
}
@@ -478,7 +488,10 @@ impl RoomState for IrohGossipState {
compute_bootstrap(self_state.addr.id, ticket.host_addr.id, &extra_bootstrap);
crate::log_msg(&format!("Bootstrap peers for join: {:?}", bootstrap_peers));
let gossip_topic = self.gossip.subscribe(topic_id, bootstrap_peers).await
let gossip_topic = self
.gossip
.subscribe(topic_id, bootstrap_peers)
.await
.map_err(|e| {
let err = format!("Failed to join gossip topic: {}", e);
crate::log_msg(&err);
@@ -505,32 +518,44 @@ impl RoomState for IrohGossipState {
let topic_bytes = ticket.topic_id;
let handle = tokio::spawn(async move {
crate::log_msg(&format!("Spawned gossip topic loop for self_id={:?}", self_id));
crate::log_msg(&format!(
"Spawned gossip topic loop for self_id={:?}",
self_id
));
let mut state_mutations_seen = HashMap::new();
let mut clock_skew_monitor = ClockSkewMonitor::default();
// Broadcast initial state
let initial_payload = {
let guard = self_state_clone.lock().unwrap();
guard.as_ref().map(|s| sign_gossip(
&secret_key,
&topic_bytes,
now_millis(),
GossipMessage::Announce(s.clone()),
))
guard.as_ref().map(|s| {
sign_gossip(
&secret_key,
&topic_bytes,
now_millis(),
GossipMessage::Announce(s.clone()),
)
})
};
if let Some(payload) = initial_payload
&& let Ok(bytes) = serde_json::to_vec(&payload) {
crate::log_msg(&format!("Broadcasting initial state from self_id={:?}", self_id));
let _ = gossip_sender_clone.broadcast(bytes.into()).await;
}
&& let Ok(bytes) = serde_json::to_vec(&payload)
{
crate::log_msg(&format!(
"Broadcasting initial state from self_id={:?}",
self_id
));
let _ = gossip_sender_clone.broadcast(bytes.into()).await;
}
// Stream topic messages
while let Some(res) = gossip_receiver.next().await {
match res {
Ok(iroh_gossip::api::Event::Received(msg)) => {
crate::log_msg(&format!("Gossip received Event::Received from delivery={:?}", msg.delivered_from));
crate::log_msg(&format!(
"Gossip received Event::Received from delivery={:?}",
msg.delivered_from
));
// Reject oversized frames BEFORE deserializing: parsing
// allocates, so a size check has to precede `from_slice` to
// bound the memory a hostile peer can make us hold.
@@ -577,13 +602,13 @@ impl RoomState for IrohGossipState {
continue;
}
let our_id = {
self_state_clone.lock().unwrap()
.as_ref()
.map(|s| s.addr.id)
self_state_clone.lock().unwrap().as_ref().map(|s| s.addr.id)
};
if Some(payload.author) == our_id {
crate::log_msg("Gossip Event::Received from ourselves; ignoring");
crate::log_msg(
"Gossip Event::Received from ourselves; ignoring",
);
continue;
}
@@ -635,7 +660,9 @@ impl RoomState for IrohGossipState {
// Screen-share tickets are capabilities and
// peer-supplied: cap/validate once at ingest
// so invalid offers never render a Watch button.
state.sharing = state.sharing.and_then(crate::screenshare::sanitize_ticket);
state.sharing = state
.sharing
.and_then(crate::screenshare::sanitize_ticket);
// The game-presence label is untrusted
// peer text like the name: sanitize +
// length-cap at ingest (strip bidi/control,
@@ -651,8 +678,11 @@ impl RoomState for IrohGossipState {
// bounds a future fetch (reject anything
// outside the attachment cap).
state.music = state.music.and_then(|mut m| {
let name = crate::sanitize::sanitize_game_label(&m.name);
if name.is_empty() || !crate::files::size_within_cap(m.size) {
let name =
crate::sanitize::sanitize_game_label(&m.name);
if name.is_empty()
|| !crate::files::size_within_cap(m.size)
{
return None;
}
m.name = name;
@@ -679,8 +709,10 @@ impl RoomState for IrohGossipState {
// cleared ONLY once re-admitted — clearing it before
// a possible reject would orphan its recovery state
// (Tier C F-01 audit).
let is_reconnecting =
disconnected_peers.lock().unwrap().contains(&payload.author);
let is_reconnecting = disconnected_peers
.lock()
.unwrap()
.contains(&payload.author);
let admitted = {
let mut peer_map = peers.lock().unwrap();
let is_new = !peer_map.contains_key(&payload.author);
@@ -689,11 +721,17 @@ impl RoomState for IrohGossipState {
// memory/tasks/dials without bound (Tier C
// F-01). Existing-peer updates and reconnects
// are exempt; only brand-new authors are gated.
let subject_to_cap = announce_subject_to_cap(is_new, is_reconnecting);
if !admit_into_roster(peer_map.len(), subject_to_cap, MAX_ACTIVE_PEERS) {
let subject_to_cap =
announce_subject_to_cap(is_new, is_reconnecting);
if !admit_into_roster(
peer_map.len(),
subject_to_cap,
MAX_ACTIVE_PEERS,
) {
None
} else {
let state_changed = peer_map.get(&payload.author) != Some(&state);
let state_changed =
peer_map.get(&payload.author) != Some(&state);
if is_new || state_changed {
peer_map.insert(payload.author, state.clone());
}
@@ -723,35 +761,54 @@ impl RoomState for IrohGossipState {
// address set, so leave/re-announce cycles
// can't accumulate attacker-supplied history
// (Tier C F-01).
let _ = address_lookup.set_endpoint_info(state.addr.clone());
let _ = event_tx.send(RoomEvent::PeerJoined(payload.author, state)).await;
let _ = address_lookup
.set_endpoint_info(state.addr.clone());
let _ = event_tx
.send(RoomEvent::PeerJoined(payload.author, state))
.await;
} else if state_changed {
crate::log_msg(&format!(
"Gossip peer state updated: {}, state: {}",
crate::short_id(&payload.author.to_string()),
peer_state_for_log(&state)
));
let _ = event_tx.send(RoomEvent::PeerUpdated(payload.author, state)).await;
let _ = event_tx
.send(RoomEvent::PeerUpdated(payload.author, state))
.await;
}
}
GossipMessage::Leave => {
crate::log_msg(&format!("Gossip peer leave request from author={:?}", payload.author));
crate::log_msg(&format!(
"Gossip peer leave request from author={:?}",
payload.author
));
// Drop this id's address-lookup entry so cycling
// distinct identities through Announce→Leave can't
// grow the lookup for the room's lifetime (Tier C
// F-01 audit). Re-announce re-populates it.
let _ = address_lookup.remove_endpoint_info(payload.author);
let removed = peers.lock().unwrap().remove(&payload.author).is_some();
let removed =
peers.lock().unwrap().remove(&payload.author).is_some();
let was_disconnected = disconnected_peers
.lock()
.unwrap()
.remove(&payload.author);
if removed || was_disconnected {
let _ = event_tx.send(RoomEvent::PeerLeft(payload.author)).await;
let _ = event_tx
.send(RoomEvent::PeerLeft(payload.author))
.await;
}
}
GossipMessage::Chat { name, text, ts, attachment } => {
crate::log_msg(&format!("Gossip chat from author={:?}", payload.author));
GossipMessage::Chat {
name,
text,
ts,
attachment,
} => {
crate::log_msg(&format!(
"Gossip chat from author={:?}",
payload.author
));
// Defensively normalize an untrusted attachment
// descriptor: sanitize the filename and drop it
// entirely if it declares an out-of-cap size.
@@ -762,18 +819,23 @@ impl RoomState for IrohGossipState {
a.name = crate::files::sanitize_filename(&a.name);
Some(a)
});
let _ = event_tx.send(RoomEvent::ChatMessage {
from: payload.author,
name,
text,
ts,
attachment,
}).await;
let _ = event_tx
.send(RoomEvent::ChatMessage {
from: payload.author,
name,
text,
ts,
attachment,
})
.await;
}
}
}
Err(e) => {
crate::log_msg(&format!("Gossip failed to deserialize payload: {:?}", e));
crate::log_msg(&format!(
"Gossip failed to deserialize payload: {:?}",
e
));
}
}
}
@@ -782,18 +844,24 @@ impl RoomState for IrohGossipState {
// Resend state on new neighbor connection to guarantee synchronization
let payload_opt = {
let guard = self_state_clone.lock().unwrap();
guard.as_ref().map(|state| sign_gossip(
&secret_key,
&topic_bytes,
now_millis(),
GossipMessage::Announce(state.clone()),
))
guard.as_ref().map(|state| {
sign_gossip(
&secret_key,
&topic_bytes,
now_millis(),
GossipMessage::Announce(state.clone()),
)
})
};
if let Some(payload) = payload_opt
&& let Ok(bytes) = serde_json::to_vec(&payload) {
crate::log_msg(&format!("Broadcasting state to new neighbor={:?}", peer_id));
let _ = gossip_sender_clone.broadcast(bytes.into()).await;
}
&& let Ok(bytes) = serde_json::to_vec(&payload)
{
crate::log_msg(&format!(
"Broadcasting state to new neighbor={:?}",
peer_id
));
let _ = gossip_sender_clone.broadcast(bytes.into()).await;
}
}
Ok(iroh_gossip::api::Event::NeighborDown(peer_id)) => {
crate::log_msg(&format!("Gossip event: NeighborDown={:?}", peer_id));
@@ -807,7 +875,10 @@ impl RoomState for IrohGossipState {
let removed = peers.lock().unwrap().remove(&peer_id).is_some();
if removed {
disconnected_peers.lock().unwrap().insert(peer_id);
crate::log_msg(&format!("Peer connection lost (NeighborDown): {:?}", peer_id));
crate::log_msg(&format!(
"Peer connection lost (NeighborDown): {:?}",
peer_id
));
let _ = event_tx.send(RoomEvent::PeerConnectionLost(peer_id)).await;
}
}
@@ -844,7 +915,9 @@ impl RoomState for IrohGossipState {
);
if let Ok(bytes) = serde_json::to_vec(&payload) {
crate::log_msg("Broadcasting updated self state to gossip");
sender.broadcast(bytes.into()).await
sender
.broadcast(bytes.into())
.await
.map_err(|e| NetError::Gossip(e.to_string()))?;
}
}
@@ -909,10 +982,17 @@ impl RoomState for IrohGossipState {
&self.secret_key,
&topic,
ts,
GossipMessage::Chat { name, text, ts, attachment },
GossipMessage::Chat {
name,
text,
ts,
attachment,
},
);
if let Ok(bytes) = serde_json::to_vec(&payload) {
sender.broadcast(bytes.into()).await
sender
.broadcast(bytes.into())
.await
.map_err(|e| NetError::Gossip(e.to_string()))?;
}
}
@@ -934,12 +1014,7 @@ impl RoomState for IrohGossipState {
let sender_opt = self.active_sender.lock().unwrap().take();
if let (Some(sender), Some(topic)) = (sender_opt, topic_opt) {
let payload = sign_gossip(
&self.secret_key,
&topic,
now_millis(),
GossipMessage::Leave,
);
let payload = sign_gossip(&self.secret_key, &topic, now_millis(), GossipMessage::Leave);
if let Ok(bytes) = serde_json::to_vec(&payload) {
crate::log_msg("Broadcasting Leave message to gossip");
let _ = sender.broadcast(bytes.into()).await;
@@ -1060,7 +1135,9 @@ mod tests {
#[test]
fn reconnecting_and_existing_peers_are_exempt_from_the_cap() {
// A brand-new author counts against the cap...
assert!(announce_subject_to_cap(/* is_new */ true, /* is_reconnecting */ false));
assert!(announce_subject_to_cap(
/* is_new */ true, /* is_reconnecting */ false
));
// ...but an ordinary update from an in-roster peer does not...
assert!(!announce_subject_to_cap(false, false));
// ...and neither does a re-announce from a peer mid-reconnect, even
@@ -1070,7 +1147,11 @@ mod tests {
// Combined with admit_into_roster: a reconnecting author passes at a full
// roster, a brand-new one does not.
assert!(admit_into_roster(3, announce_subject_to_cap(true, true), 3));
assert!(!admit_into_roster(3, announce_subject_to_cap(true, false), 3));
assert!(!admit_into_roster(
3,
announce_subject_to_cap(true, false),
3
));
}
#[test]
@@ -1105,7 +1186,10 @@ mod tests {
assert_eq!(monitor.observe(author, -122_000, 40_000), None);
assert_eq!(
monitor.observe(author, -123_000, 69_999),
Some(ClockSkewWarning { author, skew_ms: -123_000 })
Some(ClockSkewWarning {
author,
skew_ms: -123_000
})
);
assert_eq!(monitor.observe(author, -124_000, 70_000), None);
}
@@ -1125,7 +1209,10 @@ mod tests {
assert_eq!(monitor.observe(author, 127_000, 319_999), None);
assert_eq!(
monitor.observe(author, 128_000, 320_000),
Some(ClockSkewWarning { author, skew_ms: 128_000 })
Some(ClockSkewWarning {
author,
skew_ms: 128_000
})
);
}
@@ -1141,11 +1228,17 @@ mod tests {
assert_eq!(monitor.observe(b, 121_000, 1_000), None);
assert_eq!(
monitor.observe(b, 121_000, 2_000),
Some(ClockSkewWarning { author: b, skew_ms: 121_000 })
Some(ClockSkewWarning {
author: b,
skew_ms: 121_000
})
);
assert_eq!(
monitor.observe(a, -121_000, 2_000),
Some(ClockSkewWarning { author: a, skew_ms: -121_000 })
Some(ClockSkewWarning {
author: a,
skew_ms: -121_000
})
);
}
@@ -1202,7 +1295,10 @@ mod tests {
assert!(long.as_str().len() > MAX_RELAY_URL_LEN);
let addr = EndpointAddr::from_parts(
id,
[TransportAddr::Relay(short.clone()), TransportAddr::Relay(long)],
[
TransportAddr::Relay(short.clone()),
TransportAddr::Relay(long),
],
);
let out = sanitize_endpoint_addr(&addr);
let relays: Vec<_> = out.relay_urls().cloned().collect();
@@ -1222,7 +1318,12 @@ mod tests {
let secret = SecretKey::generate();
let topic = [9u8; 32];
let peer_state = sample_peer_state_for(secret.public());
let payload = sign_gossip(&secret, &topic, 1000, GossipMessage::Announce(peer_state.clone()));
let payload = sign_gossip(
&secret,
&topic,
1000,
GossipMessage::Announce(peer_state.clone()),
);
let serialized = serde_json::to_string(&payload).unwrap();
let deserialized: GossipPayload = serde_json::from_str(&serialized).unwrap();
@@ -1252,7 +1353,13 @@ mod tests {
};
let serialized = serde_json::to_string(&original).unwrap();
let deserialized: GossipMessage = serde_json::from_str(&serialized).unwrap();
if let GossipMessage::Chat { name, text, ts, attachment } = deserialized {
if let GossipMessage::Chat {
name,
text,
ts,
attachment,
} = deserialized
{
assert_eq!(name, "Alice");
assert_eq!(text, "Hello");
assert_eq!(ts, 123456789);
@@ -1305,7 +1412,10 @@ mod tests {
// defaulting the attachment to None (serde(default)).
let legacy = r#"{"Chat":{"name":"Old","text":"hi","ts":7}}"#;
let parsed: GossipMessage = serde_json::from_str(legacy).unwrap();
if let GossipMessage::Chat { name, attachment, .. } = parsed {
if let GossipMessage::Chat {
name, attachment, ..
} = parsed
{
assert_eq!(name, "Old");
assert_eq!(attachment, None);
} else {
@@ -1368,7 +1478,10 @@ mod tests {
let secret = SecretKey::generate();
let topic = [1u8; 32];
let p = sign_gossip(&secret, &topic, 5_000, GossipMessage::Leave);
assert_eq!(verify_gossip(&p, &topic, 5_000, GOSSIP_FRESHNESS_MS), Ok(()));
assert_eq!(
verify_gossip(&p, &topic, 5_000, GOSSIP_FRESHNESS_MS),
Ok(())
);
}
#[test]
@@ -1392,7 +1505,12 @@ mod tests {
let secret = SecretKey::generate();
let topic = [4u8; 32];
let mut p = sign_gossip(&secret, &topic, 5_000, GossipMessage::Leave);
p.msg = GossipMessage::Chat { name: "x".into(), text: "y".into(), ts: 5_000, attachment: None };
p.msg = GossipMessage::Chat {
name: "x".into(),
text: "y".into(),
ts: 5_000,
attachment: None,
};
assert_eq!(
verify_gossip(&p, &topic, 5_000, GOSSIP_FRESHNESS_MS),
Err(GossipReject::BadSignature)
@@ -1418,16 +1536,34 @@ mod tests {
let p = sign_gossip(&secret, &topic, 1_000_000, GossipMessage::Leave);
// Far in the past relative to "now" → stale (replay).
assert_eq!(
verify_gossip(&p, &topic, 1_000_000 + GOSSIP_FRESHNESS_MS + 1, GOSSIP_FRESHNESS_MS),
verify_gossip(
&p,
&topic,
1_000_000 + GOSSIP_FRESHNESS_MS + 1,
GOSSIP_FRESHNESS_MS
),
Err(GossipReject::OutOfWindow)
);
// Implausibly future.
assert_eq!(
verify_gossip(&p, &topic, 1_000_000 - GOSSIP_FRESHNESS_MS - 1, GOSSIP_FRESHNESS_MS),
verify_gossip(
&p,
&topic,
1_000_000 - GOSSIP_FRESHNESS_MS - 1,
GOSSIP_FRESHNESS_MS
),
Err(GossipReject::OutOfWindow)
);
// Within the window (clock skew tolerance) → accepted.
assert!(verify_gossip(&p, &topic, 1_000_000 + GOSSIP_FRESHNESS_MS - 1, GOSSIP_FRESHNESS_MS).is_ok());
assert!(
verify_gossip(
&p,
&topic,
1_000_000 + GOSSIP_FRESHNESS_MS - 1,
GOSSIP_FRESHNESS_MS
)
.is_ok()
);
}
#[test]
@@ -1449,10 +1585,30 @@ mod tests {
let author = fresh_id();
let mut seen = HashMap::new();
assert!(admit_state_mutation(&mut seen, author, &GossipMessage::Leave, 10));
assert!(!admit_state_mutation(&mut seen, author, &GossipMessage::Leave, 10));
assert!(!admit_state_mutation(&mut seen, author, &GossipMessage::Leave, 9));
assert!(admit_state_mutation(&mut seen, author, &GossipMessage::Leave, 11));
assert!(admit_state_mutation(
&mut seen,
author,
&GossipMessage::Leave,
10
));
assert!(!admit_state_mutation(
&mut seen,
author,
&GossipMessage::Leave,
10
));
assert!(!admit_state_mutation(
&mut seen,
author,
&GossipMessage::Leave,
9
));
assert!(admit_state_mutation(
&mut seen,
author,
&GossipMessage::Leave,
11
));
let announce = GossipMessage::Announce(sample_peer_state_for(author));
assert!(admit_state_mutation(&mut seen, author, &announce, 10));
@@ -1465,13 +1621,26 @@ mod tests {
fn state_mutation_replay_gate_leaves_chat_ordering_untouched() {
let author = fresh_id();
let mut seen = HashMap::new();
let later_chat = GossipMessage::Chat { name: "A".into(), text: "later".into(), ts: 200, attachment: None };
let earlier_chat = GossipMessage::Chat { name: "A".into(), text: "earlier".into(), ts: 100, attachment: None };
let later_chat = GossipMessage::Chat {
name: "A".into(),
text: "later".into(),
ts: 200,
attachment: None,
};
let earlier_chat = GossipMessage::Chat {
name: "A".into(),
text: "earlier".into(),
ts: 100,
attachment: None,
};
assert!(admit_state_mutation(&mut seen, author, &later_chat, 200));
assert!(admit_state_mutation(&mut seen, author, &earlier_chat, 100));
assert!(admit_state_mutation(&mut seen, author, &later_chat, 200));
assert!(seen.is_empty(), "chat must not populate the state-mutation replay map");
assert!(
seen.is_empty(),
"chat must not populate the state-mutation replay map"
);
}
#[test]
@@ -1481,8 +1650,18 @@ mod tests {
let mut seen = HashMap::new();
let announce = GossipMessage::Announce(sample_peer_state_for(author));
assert!(admit_state_mutation(&mut seen, author, &GossipMessage::Leave, 5));
assert!(admit_state_mutation(
&mut seen,
author,
&GossipMessage::Leave,
5
));
assert!(admit_state_mutation(&mut seen, author, &announce, 5));
assert!(admit_state_mutation(&mut seen, other, &GossipMessage::Leave, 5));
assert!(admit_state_mutation(
&mut seen,
other,
&GossipMessage::Leave,
5
));
}
}
+71 -26
View File
@@ -1,16 +1,16 @@
use crate::network::{NetworkTransport, NetError, ConnEvent};
use iroh::{Endpoint, EndpointId};
use iroh::endpoint::{Connection, ConnectionError, VarInt};
use crate::network::{ConnEvent, NetError, NetworkTransport};
use async_trait::async_trait;
use bytes::Bytes;
use iroh::endpoint::{Connection, ConnectionError, VarInt};
use iroh::{Endpoint, EndpointId};
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex as StdMutex};
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::sync::mpsc::Receiver;
use std::sync::{Arc, Mutex as StdMutex};
use std::collections::{HashMap, HashSet};
use std::time::Duration;
use async_trait::async_trait;
use crate::protocol::{AUDIO_ALPN, FILES_ALPN};
use crate::files::{AttachmentId, ChatAttachment};
use crate::protocol::{AUDIO_ALPN, FILES_ALPN};
/// Per-peer datagram send queue depth. Audio is real-time, so a backlog is
/// useless latency — keep it shallow and drop the oldest frame when full.
@@ -111,7 +111,13 @@ impl Shared {
let shared = self.clone();
let supervisor = tokio::spawn(supervise(shared, peer_id, inbound_rx));
let inbound_tx_ret = inbound_tx.clone();
peers.insert(peer_id, PeerHandle { supervisor, inbound_tx });
peers.insert(
peer_id,
PeerHandle {
supervisor,
inbound_tx,
},
);
crate::log_msg(&format!("Transport: supervising peer {:?}", peer_id));
inbound_tx_ret
}
@@ -123,7 +129,10 @@ impl Shared {
self.addrs.lock().unwrap().remove(&peer_id);
if let Some(handle) = self.peers.lock().await.remove(&peer_id) {
handle.supervisor.abort();
crate::log_msg(&format!("Transport: stopped supervising peer {:?}", peer_id));
crate::log_msg(&format!(
"Transport: stopped supervising peer {:?}",
peer_id
));
}
}
@@ -208,12 +217,15 @@ async fn supervise(
let mut backoff = INITIAL_BACKOFF;
// Show "connecting" until the first link is actually up.
let _ = shared.conn_events_tx.try_send(ConnEvent::Connecting(peer_id));
let _ = shared
.conn_events_tx
.try_send(ConnEvent::Connecting(peer_id));
let mut conn = match obtain_conn(&shared, peer_id, is_dialer, &mut inbound_rx, &mut backoff).await {
Some(conn) => conn,
None => return, // retired before we ever connected
};
let mut conn =
match obtain_conn(&shared, peer_id, is_dialer, &mut inbound_rx, &mut backoff).await {
Some(conn) => conn,
None => return, // retired before we ever connected
};
loop {
// A healthy link resets the dialer's backoff for the next outage.
@@ -223,8 +235,14 @@ async fn supervise(
shared.senders.lock().unwrap().insert(peer_id, send_tx);
// Publish the live connection so an intentional leave can close it with
// the goodbye code.
shared.live_conns.lock().unwrap().insert(peer_id, conn.clone());
let _ = shared.conn_events_tx.try_send(ConnEvent::Connected(peer_id));
shared
.live_conns
.lock()
.unwrap()
.insert(peer_id, conn.clone());
let _ = shared
.conn_events_tx
.try_send(ConnEvent::Connected(peer_id));
crate::log_msg(&format!("Transport: peer {:?} link up", peer_id));
// Run until the link dies, a replacement arrives, or we're retired. The
@@ -272,21 +290,36 @@ async fn supervise(
match wake {
Wake::Shutdown => return,
Wake::Replacement(new_conn) => {
crate::log_msg(&format!("Transport: peer {:?} replaced with new inbound link", peer_id));
let _ = shared.conn_events_tx.try_send(ConnEvent::Connecting(peer_id));
crate::log_msg(&format!(
"Transport: peer {:?} replaced with new inbound link",
peer_id
));
let _ = shared
.conn_events_tx
.try_send(ConnEvent::Connecting(peer_id));
conn = new_conn;
}
Wake::Closed(reason) => {
// A graceful application close means the peer left on purpose —
// don't reconnect; tell the core to evict it now.
if is_graceful_leave(&reason) {
crate::log_msg(&format!("Transport: peer {:?} left gracefully ({:?})", peer_id, reason));
crate::log_msg(&format!(
"Transport: peer {:?} left gracefully ({:?})",
peer_id, reason
));
let _ = shared.conn_events_tx.try_send(ConnEvent::Left(peer_id));
return;
}
crate::log_msg(&format!("Transport: peer {:?} link dropped; reconnecting", peer_id));
let _ = shared.conn_events_tx.try_send(ConnEvent::Connecting(peer_id));
conn = match obtain_conn(&shared, peer_id, is_dialer, &mut inbound_rx, &mut backoff).await {
crate::log_msg(&format!(
"Transport: peer {:?} link dropped; reconnecting",
peer_id
));
let _ = shared
.conn_events_tx
.try_send(ConnEvent::Connecting(peer_id));
conn = match obtain_conn(&shared, peer_id, is_dialer, &mut inbound_rx, &mut backoff)
.await
{
Some(conn) => conn,
None => return, // retired while reconnecting
};
@@ -405,7 +438,10 @@ impl iroh::protocol::ProtocolHandler for AudioRouter {
// only happens if links are churning, and the supervisor gets the next one.
let inbound_tx = shared.ensure_supervisor(peer_id).await;
if inbound_tx.try_send(connection).is_err() {
crate::log_msg(&format!("Transport: dropped inbound link from {:?} (queue full)", peer_id));
crate::log_msg(&format!(
"Transport: dropped inbound link from {:?} (queue full)",
peer_id
));
}
Ok(())
}
@@ -545,7 +581,14 @@ impl IrohTransport {
/// all supervisors so none linger redialing the about-to-close endpoint.
/// Call this before shutting the router down.
pub async fn leave(&self) {
let conns: Vec<Connection> = self.shared.live_conns.lock().unwrap().drain().map(|(_, c)| c).collect();
let conns: Vec<Connection> = self
.shared
.live_conns
.lock()
.unwrap()
.drain()
.map(|(_, c)| c)
.collect();
for conn in &conns {
conn.close(VarInt::from_u32(GOODBYE_CODE), b"leave");
}
@@ -639,7 +682,9 @@ impl IrohTransport {
.map_err(|_| NetError::Other("file fetch: read timed out".to_string()))?
.map_err(|e| NetError::Other(format!("file fetch: read failed: {e}")))?;
if bytes.is_empty() {
return Err(NetError::Other("file fetch: sender no longer has the file".to_string()));
return Err(NetError::Other(
"file fetch: sender no longer has the file".to_string(),
));
}
Ok(bytes)
}
+53 -29
View File
@@ -1,10 +1,10 @@
use iroh::{EndpointId, EndpointAddr};
use async_trait::async_trait;
use bytes::Bytes;
use iroh::{EndpointAddr, EndpointId};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use thiserror::Error;
use tokio::sync::mpsc::Receiver;
use async_trait::async_trait;
use serde::{Serialize, Deserialize};
use std::str::FromStr;
#[derive(Error, Debug)]
pub enum NetError {
@@ -148,7 +148,10 @@ pub enum RoomEvent {
/// A validly signed gossip payload was rejected only because its timestamp is
/// outside the replay-protection window. The peer is not in the roster yet,
/// so this surfaces as a room-level warning instead of a peer-card state.
ClockSkewSuspected { author: EndpointId, skew_ms: i64 },
ClockSkewSuspected {
author: EndpointId,
skew_ms: i64,
},
/// A peer sent a room text-chat message. Carries the sender's id, their
/// display name (embedded so it shows even without a presence entry), the
/// text, and a sender-stamped millisecond timestamp.
@@ -203,10 +206,12 @@ impl PeerSpeakTicket {
/// is idempotent.
pub fn restamp(ticket_str: &str, my_addr: iroh::EndpointAddr) -> String {
match ticket_str.parse::<PeerSpeakTicket>() {
Ok(t) => {
PeerSpeakTicket { host_addr: my_addr, topic_id: t.topic_id, name: t.name }
.to_string()
Ok(t) => PeerSpeakTicket {
host_addr: my_addr,
topic_id: t.topic_id,
name: t.name,
}
.to_string(),
Err(_) => ticket_str.to_string(),
}
}
@@ -215,7 +220,10 @@ impl PeerSpeakTicket {
/// can't be parsed or carries no label. Pure; used to label the gathering both
/// in the room UI and in the presence we report to friends.
pub fn label_of(ticket_str: &str) -> String {
ticket_str.parse::<PeerSpeakTicket>().map(|t| t.name).unwrap_or_default()
ticket_str
.parse::<PeerSpeakTicket>()
.map(|t| t.name)
.unwrap_or_default()
}
/// The room's `topic_id` embedded in a ticket string, or `None` if the ticket
@@ -223,7 +231,10 @@ impl PeerSpeakTicket {
/// the recents list (the host address and label change between members/sessions,
/// but the topic uniquely identifies the gathering).
pub fn topic_of(ticket_str: &str) -> Option<[u8; 32]> {
ticket_str.parse::<PeerSpeakTicket>().ok().map(|t| t.topic_id)
ticket_str
.parse::<PeerSpeakTicket>()
.ok()
.map(|t| t.topic_id)
}
}
@@ -246,8 +257,8 @@ impl FromStr for PeerSpeakTicket {
fn from_str(s: &str) -> Result<Self, Self::Err> {
let decoded = base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, s)
.map_err(|e| NetError::InvalidTicket(e.to_string()))?;
let ticket: PeerSpeakTicket = serde_json::from_slice(&decoded)
.map_err(|e| NetError::InvalidTicket(e.to_string()))?;
let ticket: PeerSpeakTicket =
serde_json::from_slice(&decoded).map_err(|e| NetError::InvalidTicket(e.to_string()))?;
Ok(ticket)
}
}
@@ -327,13 +338,13 @@ pub trait RoomState: Send + Sync {
async fn subscribe_events(&self) -> Result<Receiver<RoomEvent>, NetError>;
}
pub mod iroh_impl;
pub mod gossip;
pub mod iroh_impl;
#[cfg(test)]
mod tests {
use super::*;
use iroh::{SecretKey, EndpointAddr};
use iroh::{EndpointAddr, SecretKey};
fn sample_peer_state() -> PeerState {
let secret = SecretKey::generate();
@@ -371,9 +382,12 @@ mod tests {
let host = SecretKey::generate().public();
let topic_id = [3u8; 32];
// A labelled ticket: restamp keeps the label, label_of reads it.
let labelled =
PeerSpeakTicket { host_addr: EndpointAddr::from(host), topic_id, name: "HangOut".into() }
.to_string();
let labelled = PeerSpeakTicket {
host_addr: EndpointAddr::from(host),
topic_id,
name: "HangOut".into(),
}
.to_string();
assert_eq!(PeerSpeakTicket::label_of(&labelled), "HangOut");
let member = SecretKey::generate().public();
let restamped = PeerSpeakTicket::restamp(&labelled, EndpointAddr::from(member));
@@ -410,10 +424,8 @@ mod tests {
// valid URL-safe-base64 that decodes to non-JSON bytes
let bad_json = b"hello world";
let encoded = base64::Engine::encode(
&base64::engine::general_purpose::URL_SAFE_NO_PAD,
bad_json,
);
let encoded =
base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, bad_json);
let res3 = encoded.parse::<PeerSpeakTicket>();
assert!(matches!(res3, Err(NetError::InvalidTicket(_))));
}
@@ -424,9 +436,12 @@ mod tests {
let host = SecretKey::generate().public();
let member = SecretKey::generate().public();
let topic_id = [42u8; 32];
let original =
PeerSpeakTicket { host_addr: EndpointAddr::from(host), topic_id, name: "HangOut".into() }
.to_string();
let original = PeerSpeakTicket {
host_addr: EndpointAddr::from(host),
topic_id,
name: "HangOut".into(),
}
.to_string();
let restamped_str = PeerSpeakTicket::restamp(&original, EndpointAddr::from(member));
let restamped = restamped_str.parse::<PeerSpeakTicket>().unwrap();
@@ -441,18 +456,27 @@ mod tests {
fn test_restamp_is_idempotent_for_same_addr() {
let me = SecretKey::generate().public();
let topic_id = [7u8; 32];
let mine =
PeerSpeakTicket { host_addr: EndpointAddr::from(me), topic_id, name: String::new() }
.to_string();
let mine = PeerSpeakTicket {
host_addr: EndpointAddr::from(me),
topic_id,
name: String::new(),
}
.to_string();
// Re-stamping my own ticket with my own addr changes nothing.
assert_eq!(PeerSpeakTicket::restamp(&mine, EndpointAddr::from(me)), mine);
assert_eq!(
PeerSpeakTicket::restamp(&mine, EndpointAddr::from(me)),
mine
);
}
#[test]
fn test_restamp_passes_through_unparseable() {
let me = SecretKey::generate().public();
// A malformed ticket is returned unchanged (the join will fail anyway).
assert_eq!(PeerSpeakTicket::restamp("not-a-ticket", EndpointAddr::from(me)), "not-a-ticket");
assert_eq!(
PeerSpeakTicket::restamp("not-a-ticket", EndpointAddr::from(me)),
"not-a-ticket"
);
}
#[test]