fix(peer): charge discovery quota to packet origin
Carry the mDNS response source independently of its advertised A or AAAA target. Active and cooling candidates now charge the observed host, so rotating peer IDs, ports, and target addresses cannot escape the eight-candidate origin budget. Test Plan: - just test - just clippy - rotating-advertised-target regression - git diff --check
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
//! mDNS peer discovery and discovery-time protocol negotiation.
|
//! mDNS peer discovery and discovery-time protocol negotiation.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashSet, VecDeque},
|
collections::{HashMap, VecDeque},
|
||||||
future::Future,
|
future::Future,
|
||||||
thread::JoinHandle,
|
thread::JoinHandle,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
@@ -11,10 +11,7 @@ use eyre::WrapErr as _;
|
|||||||
use futures::{StreamExt as _, stream::FuturesUnordered};
|
use futures::{StreamExt as _, stream::FuturesUnordered};
|
||||||
use lanspread_mdns::{LANSPREAD_SERVICE_TYPE, MdnsBrowser, MdnsService, MdnsServicePoll};
|
use lanspread_mdns::{LANSPREAD_SERVICE_TYPE, MdnsBrowser, MdnsService, MdnsServicePoll};
|
||||||
use lanspread_proto::{PROTOCOL_VERSION, PeerEndpoint, PeerId};
|
use lanspread_proto::{PROTOCOL_VERSION, PeerEndpoint, PeerId};
|
||||||
use tokio::sync::{
|
use tokio::sync::{mpsc, oneshot};
|
||||||
mpsc,
|
|
||||||
oneshot,
|
|
||||||
};
|
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -29,30 +26,35 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MAX_ACTIVE_DISCOVERY_CANDIDATES: usize = 64;
|
const MAX_ACTIVE_DISCOVERY_CANDIDATES: usize = 64;
|
||||||
/// Upper bound on active-or-cooling candidates advertised from one source IP.
|
/// Upper bound on active-or-cooling candidates observed from one response IP.
|
||||||
/// mDNS lets a single LAN host claim arbitrarily many peer IDs and ports, so
|
/// mDNS lets a single LAN host claim arbitrarily many target addresses, peer
|
||||||
/// without this bound one host could occupy every discovery slot and starve
|
/// IDs, and ports, so accounting must use packet provenance rather than the
|
||||||
/// genuinely new peers. Legitimate hosts run one peer, occasionally a few.
|
/// advertised A/AAAA record. Legitimate hosts run one peer, occasionally a few.
|
||||||
const MAX_DISCOVERY_CANDIDATES_PER_SOURCE_IP: usize = 8;
|
const MAX_DISCOVERY_CANDIDATES_PER_SOURCE_IP: usize = 8;
|
||||||
const MAX_PENDING_MDNS_SERVICES: usize = 64;
|
const MAX_PENDING_MDNS_SERVICES: usize = 64;
|
||||||
const DISCOVERY_CANDIDATE_COOLDOWN: Duration = Duration::from_secs(5);
|
const DISCOVERY_CANDIDATE_COOLDOWN: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct RecentCandidates {
|
struct RecentCandidates {
|
||||||
entries: VecDeque<(PeerEndpoint, tokio::time::Instant)>,
|
entries: VecDeque<(PeerEndpoint, std::net::IpAddr, tokio::time::Instant)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecentCandidates {
|
impl RecentCandidates {
|
||||||
fn try_record(&mut self, candidate: PeerEndpoint, now: tokio::time::Instant) -> bool {
|
fn try_record(
|
||||||
|
&mut self,
|
||||||
|
candidate: PeerEndpoint,
|
||||||
|
source_ip: std::net::IpAddr,
|
||||||
|
now: tokio::time::Instant,
|
||||||
|
) -> bool {
|
||||||
self.expire(now);
|
self.expire(now);
|
||||||
if self.entries.iter().any(|(endpoint, _)| {
|
if self.entries.iter().any(|(endpoint, _, _)| {
|
||||||
endpoint.peer_id == candidate.peer_id || endpoint.addr == candidate.addr
|
endpoint.peer_id == candidate.peer_id || endpoint.addr == candidate.addr
|
||||||
}) || self.entries.len() >= MAX_ACTIVE_DISCOVERY_CANDIDATES
|
}) || self.entries.len() >= MAX_ACTIVE_DISCOVERY_CANDIDATES
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
self.entries
|
self.entries
|
||||||
.push_back((candidate, now + DISCOVERY_CANDIDATE_COOLDOWN));
|
.push_back((candidate, source_ip, now + DISCOVERY_CANDIDATE_COOLDOWN));
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +62,7 @@ impl RecentCandidates {
|
|||||||
while self
|
while self
|
||||||
.entries
|
.entries
|
||||||
.front()
|
.front()
|
||||||
.is_some_and(|(_, deadline)| *deadline <= now)
|
.is_some_and(|(_, _, deadline)| *deadline <= now)
|
||||||
{
|
{
|
||||||
self.entries.pop_front();
|
self.entries.pop_front();
|
||||||
}
|
}
|
||||||
@@ -71,13 +73,13 @@ impl RecentCandidates {
|
|||||||
fn count_from_ip_outside(
|
fn count_from_ip_outside(
|
||||||
&mut self,
|
&mut self,
|
||||||
ip: std::net::IpAddr,
|
ip: std::net::IpAddr,
|
||||||
active: &HashSet<PeerEndpoint>,
|
active: &HashMap<PeerEndpoint, std::net::IpAddr>,
|
||||||
now: tokio::time::Instant,
|
now: tokio::time::Instant,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
self.expire(now);
|
self.expire(now);
|
||||||
self.entries
|
self.entries
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(endpoint, _)| endpoint.addr.ip() == ip && !active.contains(endpoint))
|
.filter(|(endpoint, source_ip, _)| *source_ip == ip && !active.contains_key(endpoint))
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +91,7 @@ impl RecentCandidates {
|
|||||||
|
|
||||||
struct MdnsPeerInfo {
|
struct MdnsPeerInfo {
|
||||||
addr: std::net::SocketAddr,
|
addr: std::net::SocketAddr,
|
||||||
|
source_ip: std::net::IpAddr,
|
||||||
peer_id: Option<PeerId>,
|
peer_id: Option<PeerId>,
|
||||||
proto_ver: Option<u32>,
|
proto_ver: Option<u32>,
|
||||||
}
|
}
|
||||||
@@ -203,7 +206,7 @@ pub async fn run_peer_discovery(
|
|||||||
let service_shutdown = ctx.shutdown.child_token();
|
let service_shutdown = ctx.shutdown.child_token();
|
||||||
let mut worker = DiscoveryWorker::spawn(service_type, service_tx, service_shutdown.clone())?;
|
let mut worker = DiscoveryWorker::spawn(service_type, service_tx, service_shutdown.clone())?;
|
||||||
let mut negotiations = FuturesUnordered::new();
|
let mut negotiations = FuturesUnordered::new();
|
||||||
let mut active_candidates = HashSet::new();
|
let mut active_candidates = HashMap::new();
|
||||||
let mut recent_candidates = RecentCandidates::default();
|
let mut recent_candidates = RecentCandidates::default();
|
||||||
let mut mismatch_emitted = false;
|
let mut mismatch_emitted = false;
|
||||||
let mut state_sync = Box::pin(run_state_sync(
|
let mut state_sync = Box::pin(run_state_sync(
|
||||||
@@ -256,6 +259,7 @@ pub async fn run_peer_discovery(
|
|||||||
&active_candidates,
|
&active_candidates,
|
||||||
&mut recent_candidates,
|
&mut recent_candidates,
|
||||||
endpoint,
|
endpoint,
|
||||||
|
info.source_ip,
|
||||||
tokio::time::Instant::now(),
|
tokio::time::Instant::now(),
|
||||||
) {
|
) {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
@@ -273,7 +277,7 @@ pub async fn run_peer_discovery(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
active_candidates.insert(endpoint);
|
active_candidates.insert(endpoint, info.source_ip);
|
||||||
negotiations.push(run_protocol_negotiation(ProtocolNegotiation {
|
negotiations.push(run_protocol_negotiation(ProtocolNegotiation {
|
||||||
endpoint,
|
endpoint,
|
||||||
handshake,
|
handshake,
|
||||||
@@ -312,39 +316,42 @@ pub async fn run_peer_discovery(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn candidate_conflicts(active: &HashSet<PeerEndpoint>, candidate: PeerEndpoint) -> bool {
|
fn candidate_conflicts(
|
||||||
|
active: &HashMap<PeerEndpoint, std::net::IpAddr>,
|
||||||
|
candidate: PeerEndpoint,
|
||||||
|
) -> bool {
|
||||||
active
|
active
|
||||||
.iter()
|
.keys()
|
||||||
.any(|endpoint| endpoint.peer_id == candidate.peer_id || endpoint.addr == candidate.addr)
|
.any(|endpoint| endpoint.peer_id == candidate.peer_id || endpoint.addr == candidate.addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether admitting `candidate` would exceed the per-source-IP
|
/// Returns whether admitting `candidate` would exceed the per-source-IP
|
||||||
/// discovery budget across active negotiations and cooling-down attempts.
|
/// discovery budget across active negotiations and cooling-down attempts.
|
||||||
fn source_ip_is_saturated(
|
fn source_ip_is_saturated(
|
||||||
active: &HashSet<PeerEndpoint>,
|
active: &HashMap<PeerEndpoint, std::net::IpAddr>,
|
||||||
recent: &mut RecentCandidates,
|
recent: &mut RecentCandidates,
|
||||||
candidate: PeerEndpoint,
|
source_ip: std::net::IpAddr,
|
||||||
now: tokio::time::Instant,
|
now: tokio::time::Instant,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let ip = candidate.addr.ip();
|
|
||||||
let active_from_ip = active
|
let active_from_ip = active
|
||||||
.iter()
|
.values()
|
||||||
.filter(|endpoint| endpoint.addr.ip() == ip)
|
.filter(|observed_ip| **observed_ip == source_ip)
|
||||||
.count();
|
.count();
|
||||||
active_from_ip + recent.count_from_ip_outside(ip, active, now)
|
active_from_ip + recent.count_from_ip_outside(source_ip, active, now)
|
||||||
>= MAX_DISCOVERY_CANDIDATES_PER_SOURCE_IP
|
>= MAX_DISCOVERY_CANDIDATES_PER_SOURCE_IP
|
||||||
}
|
}
|
||||||
|
|
||||||
fn candidate_is_admissible(
|
fn candidate_is_admissible(
|
||||||
active: &HashSet<PeerEndpoint>,
|
active: &HashMap<PeerEndpoint, std::net::IpAddr>,
|
||||||
recent: &mut RecentCandidates,
|
recent: &mut RecentCandidates,
|
||||||
candidate: PeerEndpoint,
|
candidate: PeerEndpoint,
|
||||||
|
source_ip: std::net::IpAddr,
|
||||||
now: tokio::time::Instant,
|
now: tokio::time::Instant,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
active.len() < MAX_ACTIVE_DISCOVERY_CANDIDATES
|
active.len() < MAX_ACTIVE_DISCOVERY_CANDIDATES
|
||||||
&& !candidate_conflicts(active, candidate)
|
&& !candidate_conflicts(active, candidate)
|
||||||
&& !source_ip_is_saturated(active, recent, candidate, now)
|
&& !source_ip_is_saturated(active, recent, source_ip, now)
|
||||||
&& recent.try_record(candidate, now)
|
&& recent.try_record(candidate, source_ip, now)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_mdns_browser(
|
fn run_mdns_browser(
|
||||||
@@ -409,6 +416,7 @@ async fn wait_for_local_peer_addr(ctx: &NetworkServiceCtx) -> bool {
|
|||||||
fn parse_mdns_peer(service: &MdnsService) -> MdnsPeerInfo {
|
fn parse_mdns_peer(service: &MdnsService) -> MdnsPeerInfo {
|
||||||
MdnsPeerInfo {
|
MdnsPeerInfo {
|
||||||
addr: service.addr,
|
addr: service.addr,
|
||||||
|
source_ip: service.source_ip,
|
||||||
peer_id: service
|
peer_id: service
|
||||||
.properties
|
.properties
|
||||||
.get("peer_id")
|
.get("peer_id")
|
||||||
@@ -496,8 +504,8 @@ where
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashMap,
|
||||||
net::SocketAddr,
|
net::{IpAddr, SocketAddr},
|
||||||
sync::{
|
sync::{
|
||||||
Arc,
|
Arc,
|
||||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||||
@@ -562,6 +570,7 @@ mod tests {
|
|||||||
let peer_id = Some(PeerId::from_bytes([7; 32]));
|
let peer_id = Some(PeerId::from_bytes([7; 32]));
|
||||||
let unicast = MdnsPeerInfo {
|
let unicast = MdnsPeerInfo {
|
||||||
addr: "192.168.1.50:42424".parse().expect("test address parses"),
|
addr: "192.168.1.50:42424".parse().expect("test address parses"),
|
||||||
|
source_ip: "192.168.1.50".parse().expect("test source parses"),
|
||||||
peer_id,
|
peer_id,
|
||||||
proto_ver: Some(lanspread_proto::PROTOCOL_VERSION),
|
proto_ver: Some(lanspread_proto::PROTOCOL_VERSION),
|
||||||
};
|
};
|
||||||
@@ -569,6 +578,7 @@ mod tests {
|
|||||||
|
|
||||||
let multicast = MdnsPeerInfo {
|
let multicast = MdnsPeerInfo {
|
||||||
addr: "224.0.0.251:42424".parse().expect("test address parses"),
|
addr: "224.0.0.251:42424".parse().expect("test address parses"),
|
||||||
|
source_ip: "192.168.1.50".parse().expect("test source parses"),
|
||||||
peer_id,
|
peer_id,
|
||||||
proto_ver: Some(lanspread_proto::PROTOCOL_VERSION),
|
proto_ver: Some(lanspread_proto::PROTOCOL_VERSION),
|
||||||
};
|
};
|
||||||
@@ -576,6 +586,7 @@ mod tests {
|
|||||||
|
|
||||||
let zero_port = MdnsPeerInfo {
|
let zero_port = MdnsPeerInfo {
|
||||||
addr: "192.168.1.50:0".parse().expect("test address parses"),
|
addr: "192.168.1.50:0".parse().expect("test address parses"),
|
||||||
|
source_ip: "192.168.1.50".parse().expect("test source parses"),
|
||||||
peer_id,
|
peer_id,
|
||||||
proto_ver: Some(lanspread_proto::PROTOCOL_VERSION),
|
proto_ver: Some(lanspread_proto::PROTOCOL_VERSION),
|
||||||
};
|
};
|
||||||
@@ -585,7 +596,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn active_candidate_keys_bound_both_claimed_identity_and_address() {
|
fn active_candidate_keys_bound_both_claimed_identity_and_address() {
|
||||||
let active_endpoint = endpoint(1, 12001);
|
let active_endpoint = endpoint(1, 12001);
|
||||||
let mut active = HashSet::from([active_endpoint]);
|
let mut active = HashMap::from([(active_endpoint, active_endpoint.addr.ip())]);
|
||||||
|
|
||||||
assert!(candidate_conflicts(&active, endpoint(1, 12002)));
|
assert!(candidate_conflicts(&active, endpoint(1, 12002)));
|
||||||
assert!(candidate_conflicts(&active, endpoint(2, 12001)));
|
assert!(candidate_conflicts(&active, endpoint(2, 12001)));
|
||||||
@@ -601,22 +612,26 @@ mod tests {
|
|||||||
let now = tokio::time::Instant::now();
|
let now = tokio::time::Instant::now();
|
||||||
let first = endpoint(1, 12001);
|
let first = endpoint(1, 12001);
|
||||||
let mut recent = RecentCandidates::default();
|
let mut recent = RecentCandidates::default();
|
||||||
assert!(recent.try_record(first, now));
|
assert!(recent.try_record(first, first.addr.ip(), now));
|
||||||
|
|
||||||
for port in 12002..12102 {
|
for port in 12002..12102 {
|
||||||
assert!(!recent.try_record(endpoint(1, port), now));
|
let candidate = endpoint(1, port);
|
||||||
|
assert!(!recent.try_record(candidate, candidate.addr.ip(), now));
|
||||||
}
|
}
|
||||||
for seed in 2..=101 {
|
for seed in 2..=101 {
|
||||||
assert!(!recent.try_record(endpoint(seed, 12001), now));
|
let candidate = endpoint(seed, 12001);
|
||||||
|
assert!(!recent.try_record(candidate, candidate.addr.ip(), now));
|
||||||
}
|
}
|
||||||
for seed in 2..=u8::try_from(MAX_ACTIVE_DISCOVERY_CANDIDATES).expect("test bound fits u8") {
|
for seed in 2..=u8::try_from(MAX_ACTIVE_DISCOVERY_CANDIDATES).expect("test bound fits u8") {
|
||||||
assert!(recent.try_record(endpoint(seed, 13000 + u16::from(seed)), now));
|
let candidate = endpoint(seed, 13000 + u16::from(seed));
|
||||||
|
assert!(recent.try_record(candidate, candidate.addr.ip(), now));
|
||||||
}
|
}
|
||||||
assert_eq!(recent.len(), MAX_ACTIVE_DISCOVERY_CANDIDATES);
|
assert_eq!(recent.len(), MAX_ACTIVE_DISCOVERY_CANDIDATES);
|
||||||
assert!(!recent.try_record(endpoint(200, 14000), now));
|
let last = endpoint(200, 14000);
|
||||||
|
assert!(!recent.try_record(last, last.addr.ip(), now));
|
||||||
|
|
||||||
let after_cooldown = now + DISCOVERY_CANDIDATE_COOLDOWN;
|
let after_cooldown = now + DISCOVERY_CANDIDATE_COOLDOWN;
|
||||||
assert!(recent.try_record(endpoint(200, 14000), after_cooldown));
|
assert!(recent.try_record(last, last.addr.ip(), after_cooldown));
|
||||||
assert_eq!(recent.len(), 1);
|
assert_eq!(recent.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,46 +640,52 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn one_source_ip_cannot_occupy_more_than_its_candidate_budget() {
|
fn rotating_advertised_targets_share_the_observed_source_budget() {
|
||||||
let now = tokio::time::Instant::now();
|
let now = tokio::time::Instant::now();
|
||||||
let mut active = HashSet::new();
|
let mut active = HashMap::new();
|
||||||
let mut recent = RecentCandidates::default();
|
let mut recent = RecentCandidates::default();
|
||||||
let flood_ip = [192, 168, 1, 66];
|
let flood_source: IpAddr = [192, 168, 1, 66].into();
|
||||||
|
|
||||||
// A single host rotating peer IDs and ports fills only its own budget.
|
// One responder can rotate advertised A records, peer IDs, and ports,
|
||||||
|
// but all candidates remain charged to the observed datagram source.
|
||||||
for index in 0..MAX_DISCOVERY_CANDIDATES_PER_SOURCE_IP {
|
for index in 0..MAX_DISCOVERY_CANDIDATES_PER_SOURCE_IP {
|
||||||
let seed = u8::try_from(index + 1).expect("test index fits u8");
|
let seed = u8::try_from(index + 1).expect("test index fits u8");
|
||||||
let candidate = endpoint_at(seed, flood_ip, 20000 + u16::from(seed));
|
let candidate = endpoint_at(seed, [10, 0, seed, 2], 20000 + u16::from(seed));
|
||||||
assert!(candidate_is_admissible(
|
assert!(candidate_is_admissible(
|
||||||
&active,
|
&active,
|
||||||
&mut recent,
|
&mut recent,
|
||||||
candidate,
|
candidate,
|
||||||
|
flood_source,
|
||||||
now
|
now
|
||||||
));
|
));
|
||||||
active.insert(candidate);
|
active.insert(candidate, flood_source);
|
||||||
}
|
}
|
||||||
assert!(!candidate_is_admissible(
|
assert!(!candidate_is_admissible(
|
||||||
&active,
|
&active,
|
||||||
&mut recent,
|
&mut recent,
|
||||||
endpoint_at(100, flood_ip, 30000),
|
endpoint_at(100, [10, 1, 1, 2], 30000),
|
||||||
|
flood_source,
|
||||||
now
|
now
|
||||||
));
|
));
|
||||||
|
|
||||||
// Other hosts are unaffected while the flood host is saturated.
|
// Other hosts are unaffected while the flood host is saturated.
|
||||||
|
let other_source: IpAddr = [192, 168, 1, 67].into();
|
||||||
assert!(candidate_is_admissible(
|
assert!(candidate_is_admissible(
|
||||||
&active,
|
&active,
|
||||||
&mut recent,
|
&mut recent,
|
||||||
endpoint_at(101, [192, 168, 1, 67], 42424),
|
endpoint_at(101, [10, 1, 1, 3], 42424),
|
||||||
|
other_source,
|
||||||
now
|
now
|
||||||
));
|
));
|
||||||
|
|
||||||
// Completed negotiations keep counting while they cool down ...
|
// Completed negotiations keep counting while they cool down ...
|
||||||
let completed = endpoint_at(1, flood_ip, 20001);
|
let completed = endpoint_at(1, [10, 0, 1, 2], 20001);
|
||||||
active.remove(&completed);
|
active.remove(&completed);
|
||||||
assert!(!candidate_is_admissible(
|
assert!(!candidate_is_admissible(
|
||||||
&active,
|
&active,
|
||||||
&mut recent,
|
&mut recent,
|
||||||
endpoint_at(102, flood_ip, 30001),
|
endpoint_at(102, [10, 1, 1, 4], 30001),
|
||||||
|
flood_source,
|
||||||
now
|
now
|
||||||
));
|
));
|
||||||
// ... and free the budget once the cooldown expires.
|
// ... and free the budget once the cooldown expires.
|
||||||
@@ -672,7 +693,8 @@ mod tests {
|
|||||||
assert!(candidate_is_admissible(
|
assert!(candidate_is_admissible(
|
||||||
&active,
|
&active,
|
||||||
&mut recent,
|
&mut recent,
|
||||||
endpoint_at(102, flood_ip, 30001),
|
endpoint_at(102, [10, 1, 1, 4], 30001),
|
||||||
|
flood_source,
|
||||||
after_cooldown
|
after_cooldown
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -685,12 +707,13 @@ mod tests {
|
|||||||
let mut active = (0..MAX_ACTIVE_DISCOVERY_CANDIDATES)
|
let mut active = (0..MAX_ACTIVE_DISCOVERY_CANDIDATES)
|
||||||
.map(|index| {
|
.map(|index| {
|
||||||
let seed = u8::try_from(index + 1).expect("test index fits u8");
|
let seed = u8::try_from(index + 1).expect("test index fits u8");
|
||||||
endpoint_at(seed, [10, 0, seed, 1], 15000 + u16::from(seed))
|
let candidate = endpoint_at(seed, [10, 0, seed, 1], 15000 + u16::from(seed));
|
||||||
|
(candidate, candidate.addr.ip())
|
||||||
})
|
})
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashMap<_, _>>();
|
||||||
let mut recent = RecentCandidates::default();
|
let mut recent = RecentCandidates::default();
|
||||||
for candidate in &active {
|
for (candidate, source_ip) in &active {
|
||||||
assert!(recent.try_record(*candidate, now));
|
assert!(recent.try_record(*candidate, *source_ip, now));
|
||||||
}
|
}
|
||||||
|
|
||||||
let after_cooldown = now + DISCOVERY_CANDIDATE_COOLDOWN;
|
let after_cooldown = now + DISCOVERY_CANDIDATE_COOLDOWN;
|
||||||
@@ -699,16 +722,18 @@ mod tests {
|
|||||||
&active,
|
&active,
|
||||||
&mut recent,
|
&mut recent,
|
||||||
next,
|
next,
|
||||||
|
next.addr.ip(),
|
||||||
after_cooldown,
|
after_cooldown,
|
||||||
));
|
));
|
||||||
assert_eq!(active.len(), MAX_ACTIVE_DISCOVERY_CANDIDATES);
|
assert_eq!(active.len(), MAX_ACTIVE_DISCOVERY_CANDIDATES);
|
||||||
|
|
||||||
let completed = *active.iter().next().expect("active set should be nonempty");
|
let completed = *active.keys().next().expect("active set should be nonempty");
|
||||||
active.remove(&completed);
|
active.remove(&completed);
|
||||||
assert!(candidate_is_admissible(
|
assert!(candidate_is_admissible(
|
||||||
&active,
|
&active,
|
||||||
&mut recent,
|
&mut recent,
|
||||||
next,
|
next,
|
||||||
|
next.addr.ip(),
|
||||||
after_cooldown,
|
after_cooldown,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user