mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-04-22 16:17:23 +08:00
fix(connector): skip self-connection when peer shares local interface IPs (#1941)
When two EasyTier instances run on the same machine and share the same network, the direct connector would expand a remote peer's 0.0.0.0 listener into local interface IPs and then attempt to connect to itself, causing an infinite loop of failed connection attempts. The existing `peer_id != my_peer_id` guard does not cover this case because the two instances have different peer IDs despite sharing the same physical network interfaces. Fix by adding a self-connection check in `spawn_direct_connect_task`: before spawning a connect task, compare the candidate (scheme, IP, port) against the local running listeners. If a local listener matches on all three dimensions — accounting for 0.0.0.0/:: wildcards by checking membership in the local interface IP sets — the candidate is silently dropped with a DEBUG log message. The fix covers all four code paths: - IPv4 unspecified (0.0.0.0) expansion loop - IPv4 specific-address branch - IPv6 unspecified (::) expansion loop - IPv6 specific-address branch The TESTING flag logic is untouched so existing unit tests are unaffected. * refactor(connector): replace is_self_connect closure with GlobalCtx::should_deny_proxy (#1954) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
net::{Ipv6Addr, SocketAddr},
|
||||
net::{IpAddr, Ipv6Addr, SocketAddr},
|
||||
str::FromStr,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
@@ -294,14 +294,42 @@ impl DirectConnectorManagerData {
|
||||
};
|
||||
let listener_host = addrs.pop();
|
||||
tracing::info!(?listener_host, ?listener, "try direct connect to peer");
|
||||
|
||||
let is_udp = matches!(listener.scheme(), "udp" | "wg");
|
||||
// Snapshot running listeners once; used for cheap port pre-checks before the
|
||||
// expensive should_deny_proxy call (which binds a socket per IP) in the
|
||||
// unspecified-address expansion loops below.
|
||||
let local_listeners = self.global_ctx.get_running_listeners();
|
||||
let port_has_local_listener = |port: u16| -> bool {
|
||||
local_listeners
|
||||
.iter()
|
||||
.any(|l| l.port() == Some(port) && (matches!(l.scheme(), "udp" | "wg") == is_udp))
|
||||
};
|
||||
|
||||
match listener_host {
|
||||
Some(SocketAddr::V4(s_addr)) => {
|
||||
if s_addr.ip().is_unspecified() {
|
||||
// Only pay the should_deny_proxy cost (bind per IP) when a local
|
||||
// listener actually uses this port+protocol; otherwise the check
|
||||
// can never return true.
|
||||
let check_self = port_has_local_listener(s_addr.port());
|
||||
ip_list
|
||||
.interface_ipv4s
|
||||
.iter()
|
||||
.chain(ip_list.public_ipv4.iter())
|
||||
.for_each(|ip| {
|
||||
let sock_addr = SocketAddr::new(
|
||||
IpAddr::V4(std::net::Ipv4Addr::from(ip.addr)),
|
||||
s_addr.port(),
|
||||
);
|
||||
if check_self && self.global_ctx.should_deny_proxy(&sock_addr, is_udp) {
|
||||
tracing::debug!(
|
||||
?ip,
|
||||
?listener,
|
||||
"skip self-connection (0.0.0.0 expansion)"
|
||||
);
|
||||
return;
|
||||
}
|
||||
let mut addr = (*listener).clone();
|
||||
if addr.set_host(Some(ip.to_string().as_str())).is_ok() {
|
||||
tasks.spawn(Self::try_connect_to_ip(
|
||||
@@ -319,16 +347,26 @@ impl DirectConnectorManagerData {
|
||||
}
|
||||
});
|
||||
} else if !s_addr.ip().is_loopback() || TESTING.load(Ordering::Relaxed) {
|
||||
tasks.spawn(Self::try_connect_to_ip(
|
||||
self.clone(),
|
||||
dst_peer_id,
|
||||
listener.to_string(),
|
||||
));
|
||||
if self
|
||||
.global_ctx
|
||||
.should_deny_proxy(&SocketAddr::from(s_addr), is_udp)
|
||||
{
|
||||
tracing::debug!(?listener, "skip self-connection (specific IPv4)");
|
||||
} else {
|
||||
tasks.spawn(Self::try_connect_to_ip(
|
||||
self.clone(),
|
||||
dst_peer_id,
|
||||
listener.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(SocketAddr::V6(s_addr)) => {
|
||||
if s_addr.ip().is_unspecified() {
|
||||
// for ipv6, only try public ip
|
||||
// Same port pre-check as IPv4: avoid binding per IP when no local
|
||||
// listener uses this port+protocol.
|
||||
let check_self = port_has_local_listener(s_addr.port());
|
||||
ip_list
|
||||
.interface_ipv6s
|
||||
.iter()
|
||||
@@ -345,6 +383,15 @@ impl DirectConnectorManagerData {
|
||||
.collect::<HashSet<_>>()
|
||||
.iter()
|
||||
.for_each(|ip| {
|
||||
let sock_addr = SocketAddr::new(IpAddr::V6(*ip), s_addr.port());
|
||||
if check_self && self.global_ctx.should_deny_proxy(&sock_addr, is_udp) {
|
||||
tracing::debug!(
|
||||
?ip,
|
||||
?listener,
|
||||
"skip self-connection (:: expansion)"
|
||||
);
|
||||
return;
|
||||
}
|
||||
let mut addr = (*listener).clone();
|
||||
if addr.set_host(Some(format!("[{}]", ip).as_str())).is_ok() {
|
||||
tasks.spawn(Self::try_connect_to_ip(
|
||||
@@ -362,11 +409,18 @@ impl DirectConnectorManagerData {
|
||||
}
|
||||
});
|
||||
} else if !s_addr.ip().is_loopback() || TESTING.load(Ordering::Relaxed) {
|
||||
tasks.spawn(Self::try_connect_to_ip(
|
||||
self.clone(),
|
||||
dst_peer_id,
|
||||
listener.to_string(),
|
||||
));
|
||||
if self
|
||||
.global_ctx
|
||||
.should_deny_proxy(&SocketAddr::from(s_addr), is_udp)
|
||||
{
|
||||
tracing::debug!(?listener, "skip self-connection (specific IPv6)");
|
||||
} else {
|
||||
tasks.spawn(Self::try_connect_to_ip(
|
||||
self.clone(),
|
||||
dst_peer_id,
|
||||
listener.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
p => {
|
||||
|
||||
Reference in New Issue
Block a user