Skip to content

Commit fbdf2c7

Browse files
fix(iroh): only switch paths if the new path is actually better
For now the restriction is that for direct to direct path switching latency must be at least 5ms better.
1 parent c92ff3d commit fbdf2c7

File tree

3 files changed

+179
-51
lines changed

3 files changed

+179
-51
lines changed

Cargo.lock

Lines changed: 22 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

iroh/src/magicsock/remote_map/remote_state.rs

Lines changed: 157 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{
22
collections::{BTreeSet, VecDeque},
3-
net::SocketAddr,
3+
net::{SocketAddr, SocketAddrV4, SocketAddrV6},
44
pin::Pin,
55
sync::Arc,
66
task::Poll,
@@ -74,6 +74,12 @@ mod path_state;
7474
/// in a high frequency, and to keep data about previous path around for subsequent connections.
7575
const ACTOR_MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
7676

77+
/// The minimum RTT difference to make it worth switching IP paths
78+
const RTT_SWITCHING_MIN_IP: Duration = Duration::from_millis(5);
79+
80+
/// How much do we prefer IPv6 over IPv4?
81+
const IPV6_RTT_ADVANTAGE: Duration = Duration::from_millis(3);
82+
7783
/// A stream of events from all paths for all connections.
7884
///
7985
/// The connection is identified using [`ConnId`]. The event `Err` variant happens when the
@@ -856,34 +862,58 @@ impl RemoteStateActor {
856862
.filter_map(|(addr, rtts)| rtts.into_iter().min().map(|rtt| (addr, rtt)))
857863
.collect();
858864

859-
// Find the fastest direct or relay path.
860-
const IPV6_RTT_ADVANTAGE: Duration = Duration::from_millis(3);
861-
let direct_path = path_rtts
865+
// Find the fastest direct IPv4 path.
866+
let direct_path_ipv4 = path_rtts
862867
.iter()
863-
.filter(|(addr, _rtt)| addr.is_ip())
864-
.map(|(addr, rtt)| {
865-
if addr.is_ipv4() {
866-
(*rtt + IPV6_RTT_ADVANTAGE, addr)
868+
.filter_map(|(addr, rtt)| {
869+
if let transports::Addr::Ip(SocketAddr::V4(addr)) = *addr {
870+
Some((addr, *rtt))
867871
} else {
868-
(*rtt, addr)
872+
None
869873
}
870874
})
871-
.min();
872-
let selected_path = direct_path.or_else(|| {
873-
// Find the fasted relay path.
874-
path_rtts
875-
.iter()
876-
.filter(|(addr, _rtt)| addr.is_relay())
877-
.map(|(addr, rtt)| (*rtt, addr))
878-
.min()
879-
});
880-
if let Some((rtt, addr)) = selected_path {
875+
.min_by_key(|(_addr, rtt)| *rtt);
876+
877+
// Find the fastest direct IPv6 path.
878+
let direct_path_ipv6 = path_rtts
879+
.iter()
880+
.filter_map(|(addr, rtt)| {
881+
if let transports::Addr::Ip(SocketAddr::V6(addr)) = *addr {
882+
Some((addr, *rtt))
883+
} else {
884+
None
885+
}
886+
})
887+
.min_by_key(|(_addr, rtt)| *rtt);
888+
889+
// Find the fastest relay path.
890+
let relay_path = path_rtts
891+
.iter()
892+
.filter(|(addr, _rtt)| addr.is_relay())
893+
.min_by_key(|(_addr, rtt)| *rtt)
894+
.map(|(addr, rtt)| (addr.clone(), *rtt));
895+
896+
let current_path = self
897+
.selected_path
898+
.get()
899+
.and_then(|addr| path_rtts.get(&addr).copied().map(|rtt| (addr, rtt)));
900+
let selected_path = select_best_path(
901+
current_path.clone(),
902+
direct_path_ipv4,
903+
direct_path_ipv6,
904+
relay_path,
905+
);
906+
907+
// Apply our new path
908+
if let Some((addr, rtt)) = selected_path {
881909
let prev = self.selected_path.set(Some(addr.clone()));
882910
if prev.is_ok() {
883911
debug!(?addr, ?rtt, ?prev, "selected new path");
884912
}
885-
self.open_path(addr);
886-
self.close_redundant_paths(addr);
913+
self.open_path(&addr);
914+
self.close_redundant_paths(&addr);
915+
} else {
916+
trace!(?current_path, "keeping current path");
887917
}
888918
}
889919

@@ -932,6 +962,83 @@ impl RemoteStateActor {
932962
}
933963
}
934964

965+
/// Returns `Some` if a new path should be selected, `None` if the `current_path` should
966+
/// continued to be used.
967+
fn select_best_path(
968+
current_path: Option<(transports::Addr, Duration)>,
969+
direct_path_ipv4: Option<(SocketAddrV4, Duration)>,
970+
direct_path_ipv6: Option<(SocketAddrV6, Duration)>,
971+
new_relay_path: Option<(transports::Addr, Duration)>,
972+
) -> Option<(transports::Addr, Duration)> {
973+
// Determine the best new IP path.
974+
let best_ip_path = match (direct_path_ipv4, direct_path_ipv6) {
975+
(Some((addr_v4, rtt_v4)), Some((addr_v6, rtt_v6))) => {
976+
Some(select_v4_v6(addr_v4, rtt_v4, addr_v6, rtt_v6))
977+
}
978+
(None, Some((addr_v6, rtt_v6))) => Some((addr_v6.into(), rtt_v6)),
979+
(Some((addr_v4, rtt_v4)), None) => Some((addr_v4.into(), rtt_v4)),
980+
(None, None) => None,
981+
};
982+
let best_ip_path = best_ip_path.map(|(addr, rtt)| (transports::Addr::Ip(addr), rtt));
983+
984+
match current_path {
985+
None => {
986+
// If we currently have no path
987+
if best_ip_path.is_some() {
988+
// Use the best IP path
989+
best_ip_path
990+
} else {
991+
// Use the new relay path
992+
new_relay_path
993+
}
994+
}
995+
Some((transports::Addr::Relay(..), _)) => {
996+
// If we have a current path, but it is relay
997+
if best_ip_path.is_some() {
998+
// Use the best IP path
999+
best_ip_path
1000+
} else {
1001+
// Use the new relay path
1002+
new_relay_path
1003+
}
1004+
}
1005+
Some((transports::Addr::Ip(_), current_rtt)) => {
1006+
match &best_ip_path {
1007+
Some((_, new_rtt)) => {
1008+
// Comparing available IP paths
1009+
if current_rtt >= *new_rtt + RTT_SWITCHING_MIN_IP {
1010+
// New IP path is faster
1011+
best_ip_path
1012+
} else {
1013+
// New IP is not faster
1014+
None
1015+
}
1016+
}
1017+
None => {
1018+
// No new IP path, don't switch away
1019+
None
1020+
}
1021+
}
1022+
}
1023+
}
1024+
}
1025+
1026+
/// Compare two IP addrs v4 & v6 and selects the "best" one.
1027+
///
1028+
/// This prefers IPv6 paths over IPv4 paths.
1029+
fn select_v4_v6(
1030+
addr_v4: SocketAddrV4,
1031+
rtt_v4: Duration,
1032+
addr_v6: SocketAddrV6,
1033+
rtt_v6: Duration,
1034+
) -> (SocketAddr, Duration) {
1035+
if rtt_v6 <= rtt_v4 + IPV6_RTT_ADVANTAGE {
1036+
(addr_v6.into(), rtt_v6)
1037+
} else {
1038+
(addr_v4.into(), rtt_v4)
1039+
}
1040+
}
1041+
9351042
fn send_datagram<'a>(
9361043
sender: &'a mut TransportsSender,
9371044
dst: transports::Addr,
@@ -1319,3 +1426,32 @@ fn to_transports_addr(
13191426
}
13201427
})
13211428
}
1429+
1430+
#[cfg(test)]
1431+
mod tests {
1432+
use std::net::{Ipv4Addr, Ipv6Addr};
1433+
1434+
use super::*;
1435+
1436+
#[test]
1437+
fn test_select_v4_v6_addr() {
1438+
let v4 = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 1);
1439+
let v6 = SocketAddrV6::new(Ipv6Addr::LOCALHOST, 1, 0, 0);
1440+
1441+
// Same RTT, prefer v6
1442+
let (addr, rtt) =
1443+
select_v4_v6(v4, Duration::from_millis(10), v6, Duration::from_millis(10));
1444+
assert_eq!(addr, v6.into());
1445+
assert_eq!(rtt, Duration::from_millis(10));
1446+
1447+
// v4 better
1448+
let (addr, rtt) = select_v4_v6(v4, Duration::from_millis(1), v6, Duration::from_millis(10));
1449+
assert_eq!(addr, v4.into());
1450+
assert_eq!(rtt, Duration::from_millis(1));
1451+
1452+
// v6 better
1453+
let (addr, rtt) = select_v4_v6(v4, Duration::from_millis(10), v6, Duration::from_millis(1));
1454+
assert_eq!(addr, v6.into());
1455+
assert_eq!(rtt, Duration::from_millis(1));
1456+
}
1457+
}

iroh/src/magicsock/transports.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -467,13 +467,6 @@ impl Addr {
467467
matches!(self, Self::Ip(_))
468468
}
469469

470-
pub(crate) fn is_ipv4(&self) -> bool {
471-
match self {
472-
Addr::Ip(socket_addr) => socket_addr.is_ipv4(),
473-
Addr::Relay(_, _) => false,
474-
}
475-
}
476-
477470
/// Returns `None` if not an `Ip`.
478471
pub(crate) fn into_socket_addr(self) -> Option<SocketAddr> {
479472
match self {

0 commit comments

Comments
 (0)