prevent EasyTier-managed IPv6 from being used as underlay connections (#2181)

When a node has public IPv6 addresses allocated by EasyTier, those addresses
are installed on the host's network interfaces. The system would then pick
them up as candidate source/destination addresses for underlay connections
(direct peer, UDP hole punch, bind addresses), causing overlay traffic to
loop back into the overlay itself.

Add a central predicate is_ip_easytier_managed_ipv6() and apply it at every
point where IPv6 addresses are selected for underlay use:
- Filter managed IPv6 from DNS-resolved connector addresses, including a
  UDP socket getsockname check to detect whether the OS would route through
  the overlay to reach a destination
- Skip managed IPv6 in bind address selection and STUN candidate filtering
- Strip managed IPv6 from GetIpListResponse RPC so peers never learn them
- Pass pre-resolved addresses to tunnel connectors to avoid re-resolution

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
KKRainbow
2026-04-29 12:17:22 +08:00
committed by GitHub
co-authored by Claude Opus 4.7
parent f66010e6f9
commit ed8df2d58f
14 changed files with 478 additions and 87 deletions
+41 -2
View File
@@ -94,6 +94,8 @@ impl AclFilter {
/// Preserves connection tracking and rate limiting state across reloads
/// Now lock-free and doesn't require &mut self!
pub fn reload_rules(&self, acl_config: Option<&Acl>) {
self.outbound_allow_records.clear();
let Some(acl_config) = acl_config else {
self.acl_enabled.store(false, Ordering::Relaxed);
return;
@@ -400,14 +402,15 @@ mod tests {
use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr},
sync::Arc,
time::Instant,
};
use crate::{
common::acl_processor::PacketInfo,
proto::acl::{ChainType, Protocol},
proto::acl::{Acl, ChainType, Protocol},
};
use super::AclFilter;
use super::{AclFilter, OutboundAllowRecord};
fn packet_info(dst_ip: IpAddr) -> PacketInfo {
PacketInfo {
@@ -445,4 +448,40 @@ mod tests {
assert_eq!(chain, ChainType::Forward);
}
#[tokio::test]
async fn reload_rules_clears_outbound_allow_records() {
let filter = AclFilter::new();
filter.outbound_allow_records.insert(
OutboundAllowRecord {
src_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
dst_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)),
src_port: Some(1234),
dst_port: Some(80),
protocol: Protocol::Tcp,
},
Instant::now(),
);
assert_eq!(filter.outbound_allow_records.len(), 1);
filter.reload_rules(Some(&Acl::default()));
assert_eq!(filter.outbound_allow_records.len(), 0);
filter.outbound_allow_records.insert(
OutboundAllowRecord {
src_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)),
dst_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
src_port: Some(4321),
dst_port: Some(443),
protocol: Protocol::Tcp,
},
Instant::now(),
);
assert_eq!(filter.outbound_allow_records.len(), 1);
filter.reload_rules(None);
assert_eq!(filter.outbound_allow_records.len(), 0);
}
}
+55 -9
View File
@@ -12,6 +12,22 @@ use crate::{
tunnel::udp,
};
fn remove_easytier_managed_ipv6s(ret: &mut GetIpListResponse, global_ctx: &ArcGlobalCtx) {
ret.interface_ipv6s.retain(|ip| {
let ip = std::net::Ipv6Addr::from(*ip);
!global_ctx.is_ip_easytier_managed_ipv6(&ip)
});
if ret
.public_ipv6
.as_ref()
.map(|ip| std::net::Ipv6Addr::from(*ip))
.is_some_and(|ip| global_ctx.is_ip_easytier_managed_ipv6(&ip))
{
ret.public_ipv6 = None;
}
}
#[derive(Clone)]
pub struct DirectConnectorManagerRpcServer {
// TODO: this only cache for one src peer, should make it global
@@ -36,15 +52,7 @@ impl DirectConnectorRpc for DirectConnectorManagerRpcServer {
.chain(self.global_ctx.get_running_listeners())
.map(Into::into)
.collect();
// remove et ipv6 from the interface ipv6 list
if let Some(et_ipv6) = self.global_ctx.get_ipv6() {
let et_ipv6: crate::proto::common::Ipv6Addr = et_ipv6.address().into();
ret.interface_ipv6s.retain(|x| *x != et_ipv6);
}
if let Some(public_ipv6) = self.global_ctx.get_public_ipv6_lease() {
let public_ipv6: crate::proto::common::Ipv6Addr = public_ipv6.address().into();
ret.interface_ipv6s.retain(|x| *x != public_ipv6);
}
remove_easytier_managed_ipv6s(&mut ret, &self.global_ctx);
tracing::trace!(
"get_ip_list: public_ipv4: {:?}, public_ipv6: {:?}, listeners: {:?}",
ret.public_ipv4,
@@ -88,3 +96,41 @@ impl DirectConnectorManagerRpcServer {
Self { global_ctx }
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use crate::{
common::global_ctx::tests::get_mock_global_ctx,
peers::peer_rpc_service::remove_easytier_managed_ipv6s, proto::peer_rpc::GetIpListResponse,
};
#[tokio::test]
async fn get_ip_list_sanitizer_removes_managed_ipv6_from_all_sources() {
let global_ctx = get_mock_global_ctx();
let virtual_ipv6 = "fd00::1/64".parse().unwrap();
let public_ipv6 = "2001:db8::2/128".parse().unwrap();
let physical_ipv6: std::net::Ipv6Addr = "2001:db8::3".parse().unwrap();
let routed_ipv6: cidr::Ipv6Inet = "2001:db8::4/128".parse().unwrap();
global_ctx.set_ipv6(Some(virtual_ipv6));
global_ctx.set_public_ipv6_lease(Some(public_ipv6));
global_ctx.set_public_ipv6_routes(BTreeSet::from([routed_ipv6]));
let mut ip_list = GetIpListResponse {
public_ipv6: Some(public_ipv6.address().into()),
interface_ipv6s: vec![
virtual_ipv6.address().into(),
public_ipv6.address().into(),
routed_ipv6.address().into(),
physical_ipv6.into(),
],
..Default::default()
};
remove_easytier_managed_ipv6s(&mut ip_list, &global_ctx);
assert_eq!(ip_list.public_ipv6, None);
assert_eq!(ip_list.interface_ipv6s, vec![physical_ipv6.into()]);
}
}
+2
View File
@@ -243,6 +243,8 @@ impl PublicIpv6Service {
.copied()
.collect::<Vec<_>>();
*cached_routes = routes;
self.global_ctx
.set_public_ipv6_routes(cached_routes.clone());
self.global_ctx
.issue_event(GlobalCtxEvent::PublicIpv6RoutesUpdated(added, removed));
}