mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-03 01:25:37 +00:00
perf(ipv6_hole_punch): handle multiple ipv6 public ip correctly (#2387)
This PR fixes IPv6 UDP hole punching for peers with multiple public IPv6 addresses by adding two RPC signals: - connector_addrs: connector-side candidate public IPv6 socket addresses that the remote peer should punch back to. - preferred_src_ipv6: remote listener IPv6 address that the remote peer should use as the UDP source when sending hole-punch packets back. Together, these let the connector try all usable local IPv6 candidates while keeping the remote punch-back packet sourced from the same IPv6 address that the connector is dialing.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::net::{IpAddr, Ipv6Addr, SocketAddr};
|
||||
|
||||
use crate::{
|
||||
common::global_ctx::ArcGlobalCtx,
|
||||
common::{global_ctx::ArcGlobalCtx, network::IPCollector},
|
||||
proto::{
|
||||
common::Void,
|
||||
peer_rpc::{
|
||||
@@ -12,6 +12,8 @@ use crate::{
|
||||
tunnel::udp,
|
||||
};
|
||||
|
||||
const MAX_UDP_HOLE_PUNCH_CONNECTOR_ADDRS: usize = 16;
|
||||
|
||||
fn remove_easytier_managed_ipv6s(ret: &mut GetIpListResponse, global_ctx: &ArcGlobalCtx) {
|
||||
ret.interface_ipv6s.retain(|ip| {
|
||||
let ip = std::net::Ipv6Addr::from(*ip);
|
||||
@@ -28,6 +30,86 @@ fn remove_easytier_managed_ipv6s(ret: &mut GetIpListResponse, global_ctx: &ArcGl
|
||||
}
|
||||
}
|
||||
|
||||
fn is_usable_preferred_src_ipv6(ip: &Ipv6Addr, global_ctx: &ArcGlobalCtx) -> bool {
|
||||
!global_ctx.is_ip_easytier_managed_ipv6(ip)
|
||||
&& !ip.is_loopback()
|
||||
&& !ip.is_unspecified()
|
||||
&& !ip.is_unique_local()
|
||||
&& !ip.is_unicast_link_local()
|
||||
&& !ip.is_multicast()
|
||||
}
|
||||
|
||||
async fn local_preferred_src_ipv6(
|
||||
global_ctx: &ArcGlobalCtx,
|
||||
preferred_src_ipv6: Option<crate::proto::common::Ipv6Addr>,
|
||||
) -> Option<udp::PreferredIpv6Source> {
|
||||
let preferred_src_ipv6 = preferred_src_ipv6.map(Ipv6Addr::from)?;
|
||||
if !is_usable_preferred_src_ipv6(&preferred_src_ipv6, global_ctx) {
|
||||
tracing::debug!(
|
||||
?preferred_src_ipv6,
|
||||
"ignore unusable preferred IPv6 source for udp hole punch"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let ifaces = IPCollector::collect_interfaces(global_ctx.net_ns.clone(), false).await;
|
||||
for iface in ifaces {
|
||||
let is_local = iface.ips.iter().any(|ip| match ip.ip() {
|
||||
IpAddr::V6(v6) => v6 == preferred_src_ipv6,
|
||||
IpAddr::V4(_) => false,
|
||||
});
|
||||
if is_local {
|
||||
tracing::debug!(
|
||||
?preferred_src_ipv6,
|
||||
ifindex = iface.index,
|
||||
"use preferred IPv6 source for udp hole punch"
|
||||
);
|
||||
return Some(udp::PreferredIpv6Source {
|
||||
ip: preferred_src_ipv6,
|
||||
ifindex: iface.index,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
?preferred_src_ipv6,
|
||||
"ignore non-local preferred IPv6 source for udp hole punch"
|
||||
);
|
||||
None
|
||||
}
|
||||
|
||||
fn connector_addrs_from_request(
|
||||
req: SendUdpHolePunchPacketRequest,
|
||||
) -> rpc_types::error::Result<(u16, Vec<SocketAddr>, Option<crate::proto::common::Ipv6Addr>)> {
|
||||
let listener_port = u16::try_from(req.listener_port)
|
||||
.map_err(|_| anyhow::anyhow!("listener_port is out of range: {}", req.listener_port))?;
|
||||
let mut connector_addrs = req
|
||||
.connector_addrs
|
||||
.into_iter()
|
||||
.map(SocketAddr::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if connector_addrs.is_empty() {
|
||||
connector_addrs.push(
|
||||
req.connector_addr
|
||||
.ok_or(anyhow::anyhow!("connector_addr is required"))?
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut deduped = Vec::with_capacity(connector_addrs.len());
|
||||
for addr in connector_addrs {
|
||||
if !deduped.contains(&addr) {
|
||||
deduped.push(addr);
|
||||
}
|
||||
if deduped.len() >= MAX_UDP_HOLE_PUNCH_CONNECTOR_ADDRS {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((listener_port, deduped, req.preferred_src_ipv6))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DirectConnectorManagerRpcServer {
|
||||
// TODO: this only cache for one src peer, should make it global
|
||||
@@ -67,23 +149,38 @@ impl DirectConnectorRpc for DirectConnectorManagerRpcServer {
|
||||
_: BaseController,
|
||||
req: SendUdpHolePunchPacketRequest,
|
||||
) -> rpc_types::error::Result<Void> {
|
||||
let listener_port = req.listener_port as u16;
|
||||
let connector_addr: SocketAddr = req
|
||||
.connector_addr
|
||||
.ok_or(anyhow::anyhow!("connector_addr is required"))?
|
||||
.into();
|
||||
let (listener_port, connector_addrs, preferred_src_ipv6) =
|
||||
connector_addrs_from_request(req)?;
|
||||
let preferred_src_ipv6 =
|
||||
local_preferred_src_ipv6(&self.global_ctx, preferred_src_ipv6).await;
|
||||
|
||||
tracing::info!(
|
||||
"Sending udp hole punch packet to {} from listener port {}",
|
||||
connector_addr,
|
||||
listener_port
|
||||
?connector_addrs,
|
||||
?preferred_src_ipv6,
|
||||
listener_port,
|
||||
"Sending udp hole punch packet"
|
||||
);
|
||||
|
||||
// send 3 packets to the connector
|
||||
for _ in 0..3 {
|
||||
match connector_addr {
|
||||
SocketAddr::V4(addr) => udp::send_v4_hole_punch_packet(listener_port, addr).await?,
|
||||
SocketAddr::V6(addr) => udp::send_v6_hole_punch_packet(listener_port, addr).await?,
|
||||
for connector_addr in &connector_addrs {
|
||||
let ret = match connector_addr {
|
||||
SocketAddr::V4(addr) => {
|
||||
udp::send_v4_hole_punch_packet(listener_port, *addr).await
|
||||
}
|
||||
SocketAddr::V6(addr) => {
|
||||
udp::send_v6_hole_punch_packet(listener_port, *addr, preferred_src_ipv6)
|
||||
.await
|
||||
}
|
||||
};
|
||||
if let Err(e) = ret {
|
||||
tracing::debug!(
|
||||
?e,
|
||||
?connector_addr,
|
||||
listener_port,
|
||||
"send udp hole punch packet failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(30)).await;
|
||||
}
|
||||
@@ -99,11 +196,12 @@ impl DirectConnectorManagerRpcServer {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeSet;
|
||||
use std::{collections::BTreeSet, net::SocketAddr};
|
||||
|
||||
use crate::{
|
||||
common::global_ctx::tests::get_mock_global_ctx,
|
||||
peers::peer_rpc_service::remove_easytier_managed_ipv6s, proto::peer_rpc::GetIpListResponse,
|
||||
peers::peer_rpc_service::{connector_addrs_from_request, remove_easytier_managed_ipv6s},
|
||||
proto::peer_rpc::{GetIpListResponse, SendUdpHolePunchPacketRequest},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
@@ -133,4 +231,58 @@ mod tests {
|
||||
assert_eq!(ip_list.public_ipv6, None);
|
||||
assert_eq!(ip_list.interface_ipv6s, vec![physical_ipv6.into()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hole_punch_request_prefers_batch_connector_addrs() {
|
||||
let old_addr: SocketAddr = "[2001:db8::1]:10001".parse().unwrap();
|
||||
let first_batch_addr: SocketAddr = "[2001:db8::2]:10002".parse().unwrap();
|
||||
let second_batch_addr: SocketAddr = "[2001:db8::3]:10003".parse().unwrap();
|
||||
let preferred_src_ipv6: std::net::Ipv6Addr = "2001:db8::4".parse().unwrap();
|
||||
|
||||
let (listener_port, connector_addrs, preferred_src) =
|
||||
connector_addrs_from_request(SendUdpHolePunchPacketRequest {
|
||||
connector_addr: Some(old_addr.into()),
|
||||
listener_port: 11010,
|
||||
preferred_src_ipv6: Some(preferred_src_ipv6.into()),
|
||||
connector_addrs: vec![
|
||||
first_batch_addr.into(),
|
||||
first_batch_addr.into(),
|
||||
second_batch_addr.into(),
|
||||
],
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(listener_port, 11010);
|
||||
assert_eq!(connector_addrs, vec![first_batch_addr, second_batch_addr]);
|
||||
assert_eq!(preferred_src, Some(preferred_src_ipv6.into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hole_punch_request_falls_back_to_legacy_connector_addr() {
|
||||
let old_addr: SocketAddr = "[2001:db8::1]:10001".parse().unwrap();
|
||||
|
||||
let (_, connector_addrs, _) = connector_addrs_from_request(SendUdpHolePunchPacketRequest {
|
||||
connector_addr: Some(old_addr.into()),
|
||||
listener_port: 11010,
|
||||
preferred_src_ipv6: None,
|
||||
connector_addrs: vec![],
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(connector_addrs, vec![old_addr]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hole_punch_request_rejects_out_of_range_listener_port() {
|
||||
let old_addr: SocketAddr = "[2001:db8::1]:10001".parse().unwrap();
|
||||
|
||||
let ret = connector_addrs_from_request(SendUdpHolePunchPacketRequest {
|
||||
connector_addr: Some(old_addr.into()),
|
||||
listener_port: u16::MAX as u32 + 1,
|
||||
preferred_src_ipv6: None,
|
||||
connector_addrs: vec![],
|
||||
});
|
||||
|
||||
assert!(ret.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user