mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-08-05 20:19:45 +00:00
fix(gateway): route local virtual IP proxy targets to loopback
Avoid proxy loops when the target is this node's virtual IP. - Normalize local virtual IP destinations to loopback - Apply the behavior to TCP, KCP, and QUIC proxy paths - Add macOS utun repro tests and normalization coverage
This commit is contained in:
@@ -19,7 +19,9 @@ use tokio::task::JoinSet;
|
||||
|
||||
use super::{
|
||||
CidrSet,
|
||||
tcp_proxy::{NatDstConnector, NatDstTcpConnector, TcpProxy},
|
||||
tcp_proxy::{
|
||||
NatDstConnector, NatDstTcpConnector, TcpProxy, normalize_dst_for_local_virtual_ip,
|
||||
},
|
||||
};
|
||||
use crate::utils::task::HedgeExt;
|
||||
use crate::{
|
||||
@@ -369,9 +371,7 @@ impl KcpProxyDst {
|
||||
}
|
||||
|
||||
let send_to_self = global_ctx.is_ip_local_virtual_ip(&dst_ip);
|
||||
if send_to_self && global_ctx.no_tun() {
|
||||
dst_socket = format!("127.0.0.1:{}", dst_socket.port()).parse().unwrap();
|
||||
}
|
||||
dst_socket = normalize_dst_for_local_virtual_ip(&global_ctx, dst_socket);
|
||||
|
||||
let acl_handler = ProxyAclHandler {
|
||||
acl_filter: global_ctx.get_acl_filter().clone(),
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::common::PeerId;
|
||||
use crate::common::acl_processor::PacketInfo;
|
||||
use crate::common::global_ctx::{ArcGlobalCtx, GlobalCtx};
|
||||
use crate::gateway::CidrSet;
|
||||
use crate::gateway::tcp_proxy::{NatDstConnector, TcpProxy};
|
||||
use crate::gateway::tcp_proxy::{NatDstConnector, TcpProxy, normalize_dst_for_local_virtual_ip};
|
||||
use crate::gateway::wrapped_proxy::{ProxyAclHandler, TcpProxyForWrappedSrcTrait};
|
||||
use crate::peers::PeerPacketFilter;
|
||||
use crate::peers::peer_manager::PeerManager;
|
||||
@@ -748,9 +748,7 @@ impl QuicStreamReceiver {
|
||||
}
|
||||
|
||||
let send_to_self = global_ctx.is_ip_local_virtual_ip(&dst_ip);
|
||||
if send_to_self && global_ctx.no_tun() {
|
||||
dst_socket = format!("127.0.0.1:{}", dst_socket.port()).parse()?;
|
||||
}
|
||||
dst_socket = normalize_dst_for_local_virtual_ip(&global_ctx, dst_socket);
|
||||
|
||||
let acl_handler = ProxyAclHandler {
|
||||
acl_filter: global_ctx.get_acl_filter().clone(),
|
||||
|
||||
@@ -9,7 +9,7 @@ use pnet::packet::ip::IpNextHeaderProtocols;
|
||||
use pnet::packet::ipv4::{Ipv4Packet, MutableIpv4Packet};
|
||||
use pnet::packet::tcp::{MutableTcpPacket, TcpPacket, ipv4_checksum};
|
||||
use socket2::{SockRef, TcpKeepalive};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU16};
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -40,6 +40,20 @@ use super::CidrSet;
|
||||
#[cfg(feature = "smoltcp")]
|
||||
use super::tokio_smoltcp::{self, Net, NetConfig, channel_device};
|
||||
|
||||
pub(crate) fn normalize_dst_for_local_virtual_ip(
|
||||
global_ctx: &GlobalCtx,
|
||||
dst: SocketAddr,
|
||||
) -> SocketAddr {
|
||||
if !global_ctx.is_ip_local_virtual_ip(&dst.ip()) {
|
||||
return dst;
|
||||
}
|
||||
|
||||
match dst {
|
||||
SocketAddr::V4(addr) => SocketAddr::new(Ipv4Addr::LOCALHOST.into(), addr.port()),
|
||||
SocketAddr::V6(addr) => SocketAddr::new(Ipv6Addr::LOCALHOST.into(), addr.port()),
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub(crate) trait NatDstConnector: Send + Sync + Clone + 'static {
|
||||
type DstStream: AsyncRead + AsyncWrite + Unpin + Send;
|
||||
@@ -762,13 +776,7 @@ impl<C: NatDstConnector> TcpProxy<C> {
|
||||
return;
|
||||
}
|
||||
|
||||
let nat_dst = if global_ctx.is_ip_local_virtual_ip(&nat_entry.real_dst.ip()) {
|
||||
format!("127.0.0.1:{}", nat_entry.real_dst.port())
|
||||
.parse()
|
||||
.unwrap()
|
||||
} else {
|
||||
nat_entry.real_dst
|
||||
};
|
||||
let nat_dst = normalize_dst_for_local_virtual_ip(&global_ctx, nat_entry.real_dst);
|
||||
|
||||
global_ctx
|
||||
.stats_manager()
|
||||
@@ -1033,3 +1041,122 @@ impl<C: NatDstConnector> TcpProxyRpcService<C> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
|
||||
use super::normalize_dst_for_local_virtual_ip;
|
||||
|
||||
#[tokio::test]
|
||||
async fn normalize_dst_for_local_virtual_ip_maps_to_loopback() {
|
||||
let global_ctx = crate::common::global_ctx::tests::get_mock_global_ctx();
|
||||
global_ctx.set_ipv4(Some("10.254.229.6/24".parse().unwrap()));
|
||||
|
||||
let local_virtual = SocketAddr::from(([10, 254, 229, 6], 22));
|
||||
let normalized = normalize_dst_for_local_virtual_ip(&global_ctx, local_virtual);
|
||||
assert_eq!(normalized, SocketAddr::from((Ipv4Addr::LOCALHOST, 22)));
|
||||
|
||||
let remote_virtual = SocketAddr::from(([10, 254, 229, 7], 22));
|
||||
assert_eq!(
|
||||
normalize_dst_for_local_virtual_ip(&global_ctx, remote_virtual),
|
||||
remote_virtual
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, target_os = "macos", feature = "tun"))]
|
||||
mod macos_utun_tests {
|
||||
use std::{net::SocketAddr, sync::Arc, time::Duration};
|
||||
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
||||
use super::{NatDstConnector as _, NatDstTcpConnector, normalize_dst_for_local_virtual_ip};
|
||||
use crate::{
|
||||
common::config::{ConfigLoader, TomlConfigLoader},
|
||||
instance::instance::Instance,
|
||||
};
|
||||
|
||||
async fn run_instance_with_utun(ipv4: &str, enable_kcp: bool, enable_quic: bool) -> Instance {
|
||||
let config = TomlConfigLoader::default();
|
||||
config.set_inst_name(format!("macos-utun-tcp-proxy-repro-{ipv4}"));
|
||||
config.set_ipv4(Some(ipv4.parse().unwrap()));
|
||||
config.set_ipv6(None);
|
||||
config.set_listeners(Vec::new());
|
||||
|
||||
let mut flags = config.get_flags();
|
||||
flags.enable_kcp_proxy = enable_kcp;
|
||||
flags.enable_quic_proxy = enable_quic;
|
||||
flags.use_smoltcp = false;
|
||||
config.set_flags(flags);
|
||||
|
||||
let mut instance = Instance::new(config);
|
||||
instance.run().await.expect(
|
||||
"failed to create macOS utun device; run this ignored reproducer with root privileges",
|
||||
);
|
||||
instance
|
||||
}
|
||||
|
||||
async fn assert_wildcard_listener_reachable_via_local_virtual_ip(
|
||||
ipv4: &str,
|
||||
enable_kcp: bool,
|
||||
enable_quic: bool,
|
||||
) {
|
||||
let mut instance = run_instance_with_utun(ipv4, enable_kcp, enable_quic).await;
|
||||
let virtual_ip = instance.get_global_ctx().get_ipv4().unwrap().address();
|
||||
|
||||
let listener = Arc::new(TcpListener::bind("0.0.0.0:0").await.unwrap());
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
|
||||
let baseline_listener = listener.clone();
|
||||
let baseline_accept = tokio::spawn(async move { baseline_listener.accept().await });
|
||||
let baseline_connect = TcpStream::connect((std::net::Ipv4Addr::LOCALHOST, port));
|
||||
let (baseline_connect, baseline_accept) = tokio::join!(baseline_connect, baseline_accept);
|
||||
baseline_connect.unwrap();
|
||||
baseline_accept.unwrap().unwrap();
|
||||
|
||||
let test_listener = listener.clone();
|
||||
let mut accept = tokio::spawn(async move { test_listener.accept().await });
|
||||
let dst = SocketAddr::new(virtual_ip.into(), port);
|
||||
let dst = normalize_dst_for_local_virtual_ip(&instance.get_global_ctx(), dst);
|
||||
assert_eq!(dst.ip(), std::net::Ipv4Addr::LOCALHOST);
|
||||
let connect = NatDstTcpConnector {}.connect("0.0.0.0:0".parse().unwrap(), dst);
|
||||
|
||||
let connect = tokio::time::timeout(Duration::from_secs(3), connect).await;
|
||||
let accept_result = tokio::time::timeout(Duration::from_secs(1), &mut accept).await;
|
||||
|
||||
if accept_result.is_err() {
|
||||
accept.abort();
|
||||
}
|
||||
instance.clear_resources().await;
|
||||
|
||||
match connect {
|
||||
Ok(Ok(_)) => {}
|
||||
Ok(Err(error)) => panic!("connect to local EasyTier virtual IP failed: {error:?}"),
|
||||
Err(_) => panic!("connect to local EasyTier virtual IP timed out"),
|
||||
}
|
||||
|
||||
match accept_result {
|
||||
Ok(Ok(Ok(_))) => {}
|
||||
Ok(Ok(Err(error))) => panic!("listener accept failed: {error:?}"),
|
||||
Ok(Err(error)) => panic!("listener task failed: {error:?}"),
|
||||
Err(_) => panic!(
|
||||
"listener did not accept connection to local EasyTier virtual IP {virtual_ip}:{port}"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires root and a real macOS utun device; covers issue #2296"]
|
||||
async fn macos_utun_kcp_proxy_dst_local_virtual_ip_reaches_wildcard_listener() {
|
||||
assert_wildcard_listener_reachable_via_local_virtual_ip("10.254.229.6/24", true, false)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires root and a real macOS utun device; covers issue #2296"]
|
||||
async fn macos_utun_quic_proxy_dst_local_virtual_ip_reaches_wildcard_listener() {
|
||||
assert_wildcard_listener_reachable_via_local_virtual_ip("10.254.230.6/24", false, true)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user