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:
fanyang
2026-03-04 09:36:35 +08:00
committed by GitHub
parent db6b9e3684
commit 9e9916efa5
+65 -11
View File
@@ -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 => {