From 28cd6da5029606316de49851886ab525de87d7f6 Mon Sep 17 00:00:00 2001 From: KKRainbow <443152178@qq.com> Date: Thu, 25 Dec 2025 00:10:32 +0800 Subject: [PATCH] Add fake tcp tunnel (experimental) (#1673) support faketcp to avoid tcp-over-tcp problem. linux/macos/windows are supported. better to be used in internet env, the maximum performance is majorly limited by windivert/raw socket. --- .github/workflows/core.yml | 6 +- .github/workflows/gui.yml | 6 +- .gitignore | 1 + Cargo.lock | 61 +- .../src-tauri/tauri.windows.conf.json | 3 +- easytier/Cargo.toml | 7 + easytier/build.rs | 2 +- easytier/src/connector/mod.rs | 19 +- easytier/src/instance/listeners.rs | 7 +- easytier/src/peers/peer_conn.rs | 4 +- easytier/src/tunnel/common.rs | 5 + easytier/src/tunnel/fake_tcp/LICENSE | 6 + easytier/src/tunnel/fake_tcp/mod.rs | 482 +++++++ .../tunnel/fake_tcp/netfilter/linux_bpf.rs | 708 +++++++++++ .../tunnel/fake_tcp/netfilter/macos_bpf.rs | 1122 +++++++++++++++++ easytier/src/tunnel/fake_tcp/netfilter/mod.rs | 87 ++ .../src/tunnel/fake_tcp/netfilter/pnet.rs | 304 +++++ .../tunnel/fake_tcp/netfilter/windivert.rs | 196 +++ easytier/src/tunnel/fake_tcp/packet.rs | 159 +++ easytier/src/tunnel/fake_tcp/stack.rs | 561 +++++++++ easytier/src/tunnel/mod.rs | 16 +- easytier/third_party/arm64/WinDivert64.sys | 1 + easytier/third_party/i686/WinDivert32.sys | Bin 0 -> 79792 bytes easytier/third_party/{ => x86_64}/Packet.dll | Bin easytier/third_party/{ => x86_64}/Packet.lib | Bin easytier/third_party/x86_64/WinDivert64.sys | Bin 0 -> 94144 bytes easytier/third_party/{ => x86_64}/wintun.dll | Bin 27 files changed, 3744 insertions(+), 19 deletions(-) create mode 100644 easytier/src/tunnel/fake_tcp/LICENSE create mode 100644 easytier/src/tunnel/fake_tcp/mod.rs create mode 100644 easytier/src/tunnel/fake_tcp/netfilter/linux_bpf.rs create mode 100644 easytier/src/tunnel/fake_tcp/netfilter/macos_bpf.rs create mode 100644 easytier/src/tunnel/fake_tcp/netfilter/mod.rs create mode 100644 easytier/src/tunnel/fake_tcp/netfilter/pnet.rs create mode 100644 easytier/src/tunnel/fake_tcp/netfilter/windivert.rs create mode 100644 easytier/src/tunnel/fake_tcp/packet.rs create mode 100644 easytier/src/tunnel/fake_tcp/stack.rs create mode 100644 easytier/third_party/arm64/WinDivert64.sys create mode 100644 easytier/third_party/i686/WinDivert32.sys rename easytier/third_party/{ => x86_64}/Packet.dll (100%) rename easytier/third_party/{ => x86_64}/Packet.lib (100%) create mode 100644 easytier/third_party/x86_64/WinDivert64.sys rename easytier/third_party/{ => x86_64}/wintun.dll (100%) diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 12cdcc9b..79703a83 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -245,13 +245,13 @@ jobs: # windows is the only OS using a different convention for executable file name if [[ $OS =~ ^windows.*$ && $TARGET =~ ^x86_64.*$ ]]; then SUFFIX=.exe - cp easytier/third_party/*.dll ./artifacts/objects/ + cp easytier/third_party/x86_64/* ./artifacts/objects/ elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^i686.*$ ]]; then SUFFIX=.exe - cp easytier/third_party/i686/*.dll ./artifacts/objects/ + cp easytier/third_party/i686/* ./artifacts/objects/ elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^aarch64.*$ ]]; then SUFFIX=.exe - cp easytier/third_party/arm64/*.dll ./artifacts/objects/ + cp easytier/third_party/arm64/* ./artifacts/objects/ fi if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then TAG=$GITHUB_REF_NAME diff --git a/.github/workflows/gui.yml b/.github/workflows/gui.yml index c402ff9b..7ac89e40 100644 --- a/.github/workflows/gui.yml +++ b/.github/workflows/gui.yml @@ -170,11 +170,11 @@ jobs: if: ${{ matrix.OS == 'windows-latest' }} run: | if [[ $GUI_TARGET =~ ^aarch64.*$ ]]; then - cp ./easytier/third_party/arm64/*.dll ./easytier-gui/src-tauri/ + cp ./easytier/third_party/arm64/* ./easytier-gui/src-tauri/ elif [[ $GUI_TARGET =~ ^i686.*$ ]]; then - cp ./easytier/third_party/i686/*.dll ./easytier-gui/src-tauri/ + cp ./easytier/third_party/i686/* ./easytier-gui/src-tauri/ else - cp ./easytier/third_party/*.dll ./easytier-gui/src-tauri/ + cp ./easytier/third_party/x86_64/* ./easytier-gui/src-tauri/ fi - name: Build GUI diff --git a/.gitignore b/.gitignore index edebcc4e..9d45b2a2 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ node_modules .vite easytier-gui/src-tauri/*.dll +easytier-gui/src-tauri/*.sys /easytier-contrib/easytier-ohrs/dist/ .direnv diff --git a/Cargo.lock b/Cargo.lock index ef348d63..5c55c4db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2095,6 +2095,7 @@ dependencies = [ "bytecodec", "byteorder", "bytes", + "cfg-if", "chrono", "cidr", "clap", @@ -2107,6 +2108,7 @@ dependencies = [ "derive_builder", "easytier-rpc-build 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "encoding", + "flume 0.12.0", "futures", "futures-util", "gethostname 0.5.0", @@ -2195,6 +2197,7 @@ dependencies = [ "which 7.0.3", "wildmatch", "winapi", + "windivert", "windows 0.52.0", "windows-service", "windows-sys 0.52.0", @@ -2577,6 +2580,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "etherparse" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "827292ea592108849932ad8e30218f8b1f21c0dfd0696698a18b5d0aed62d990" +dependencies = [ + "arrayvec", +] + [[package]] name = "event-listener" version = "5.3.1" @@ -2615,6 +2627,9 @@ name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +dependencies = [ + "getrandom 0.2.15", +] [[package]] name = "fdeflate" @@ -2675,6 +2690,18 @@ dependencies = [ "spin", ] +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "fastrand", + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -8175,7 +8202,7 @@ checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" dependencies = [ "atoi", "chrono", - "flume", + "flume 0.11.0", "futures-channel", "futures-core", "futures-executor", @@ -10327,6 +10354,29 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windivert" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6b6833a760d1c36b489314a5541a12a39d162dc8341d8f6f400212b96d3df1" +dependencies = [ + "etherparse", + "thiserror 1.0.63", + "windivert-sys", + "windows 0.48.0", +] + +[[package]] +name = "windivert-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832bc4af9272458a8a64395b3aabe10dc4089546486fcbd0e19b9b6d28ba6e54" +dependencies = [ + "cc", + "thiserror 1.0.63", + "windows 0.48.0", +] + [[package]] name = "window-vibrancy" version = "0.6.0" @@ -10342,6 +10392,15 @@ dependencies = [ "windows-version", ] +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows" version = "0.52.0" diff --git a/easytier-gui/src-tauri/tauri.windows.conf.json b/easytier-gui/src-tauri/tauri.windows.conf.json index 43d71532..b71ceaa6 100644 --- a/easytier-gui/src-tauri/tauri.windows.conf.json +++ b/easytier-gui/src-tauri/tauri.windows.conf.json @@ -3,7 +3,8 @@ "externalBin": [], "resources": [ "./wintun.dll", - "./Packet.dll" + "./Packet.dll", + "./*.sys" ], "windows": { "webviewInstallMode": { diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index 86d8de6a..9e7fb99a 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -220,6 +220,10 @@ hmac = "0.12.1" sha2 = "0.10.8" shellexpand = "3.1.1" +# for fake tcp +flume = "0.12" +cfg-if = "1.0" + [target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies] machine-uid = "0.5.3" @@ -233,6 +237,9 @@ resolv-conf = "0.7.3" dbus = { version = "0.9.7", features = ["vendored"] } which = "7.0.3" +[target.'cfg(all(windows, any(target_arch = "x86_64", target_arch = "x86")))'.dependencies] +windivert = { version = "0.6.0", features = ["static"] } + [target.'cfg(windows)'.dependencies] windows = { version = "0.52.0", features = [ "Win32_Foundation", diff --git a/easytier/build.rs b/easytier/build.rs index adef2ef1..bd4e3c00 100644 --- a/easytier/build.rs +++ b/easytier/build.rs @@ -70,7 +70,7 @@ impl WindowsBuild { let target = std::env::var("TARGET").unwrap_or_default(); if target.contains("x86_64") { - println!("cargo:rustc-link-search=native=easytier/third_party/"); + println!("cargo:rustc-link-search=native=easytier/third_party/x86_64/"); } else if target.contains("i686") { println!("cargo:rustc-link-search=native=easytier/third_party/i686/"); } else if target.contains("aarch64") { diff --git a/easytier/src/connector/mod.rs b/easytier/src/connector/mod.rs index 4b50fb54..a3ea8644 100644 --- a/easytier/src/connector/mod.rs +++ b/easytier/src/connector/mod.rs @@ -12,8 +12,9 @@ use crate::tunnel::wireguard::{WgConfig, WgTunnelConnector}; use crate::{ common::{error::Error, global_ctx::ArcGlobalCtx, idn, network::IPCollector}, tunnel::{ - check_scheme_and_get_socket_addr, ring::RingTunnelConnector, tcp::TcpTunnelConnector, - udp::UdpTunnelConnector, IpVersion, TunnelConnector, + check_scheme_and_get_socket_addr, fake_tcp::FakeTcpTunnelConnector, + ring::RingTunnelConnector, tcp::TcpTunnelConnector, udp::UdpTunnelConnector, IpVersion, + TunnelConnector, }, }; @@ -157,6 +158,20 @@ pub async fn create_connector_by_url( let connector = dns_connector::DNSTunnelConnector::new(url, global_ctx.clone()); Box::new(connector) } + "faketcp" => { + let dst_addr = + check_scheme_and_get_socket_addr::(&url, "faketcp", ip_version).await?; + let mut connector = FakeTcpTunnelConnector::new(url); + if global_ctx.config.get_flags().bind_device { + set_bind_addr_for_peer_connector( + &mut connector, + dst_addr.is_ipv4(), + &global_ctx.get_ip_collector(), + ) + .await; + } + Box::new(connector) + } _ => { return Err(Error::InvalidUrl(url.into())); } diff --git a/easytier/src/instance/listeners.rs b/easytier/src/instance/listeners.rs index 2fb8ed28..27e7fc17 100644 --- a/easytier/src/instance/listeners.rs +++ b/easytier/src/instance/listeners.rs @@ -21,8 +21,8 @@ use crate::{ }, peers::peer_manager::PeerManager, tunnel::{ - ring::RingTunnelListener, tcp::TcpTunnelListener, udp::UdpTunnelListener, Tunnel, - TunnelListener, + fake_tcp::FakeTcpTunnelListener, ring::RingTunnelListener, tcp::TcpTunnelListener, + udp::UdpTunnelListener, Tunnel, TunnelListener, }, }; @@ -49,6 +49,7 @@ pub fn get_listener_by_url( use crate::tunnel::websocket::WSTunnelListener; Box::new(WSTunnelListener::new(l.clone())) } + "faketcp" => Box::new(FakeTcpTunnelListener::new(l.clone())), _ => { return Err(Error::InvalidUrl(l.to_string())); } @@ -143,7 +144,7 @@ impl ListenerManage && !is_url_host_ipv6(&l) && is_url_host_unspecified(&l) // quic enables dual-stack by default, may conflict with v4 listener - && l.scheme() != "quic" + && l.scheme() != "quic" && l.scheme() != "faketcp" { let mut ipv6_listener = l.clone(); ipv6_listener diff --git a/easytier/src/peers/peer_conn.rs b/easytier/src/peers/peer_conn.rs index 98d322d8..c6f2c002 100644 --- a/easytier/src/peers/peer_conn.rs +++ b/easytier/src/peers/peer_conn.rs @@ -223,8 +223,8 @@ impl PeerConn { if peer_mgr_hdr.packet_type != PacketType::HandShake as u8 { return Err(Error::WaitRespError(format!( - "unexpected packet type: {:?}", - peer_mgr_hdr.packet_type + "unexpected packet type: {:?}, packet: {:?}", + peer_mgr_hdr.packet_type, rsp ))); } diff --git a/easytier/src/tunnel/common.rs b/easytier/src/tunnel/common.rs index b5ac636d..50b81e6b 100644 --- a/easytier/src/tunnel/common.rs +++ b/easytier/src/tunnel/common.rs @@ -534,6 +534,11 @@ pub mod tests { let tunnel = c_netns.run_async(|| connector.connect()).await.unwrap(); println!("connect: {:?}", tunnel.info()); + if connector.remote_url().scheme() == "faketcp" { + // listener need some time to start capturing packet + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + assert_eq!( url::Url::from(tunnel.info().unwrap().remote_addr.unwrap()), connector.remote_url(), diff --git a/easytier/src/tunnel/fake_tcp/LICENSE b/easytier/src/tunnel/fake_tcp/LICENSE new file mode 100644 index 00000000..e1b074a5 --- /dev/null +++ b/easytier/src/tunnel/fake_tcp/LICENSE @@ -0,0 +1,6 @@ +Copyright 2021-2025 Datong Sun dndx@idndx.com + +Licensed under the Apache License, Version 2.0 or the MIT license , at your option. Files in the project may +not be copied, modified, or distributed except according to those terms. \ No newline at end of file diff --git a/easytier/src/tunnel/fake_tcp/mod.rs b/easytier/src/tunnel/fake_tcp/mod.rs new file mode 100644 index 00000000..6f77a5e7 --- /dev/null +++ b/easytier/src/tunnel/fake_tcp/mod.rs @@ -0,0 +1,482 @@ +mod netfilter; +mod packet; +mod stack; + +use std::net::{IpAddr, Ipv4Addr, UdpSocket}; +use std::sync::Arc; +use std::{net::SocketAddr, pin::Pin}; + +use bytes::BytesMut; +use pnet::datalink; +use pnet::util::MacAddr; +use tokio::io::AsyncReadExt; +use tokio::net::TcpStream; +use tokio::sync::Mutex; + +use crate::common::scoped_task::ScopedTask; +use crate::tunnel::fake_tcp::netfilter::create_tun; +use crate::tunnel::{common::TunnelWrapper, Tunnel, TunnelError, TunnelInfo, TunnelListener}; + +use futures::Future; + +use dashmap::DashMap; + +struct IpToIfNameCache { + ip_to_ifname: DashMap)>, +} + +impl IpToIfNameCache { + fn new() -> Self { + Self { + ip_to_ifname: DashMap::new(), + } + } + + fn reload_ip_to_ifname(&self) { + self.ip_to_ifname.clear(); + let interfaces = datalink::interfaces(); + for iface in interfaces { + for ip in iface.ips.iter() { + self.ip_to_ifname + .insert(ip.ip(), (iface.name.clone(), iface.mac)); + } + } + } + + fn get_ifname(&self, ip: &IpAddr) -> Option<(String, Option)> { + if let Some(ifname) = self.ip_to_ifname.get(ip) { + Some(ifname.clone()) + } else { + self.reload_ip_to_ifname(); + self.ip_to_ifname.get(ip).map(|s| s.clone()) + } + } +} + +fn get_faketcp_tunnel_type_str(driver_type: &str) -> String { + format!("faketcp_{}", driver_type) +} + +pub struct FakeTcpTunnelListener { + addr: url::Url, + os_listener: Option, + // interface_name -> fake tcp stack + stack_map: DashMap>>, + // a cache from ip addr to interface name + ip_to_ifname: IpToIfNameCache, +} + +impl FakeTcpTunnelListener { + pub fn new(addr: url::Url) -> Self { + // Define filter: Capture all packets (or refine this if needed) + // For FakeTCP, we probably want to capture packets destined to us? + // But `stack::Stack` handles IP/TCP logic. + // Maybe we just capture everything for now as a raw tunnel? + // Or better, filter based on some criteria? + // The user said "satisfy filter function". + // Let's create a filter that accepts everything for now, or maybe only IP packets? + FakeTcpTunnelListener { + addr, + os_listener: None, + stack_map: DashMap::new(), + ip_to_ifname: IpToIfNameCache::new(), + } + } + + async fn do_accept(&mut self) -> Result { + loop { + match self.os_listener.as_mut().unwrap().accept().await { + Ok((s, remote_addr)) => { + let Ok(local_addr) = s.local_addr() else { + tracing::warn!("accept fail with local_addr error"); + continue; + }; + let Some((interface_name, mac)) = + self.ip_to_ifname.get_ifname(&local_addr.ip()) + else { + tracing::warn!("accept fail with interface_name error"); + continue; + }; + return Ok(AcceptResult { + socket: s, + local_addr, + remote_addr, + interface_name, + mac, + }); + } + Err(e) => { + use std::io::ErrorKind::*; + if matches!( + e.kind(), + NotConnected | ConnectionAborted | ConnectionRefused | ConnectionReset + ) { + tracing::warn!(?e, "accept fail with retryable error: {:?}", e); + continue; + } + tracing::warn!(?e, "accept fail"); + return Err(e.into()); + } + } + } + } + + async fn get_stack( + &self, + accept_result: &AcceptResult, + ) -> Result>, TunnelError> { + let local_socket_addr = accept_result.local_addr; + + let interface_name = &accept_result.interface_name; + + let (local_ip, local_ip6) = match local_socket_addr.ip() { + IpAddr::V4(ip) => (Some(ip), None), + IpAddr::V6(ip) => (None, Some(ip)), + }; + + let ret = self + .stack_map + .entry(interface_name.to_string()) + .or_insert_with(|| { + let tun = create_tun(interface_name, None, local_socket_addr); + tracing::info!( + ?local_socket_addr, + "create new stack with interface_name: {:?}", + interface_name + ); + // TODO: Get local MAC address of the interface + Arc::new(Mutex::new(stack::Stack::new( + tun, + local_ip.unwrap_or(Ipv4Addr::UNSPECIFIED), + local_ip6, + accept_result.mac, + ))) + }) + .clone(); + + Ok(ret) + } +} + +fn build_os_socket_reader_task(mut socket: TcpStream) -> ScopedTask<()> { + let os_socket_reader_task: ScopedTask<()> = tokio::spawn(async move { + // read the os socket until it's closed + let mut buf = [0u8; 1024]; + while let Ok(size) = socket.read(&mut buf).await { + tracing::trace!("read {} bytes from os socket", size); + if size == 0 { + break; + } + } + tracing::info!("FakeTcpTunnelListener os socket closed"); + }) + .into(); + os_socket_reader_task +} + +#[derive(Debug)] +struct AcceptResult { + socket: TcpStream, + local_addr: SocketAddr, + remote_addr: SocketAddr, + interface_name: String, + mac: Option, +} + +#[async_trait::async_trait] +impl TunnelListener for FakeTcpTunnelListener { + async fn listen(&mut self) -> Result<(), TunnelError> { + let port = self.addr.port().unwrap_or(0); + let bind_addr = crate::tunnel::check_scheme_and_get_socket_addr::( + &self.addr, + "faketcp", + crate::tunnel::IpVersion::Both, + ) + .await?; + let os_listener = tokio::net::TcpListener::bind(bind_addr).await?; + tracing::info!(port, "FakeTcpTunnelListener listening"); + self.os_listener = Some(os_listener); + // self.stack.lock().await.listen(port); + Ok(()) + } + + async fn accept(&mut self) -> Result, TunnelError> { + tracing::debug!("FakeTcpTunnelListener waiting for accept"); + let res = self.do_accept().await?; + let stack = self.get_stack(&res).await?; + let socket = stack + .lock() + .await + .alloc_established_socket(res.local_addr, res.remote_addr, stack::State::Established) + .await; + + tracing::info!( + ?res, + remote = socket.remote_addr().to_string(), + "FakeTcpTunnelListener accepted connection" + ); + + let info = TunnelInfo { + tunnel_type: get_faketcp_tunnel_type_str(stack.lock().await.driver_type()), + local_addr: Some(self.local_url().into()), + remote_addr: Some( + crate::tunnel::build_url_from_socket_addr( + &socket.remote_addr().to_string(), + "faketcp", + ) + .into(), + ), + }; + + // We treat the fake tcp socket as a datagram tunnel directly + // The reader/writer will interface with the socket using recv_bytes/send + // We need to adapt the socket to ZCPacketStream and ZCPacketSink + + // Since FakeTcpTunnel is a datagram tunnel, we don't need FramedReader/Writer (which are for stream based tunnels like TCP) + // We should wrap the socket into something that produces/consumes ZCPacket directly. + + let socket = Arc::new(socket); + let reader = FakeTcpStream::new(socket.clone()); + let writer = FakeTcpSink::new(socket); + + Ok(Box::new(TunnelWrapper::new_with_associate_data( + reader, + writer, + Some(info), + Some(Box::new(build_os_socket_reader_task(res.socket))), + ))) + } + + fn local_url(&self) -> url::Url { + self.addr.clone() + } +} + +pub struct FakeTcpTunnelConnector { + addr: url::Url, + ip_to_if_name: IpToIfNameCache, +} + +impl FakeTcpTunnelConnector { + pub fn new(addr: url::Url) -> Self { + FakeTcpTunnelConnector { + addr, + ip_to_if_name: IpToIfNameCache::new(), + } + } +} + +fn get_local_ip_for_destination(destination: IpAddr) -> Option { + // 使用一个不可路由的、私有的、或回环地址创建一个临时的 socket,让内核自动选择源接口。 + // 对于 IPv4,使用 0.0.0.0; 对于 IPv6,使用 :: + let bind_addr = if destination.is_ipv4() { + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) + } else { + IpAddr::V6(std::net::Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)) + }; + + // 绑定到一个临时端口 (0) + let socket = UdpSocket::bind((bind_addr, 0)).ok()?; + + // 尝试连接到目标地址。这不会真正发送数据包,只是让内核确定路由。 + socket.connect((destination, 80)).ok()?; // 使用一个常见的端口,例如 80 + + // 获取 socket 的本地地址信息 + socket.local_addr().map(|addr| addr.ip()).ok() +} + +#[async_trait::async_trait] +impl crate::tunnel::TunnelConnector for FakeTcpTunnelConnector { + async fn connect(&mut self) -> Result, TunnelError> { + let remote_addr = crate::tunnel::check_scheme_and_get_socket_addr::( + &self.addr, + "faketcp", + crate::tunnel::IpVersion::Both, + ) + .await?; + let local_ip = get_local_ip_for_destination(remote_addr.ip()) + .ok_or(TunnelError::InternalError("Failed to get local ip".into()))?; + + let os_socket = tokio::net::TcpSocket::new_v4()?; + os_socket.bind("0.0.0.0:0".parse().unwrap())?; + let local_port = os_socket.local_addr()?.port(); + let local_addr = SocketAddr::new(local_ip, local_port); + + let (interface_name, mac) = + self.ip_to_if_name + .get_ifname(&local_ip) + .ok_or(TunnelError::InternalError( + "Failed to get interface name".into(), + ))?; + + let (local_ip, local_ip6) = match local_ip { + IpAddr::V4(ip) => (Some(ip), None), + IpAddr::V6(ip) => (None, Some(ip)), + }; + + let tun = create_tun(&interface_name, Some(remote_addr), local_addr); + let local_ip = local_ip.unwrap_or("0.0.0.0".parse().unwrap()); + let mut stack = stack::Stack::new(tun, local_ip, local_ip6, mac); + let driver_type = stack.driver_type(); + + let socket = stack + .alloc_established_socket(local_addr, remote_addr, stack::State::SynSent) + .await; + + let os_stream = os_socket.connect(remote_addr).await?; + + tracing::info!(?remote_addr, "FakeTcpTunnelConnector connecting"); + + socket.recv_bytes().await.ok_or(TunnelError::InternalError( + "Failed to recv bytes to establish connection".into(), + ))?; + + tracing::info!(local_addr = ?socket.local_addr(), "FakeTcpTunnelConnector connected"); + + let info = TunnelInfo { + tunnel_type: get_faketcp_tunnel_type_str(driver_type), + local_addr: Some( + crate::tunnel::build_url_from_socket_addr( + &socket.local_addr().to_string(), + "faketcp", + ) + .into(), + ), + remote_addr: Some(self.addr.clone().into()), + }; + + let socket = Arc::new(socket); + let reader = FakeTcpStream::new(socket.clone()); + let writer = FakeTcpSink::new(socket); + + Ok(Box::new(TunnelWrapper::new_with_associate_data( + reader, + writer, + Some(info), + Some(Box::new((build_os_socket_reader_task(os_stream), stack))), + ))) + } + + fn remote_url(&self) -> url::Url { + self.addr.clone() + } +} + +use crate::tunnel::packet_def::{ZCPacket, ZCPacketType}; +use crate::tunnel::{SinkError, SinkItem, StreamItem}; +use futures::{Sink, Stream}; +use std::task::{Context as TaskContext, Poll}; + +struct FakeTcpStream { + socket: Arc, + #[allow(clippy::type_complexity)] + recv_fut: Option>> + Send + Sync>>>, +} + +impl FakeTcpStream { + fn new(socket: Arc) -> Self { + Self { + socket, + recv_fut: None, + } + } +} + +impl Stream for FakeTcpStream { + type Item = StreamItem; + + fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { + let s = self.get_mut(); + if s.recv_fut.is_none() { + let socket = s.socket.clone(); + s.recv_fut = Some(Box::pin(async move { socket.recv_bytes().await })); + } + + match s.recv_fut.as_mut().unwrap().as_mut().poll(cx) { + Poll::Ready(Some(data)) => { + let mut buf = BytesMut::new(); + buf.extend_from_slice(&data); + let packet = ZCPacket::new_from_buf(buf, ZCPacketType::DummyTunnel); + + s.recv_fut = None; + + Poll::Ready(Some(Ok(packet))) + } + Poll::Ready(None) => { + // 连接关闭 + s.recv_fut = None; + Poll::Ready(None) + } + Poll::Pending => Poll::Pending, + } + } +} + +struct FakeTcpSink { + socket: Arc, +} + +impl FakeTcpSink { + fn new(socket: Arc) -> Self { + Self { socket } + } +} + +impl Sink for FakeTcpSink { + type Error = SinkError; + + fn poll_ready( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + + fn start_send(self: Pin<&mut Self>, item: SinkItem) -> Result<(), Self::Error> { + // We need to send the packet as bytes + // The item is ZCPacket, which has into_bytes() method + let bytes = item.convert_type(ZCPacketType::DummyTunnel).into_bytes(); + + // Let's just spawn for now as a simple implementation, noting the limitation. + self.socket.try_send(&bytes); + + Ok(()) + } + + fn poll_flush( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_close( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + ) -> Poll> { + self.socket.close(); + Poll::Ready(Ok(())) + } +} + +#[cfg(test)] +mod tests { + use crate::tunnel::common::tests::_tunnel_pingpong; + + use super::*; + + #[tokio::test] + async fn faketcp_pingpong() { + #[cfg(target_family = "unix")] + { + if unsafe { nix::libc::geteuid() } != 0 { + return; + } + } + + let listener = FakeTcpTunnelListener::new("faketcp://0.0.0.0:31011".parse().unwrap()); + let connector = FakeTcpTunnelConnector::new("faketcp://127.0.0.1:31011".parse().unwrap()); + + _tunnel_pingpong(listener, connector).await + } +} diff --git a/easytier/src/tunnel/fake_tcp/netfilter/linux_bpf.rs b/easytier/src/tunnel/fake_tcp/netfilter/linux_bpf.rs new file mode 100644 index 00000000..8e5379b9 --- /dev/null +++ b/easytier/src/tunnel/fake_tcp/netfilter/linux_bpf.rs @@ -0,0 +1,708 @@ +use bytes::Bytes; +use bytes::BytesMut; +use nix::libc; +use std::ffi::CString; +use std::io; +use std::mem; +use std::net::IpAddr; +use std::net::SocketAddr; +use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; +use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering}; +use std::sync::Arc; +use tokio::sync::Mutex; + +use crate::tunnel::fake_tcp::stack; + +const ETH_HDR_LEN: usize = 14; +const ETH_TYPE_OFFSET: u32 = 12; +const ETHERTYPE_IPV4: u32 = 0x0800; +const ETHERTYPE_IPV6: u32 = 0x86DD; +const IPPROTO_TCP_U32: u32 = 6; + +const BPF_LD: u16 = 0x00; +const BPF_LDX: u16 = 0x01; +const BPF_JMP: u16 = 0x05; +const BPF_RET: u16 = 0x06; + +const BPF_W: u16 = 0x00; +const BPF_H: u16 = 0x08; +const BPF_B: u16 = 0x10; + +const BPF_ABS: u16 = 0x20; +const BPF_IND: u16 = 0x40; +const BPF_MSH: u16 = 0xa0; + +const BPF_JA: u16 = 0x00; +const BPF_JEQ: u16 = 0x10; + +const BPF_K: u16 = 0x00; + +fn stmt(code: u16, k: u32) -> libc::sock_filter { + libc::sock_filter { + code, + jt: 0, + jf: 0, + k, + } +} + +fn jeq(k: u32, jt: u8, jf: u8) -> libc::sock_filter { + libc::sock_filter { + code: BPF_JMP | BPF_JEQ | BPF_K, + jt, + jf, + k, + } +} + +fn ja(k: u32) -> libc::sock_filter { + libc::sock_filter { + code: BPF_JMP | BPF_JA, + jt: 0, + jf: 0, + k, + } +} + +#[derive(Clone, Copy)] +struct Label(usize); + +struct JeqPatch { + idx: usize, + t: Label, + f: Label, +} + +struct JaPatch { + idx: usize, + target: Label, +} + +struct BpfBuilder { + insns: Vec, + labels: Vec>, + jeq_patches: Vec, + ja_patches: Vec, +} + +impl BpfBuilder { + fn new() -> Self { + Self { + insns: Vec::new(), + labels: Vec::new(), + jeq_patches: Vec::new(), + ja_patches: Vec::new(), + } + } + + fn new_label(&mut self) -> Label { + let idx = self.labels.len(); + self.labels.push(None); + Label(idx) + } + + fn set_label(&mut self, label: Label) { + self.labels[label.0] = Some(self.insns.len()); + } + + fn push(&mut self, insn: libc::sock_filter) { + self.insns.push(insn); + } + + fn push_jeq(&mut self, k: u32, t: Label, f: Label) { + let idx = self.insns.len(); + self.insns.push(jeq(k, 0, 0)); + self.jeq_patches.push(JeqPatch { idx, t, f }); + } + + fn push_ja(&mut self, target: Label) { + let idx = self.insns.len(); + self.insns.push(ja(0)); + self.ja_patches.push(JaPatch { idx, target }); + } + + fn finish(mut self) -> io::Result> { + for patch in self.jeq_patches { + let JeqPatch { idx, t, f } = patch; + let cur = idx + 1; + let t_pos = + self.labels.get(t.0).and_then(|v| *v).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "unresolved label") + })?; + let f_pos = + self.labels.get(f.0).and_then(|v| *v).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "unresolved label") + })?; + + if t_pos < cur || f_pos < cur { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "backward bpf jump", + )); + } + + let jt: u8 = (t_pos - cur) + .try_into() + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "bpf jump too far"))?; + let jf: u8 = (f_pos - cur) + .try_into() + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "bpf jump too far"))?; + + self.insns[idx].jt = jt; + self.insns[idx].jf = jf; + } + + for patch in self.ja_patches { + let JaPatch { idx, target } = patch; + let cur = idx + 1; + let t_pos = + self.labels.get(target.0).and_then(|v| *v).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "unresolved label") + })?; + if t_pos < cur { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "backward bpf jump", + )); + } + self.insns[idx].k = (t_pos - cur) as u32; + } + + Ok(self.insns) + } +} + +fn build_tcp_filter( + src_addr: Option, + dst_addr: SocketAddr, +) -> io::Result> { + if let Some(src) = src_addr { + if src.is_ipv4() != dst_addr.is_ipv4() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "src/dst addr family mismatch", + )); + } + } + + let mut b = BpfBuilder::new(); + let l_check_ipv6 = b.new_label(); + let l_ipv4 = b.new_label(); + let l_ipv6 = b.new_label(); + let l_accept = b.new_label(); + let l_reject = b.new_label(); + + b.push(stmt(BPF_LD | BPF_H | BPF_ABS, ETH_TYPE_OFFSET)); + b.push_jeq(ETHERTYPE_IPV4, l_ipv4, l_check_ipv6); + + b.set_label(l_check_ipv6); + b.push_jeq(ETHERTYPE_IPV6, l_ipv6, l_reject); + + if dst_addr.is_ipv4() { + b.set_label(l_ipv4); + let l_v4_proto_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_B | BPF_ABS, (ETH_HDR_LEN + 9) as u32)); + b.push_jeq(IPPROTO_TCP_U32, l_v4_proto_ok, l_reject); + + b.set_label(l_v4_proto_ok); + let dst_ip = match dst_addr.ip() { + IpAddr::V4(ip) => u32::from(ip), + _ => unreachable!(), + }; + let l_v4_dstip_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_W | BPF_ABS, (ETH_HDR_LEN + 16) as u32)); + b.push_jeq(dst_ip, l_v4_dstip_ok, l_reject); + + b.set_label(l_v4_dstip_ok); + if let Some(src) = src_addr { + let src_ip = match src.ip() { + IpAddr::V4(ip) => u32::from(ip), + _ => unreachable!(), + }; + let l_v4_srcip_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_W | BPF_ABS, (ETH_HDR_LEN + 12) as u32)); + b.push_jeq(src_ip, l_v4_srcip_ok, l_reject); + b.set_label(l_v4_srcip_ok); + } + + b.push(stmt(BPF_LDX | BPF_B | BPF_MSH, ETH_HDR_LEN as u32)); + + let l_v4_dstport_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_H | BPF_IND, (ETH_HDR_LEN + 2) as u32)); + b.push_jeq(dst_addr.port() as u32, l_v4_dstport_ok, l_reject); + + b.set_label(l_v4_dstport_ok); + if let Some(src) = src_addr { + b.push(stmt(BPF_LD | BPF_H | BPF_IND, ETH_HDR_LEN as u32)); + b.push_jeq(src.port() as u32, l_accept, l_reject); + } else { + b.push_ja(l_accept); + } + } else { + b.set_label(l_ipv6); + let l_v6_proto_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_B | BPF_ABS, (ETH_HDR_LEN + 6) as u32)); + b.push_jeq(IPPROTO_TCP_U32, l_v6_proto_ok, l_reject); + + b.set_label(l_v6_proto_ok); + let dst_ip = match dst_addr.ip() { + IpAddr::V6(ip) => ip.octets(), + _ => unreachable!(), + }; + for (i, chunk) in dst_ip.chunks_exact(4).enumerate() { + let off = ETH_HDR_LEN + 24 + (i * 4); + let v = u32::from_be_bytes(chunk.try_into().unwrap()); + let l_v6_dstip_word_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_W | BPF_ABS, off as u32)); + b.push_jeq(v, l_v6_dstip_word_ok, l_reject); + b.set_label(l_v6_dstip_word_ok); + } + + if let Some(src) = src_addr { + let src_ip = match src.ip() { + IpAddr::V6(ip) => ip.octets(), + _ => unreachable!(), + }; + for (i, chunk) in src_ip.chunks_exact(4).enumerate() { + let off = ETH_HDR_LEN + 8 + (i * 4); + let v = u32::from_be_bytes(chunk.try_into().unwrap()); + let l_v6_srcip_word_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_W | BPF_ABS, off as u32)); + b.push_jeq(v, l_v6_srcip_word_ok, l_reject); + b.set_label(l_v6_srcip_word_ok); + } + } + + let l_v6_dstport_ok = b.new_label(); + b.push(stmt( + BPF_LD | BPF_H | BPF_ABS, + (ETH_HDR_LEN + 40 + 2) as u32, + )); + b.push_jeq(dst_addr.port() as u32, l_v6_dstport_ok, l_reject); + + b.set_label(l_v6_dstport_ok); + if let Some(src) = src_addr { + b.push(stmt(BPF_LD | BPF_H | BPF_ABS, (ETH_HDR_LEN + 40) as u32)); + b.push_jeq(src.port() as u32, l_accept, l_reject); + } else { + b.push_ja(l_accept); + } + } + + b.set_label(l_accept); + b.push(stmt(BPF_RET | BPF_K, 0xFFFF)); + + b.set_label(l_reject); + if dst_addr.is_ipv4() { + b.set_label(l_ipv6); + } else { + b.set_label(l_ipv4); + } + b.push(stmt(BPF_RET | BPF_K, 0)); + + b.finish() +} + +pub struct LinuxBpfTun { + fd: OwnedFd, + ifindex: i32, + stop: Arc, + worker: Option>, + recv_queue: Mutex>>, +} + +impl LinuxBpfTun { + pub fn new( + interface_name: &str, + src_addr: Option, + dst_addr: SocketAddr, + ) -> io::Result { + let c_ifname = CString::new(interface_name) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid interface name"))?; + let ifindex = unsafe { libc::if_nametoindex(c_ifname.as_ptr()) as i32 }; + if ifindex <= 0 { + return Err(io::Error::new( + io::ErrorKind::NotFound, + "interface not found", + )); + } + + let proto: i32 = (libc::ETH_P_ALL as u16).to_be() as i32; + let fd = unsafe { libc::socket(libc::AF_PACKET, libc::SOCK_RAW, proto) }; + if fd < 0 { + return Err(io::Error::last_os_error()); + } + let fd = unsafe { OwnedFd::from_raw_fd(fd) }; + + let mut addr: libc::sockaddr_ll = unsafe { mem::zeroed() }; + addr.sll_family = libc::AF_PACKET as u16; + addr.sll_protocol = (libc::ETH_P_ALL as u16).to_be(); + addr.sll_ifindex = ifindex; + + let bind_ret = unsafe { + libc::bind( + fd.as_raw_fd(), + &addr as *const _ as *const libc::sockaddr, + mem::size_of::() as u32, + ) + }; + if bind_ret != 0 { + return Err(io::Error::last_os_error()); + } + + let filter = build_tcp_filter(src_addr, dst_addr)?; + let mut prog = libc::sock_fprog { + len: filter + .len() + .try_into() + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "bpf program too long"))?, + filter: filter.as_ptr() as *mut libc::sock_filter, + }; + let opt_ret = unsafe { + libc::setsockopt( + fd.as_raw_fd(), + libc::SOL_SOCKET, + libc::SO_ATTACH_FILTER, + &mut prog as *mut _ as *mut libc::c_void, + mem::size_of::() as u32, + ) + }; + if opt_ret != 0 { + return Err(io::Error::last_os_error()); + } + + let timeout = libc::timeval { + tv_sec: 0, + tv_usec: 200_000, + }; + let _ = unsafe { + libc::setsockopt( + fd.as_raw_fd(), + libc::SOL_SOCKET, + libc::SO_RCVTIMEO, + &timeout as *const _ as *const libc::c_void, + mem::size_of::() as u32, + ) + }; + + let stop = Arc::new(AtomicBool::new(false)); + let (tx, rx) = tokio::sync::mpsc::channel(1024); + let stop_clone = stop.clone(); + let read_fd = fd.as_raw_fd(); + + let worker = std::thread::spawn(move || { + let mut buf = vec![0u8; 65536]; + while !stop_clone.load(AtomicOrdering::Relaxed) { + let n = unsafe { + libc::recv(read_fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len(), 0) + }; + if n < 0 { + let err = io::Error::last_os_error(); + if matches!( + err.kind(), + io::ErrorKind::Interrupted | io::ErrorKind::WouldBlock + ) { + continue; + } + break; + } + if n == 0 { + continue; + } + let data = buf[..(n as usize)].to_vec(); + if tx.blocking_send(data).is_err() { + break; + } + } + }); + + tracing::info!( + interface_name, + ifindex, + "LinuxBpfTun created with filter {:?}", + filter + ); + + Ok(Self { + fd, + ifindex, + stop, + worker: Some(worker), + recv_queue: Mutex::new(rx), + }) + } +} + +impl Drop for LinuxBpfTun { + fn drop(&mut self) { + self.stop.store(true, AtomicOrdering::Relaxed); + let _ = unsafe { libc::shutdown(self.fd.as_raw_fd(), libc::SHUT_RD) }; + if let Some(worker) = self.worker.take() { + let _ = worker.join(); + } + } +} + +#[async_trait::async_trait] +impl stack::Tun for LinuxBpfTun { + async fn recv(&self, packet: &mut BytesMut) -> Result { + let mut rx = self.recv_queue.lock().await; + match rx.recv().await { + Some(data) => { + packet.extend_from_slice(&data); + Ok(data.len()) + } + None => Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "LinuxBpfTun channel closed", + )), + } + } + + fn try_send(&self, packet: &Bytes) -> Result<(), std::io::Error> { + if packet.len() < 6 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "packet too short", + )); + } + + let mut addr: libc::sockaddr_ll = unsafe { mem::zeroed() }; + addr.sll_family = libc::AF_PACKET as u16; + addr.sll_protocol = (libc::ETH_P_ALL as u16).to_be(); + addr.sll_ifindex = self.ifindex; + addr.sll_halen = 6; + addr.sll_addr[..6].copy_from_slice(&packet[..6]); + + let ret = unsafe { + libc::sendto( + self.fd.as_raw_fd(), + packet.as_ptr() as *const libc::c_void, + packet.len(), + 0, + &addr as *const _ as *const libc::sockaddr, + mem::size_of::() as u32, + ) + }; + if ret < 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + } + + fn driver_type(&self) -> &'static str { + "linux_bpf" + } +} + +#[cfg(all(test, target_os = "linux"))] +mod tests { + use super::*; + + use crate::tunnel::fake_tcp::packet::build_tcp_packet; + use crate::tunnel::fake_tcp::stack::Tun; + use pnet::datalink; + use pnet::packet::tcp::TcpFlags; + use pnet::util::MacAddr; + use rand::Rng; + use std::net::{IpAddr, Ipv4Addr}; + use tokio::time::{timeout, Duration}; + + fn is_root() -> bool { + unsafe { libc::geteuid() == 0 } + } + + fn pick_interface_v4() -> Option<(String, Ipv4Addr, MacAddr)> { + let interfaces = datalink::interfaces(); + for iface in interfaces { + let Some(mac) = iface.mac else { + continue; + }; + if iface.is_loopback() { + continue; + } + let ipv4 = iface.ips.iter().find_map(|n| match n.ip() { + IpAddr::V4(ip) => Some(ip), + IpAddr::V6(_) => None, + })?; + return Some((iface.name, ipv4, mac)); + } + None + } + + fn send_raw_frame(interface_name: &str, frame: &[u8]) -> io::Result<()> { + if frame.len() < 6 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "frame too short", + )); + } + + let c_ifname = CString::new(interface_name) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid interface name"))?; + let ifindex = unsafe { libc::if_nametoindex(c_ifname.as_ptr()) as i32 }; + if ifindex <= 0 { + return Err(io::Error::new( + io::ErrorKind::NotFound, + "interface not found", + )); + } + + let proto: i32 = (libc::ETH_P_ALL as u16).to_be() as i32; + let fd = unsafe { libc::socket(libc::AF_PACKET, libc::SOCK_RAW, proto) }; + if fd < 0 { + return Err(io::Error::last_os_error()); + } + let fd = unsafe { OwnedFd::from_raw_fd(fd) }; + + let mut addr: libc::sockaddr_ll = unsafe { mem::zeroed() }; + addr.sll_family = libc::AF_PACKET as u16; + addr.sll_protocol = (libc::ETH_P_ALL as u16).to_be(); + addr.sll_ifindex = ifindex; + addr.sll_halen = 6; + addr.sll_addr[..6].copy_from_slice(&frame[..6]); + + let ret = unsafe { + libc::sendto( + fd.as_raw_fd(), + frame.as_ptr() as *const libc::c_void, + frame.len(), + 0, + &addr as *const _ as *const libc::sockaddr, + mem::size_of::() as u32, + ) + }; + if ret < 0 { + return Err(io::Error::last_os_error()); + } + + Ok(()) + } + + #[tokio::test] + async fn linux_bpf_tun_receives_matching_ipv4_frame() { + if !is_root() { + eprintln!("linux_bpf_tun_receives_matching_ipv4_frame: skipped (not root)"); + return; + } + + let Some((ifname, dst_ip, mac)) = pick_interface_v4() else { + eprintln!("linux_bpf_tun_receives_matching_ipv4_frame: skipped (no suitable iface)"); + return; + }; + + let dst_port: u16 = rand::thread_rng().gen_range(40000..60000); + let dst_addr = SocketAddr::new(IpAddr::V4(dst_ip), dst_port); + eprintln!( + "linux_bpf_tun_receives_matching_ipv4_frame: ifname={ifname} dst_addr={dst_addr} mac={mac}" + ); + + let tun = LinuxBpfTun::new(&ifname, None, dst_addr).unwrap(); + + let src_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 123, 0, 1)), 12345); + eprintln!( + "linux_bpf_tun_receives_matching_ipv4_frame: sending frame src_addr={src_addr} -> dst_addr={dst_addr}" + ); + let frame = build_tcp_packet( + mac, + mac, + src_addr, + dst_addr, + 1, + 0, + TcpFlags::SYN, + Some(b"ping"), + ); + + send_raw_frame(&ifname, &frame).unwrap(); + + let mut received = BytesMut::new(); + let n = timeout(Duration::from_secs(2), tun.recv(&mut received)) + .await + .unwrap() + .unwrap(); + eprintln!( + "linux_bpf_tun_receives_matching_ipv4_frame: received {} bytes", + n + ); + assert_eq!(n, frame.len()); + assert_eq!(&received[..], &frame[..]); + } + + #[tokio::test] + async fn linux_bpf_tun_filters_out_non_matching_ipv4_frame() { + if !is_root() { + eprintln!("linux_bpf_tun_filters_out_non_matching_ipv4_frame: skipped (not root)"); + return; + } + + let Some((ifname, dst_ip, mac)) = pick_interface_v4() else { + eprintln!( + "linux_bpf_tun_filters_out_non_matching_ipv4_frame: skipped (no suitable iface)" + ); + return; + }; + + let dst_port: u16 = rand::thread_rng().gen_range(40000..60000); + let dst_addr = SocketAddr::new(IpAddr::V4(dst_ip), dst_port); + eprintln!( + "linux_bpf_tun_filters_out_non_matching_ipv4_frame: ifname={ifname} dst_addr={dst_addr} mac={mac}" + ); + + let tun = LinuxBpfTun::new(&ifname, None, dst_addr).unwrap(); + + let src_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 123, 0, 2)), 23456); + let non_matching_dst = SocketAddr::new(IpAddr::V4(dst_ip), dst_port.wrapping_add(1)); + eprintln!( + "linux_bpf_tun_filters_out_non_matching_ipv4_frame: sending non-matching src_addr={src_addr} -> dst_addr={non_matching_dst}" + ); + let non_matching = build_tcp_packet( + mac, + mac, + src_addr, + non_matching_dst, + 1, + 0, + TcpFlags::SYN, + Some(b"nope"), + ); + send_raw_frame(&ifname, &non_matching).unwrap(); + + let mut received = BytesMut::new(); + let non_matching_timeout = timeout(Duration::from_millis(400), tun.recv(&mut received)) + .await + .is_err(); + eprintln!( + "linux_bpf_tun_filters_out_non_matching_ipv4_frame: non-matching recv timeout={}", + non_matching_timeout + ); + assert!(non_matching_timeout); + + eprintln!( + "linux_bpf_tun_filters_out_non_matching_ipv4_frame: sending matching src_addr={src_addr} -> dst_addr={dst_addr}" + ); + let matching = build_tcp_packet( + mac, + mac, + src_addr, + dst_addr, + 2, + 0, + TcpFlags::SYN, + Some(b"ok"), + ); + send_raw_frame(&ifname, &matching).unwrap(); + + let mut received2 = BytesMut::new(); + let n = timeout(Duration::from_secs(2), tun.recv(&mut received2)) + .await + .unwrap() + .unwrap(); + eprintln!( + "linux_bpf_tun_filters_out_non_matching_ipv4_frame: received {} bytes", + n + ); + assert_eq!(n, matching.len()); + assert_eq!(&received2[..], &matching[..]); + } +} diff --git a/easytier/src/tunnel/fake_tcp/netfilter/macos_bpf.rs b/easytier/src/tunnel/fake_tcp/netfilter/macos_bpf.rs new file mode 100644 index 00000000..ca252257 --- /dev/null +++ b/easytier/src/tunnel/fake_tcp/netfilter/macos_bpf.rs @@ -0,0 +1,1122 @@ +use bytes::Bytes; +use bytes::BytesMut; +use nix::libc; +use std::ffi::CString; +use std::io; +use std::mem; +use std::net::IpAddr; +use std::net::SocketAddr; +use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; +use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering}; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing::{debug, info, warn}; + +use crate::tunnel::fake_tcp::stack; + +const ETH_HDR_LEN: usize = 14; +const ETH_TYPE_OFFSET: u32 = 12; +const ETHERTYPE_IPV4: u32 = 0x0800; +const ETHERTYPE_IPV6: u32 = 0x86DD; +const IPPROTO_TCP_U32: u32 = 6; + +const DLT_EN10MB: u32 = 1; +const DLT_NULL: u32 = 4; +const DLT_RAW: u32 = 12; +const DLT_LOOP: u32 = 108; + +const BPF_LD: u16 = 0x00; +const BPF_LDX: u16 = 0x01; +const BPF_JMP: u16 = 0x05; +const BPF_RET: u16 = 0x06; + +const BPF_W: u16 = 0x00; +const BPF_H: u16 = 0x08; +const BPF_B: u16 = 0x10; + +const BPF_ABS: u16 = 0x20; +const BPF_IND: u16 = 0x40; +const BPF_MSH: u16 = 0xa0; + +const BPF_JA: u16 = 0x00; +const BPF_JEQ: u16 = 0x10; + +const BPF_K: u16 = 0x00; + +const BPF_GROUP: u8 = b'B'; + +const BIOCGBLEN_NUM: u8 = 102; +const BIOCSBLEN_NUM: u8 = 102; +const BIOCSETF_NUM: u8 = 103; +const BIOCFLUSH_NUM: u8 = 104; +const BIOCGDLT_NUM: u8 = 106; +const BIOCSETIF_NUM: u8 = 108; +const BIOCSRTIMEOUT_NUM: u8 = 109; +const BIOCIMMEDIATE_NUM: u8 = 112; +const BIOCSHDRCMPLT_NUM: u8 = 117; +const BIOCSSEESENT_NUM: u8 = 119; + +const IOCPARM_MASK: u32 = 0x1fff; +const IOC_VOID: u32 = 0x2000_0000; +const IOC_OUT: u32 = 0x4000_0000; +const IOC_IN: u32 = 0x8000_0000; +const IOC_INOUT: u32 = IOC_IN | IOC_OUT; + +#[derive(Clone, Copy)] +enum LinkType { + En10Mb, + Null, + Raw, + Loop, + Utun, +} + +impl LinkType { + fn from_dlt(dlt: u32) -> Option { + match dlt { + DLT_EN10MB => Some(Self::En10Mb), + DLT_NULL => Some(Self::Null), + DLT_RAW => Some(Self::Raw), + DLT_LOOP => Some(Self::Loop), + _ => None, + } + } +} + +fn looks_like_ip(packet: &[u8]) -> bool { + matches!(packet.first().map(|b| b >> 4), Some(4 | 6)) +} + +fn maybe_unwrap_utun_payload(packet: &[u8]) -> Option<&[u8]> { + if looks_like_ip(packet) { + return Some(packet); + } + if packet.len() < 5 { + return None; + } + let payload = &packet[4..]; + if !looks_like_ip(payload) { + return None; + } + Some(payload) +} + +fn ether_type_from_ip_packet(ip: &[u8]) -> Option { + let v = *ip.first()?; + match v >> 4 { + 4 => Some(0x0800), + 6 => Some(0x86DD), + _ => None, + } +} + +fn wrap_ip_with_ethernet(ip: &[u8]) -> Option> { + let ether_type = ether_type_from_ip_packet(ip)?; + let mut out = vec![0u8; ETH_HDR_LEN + ip.len()]; + out[12..14].copy_from_slice(ðer_type.to_be_bytes()); + out[ETH_HDR_LEN..].copy_from_slice(ip); + Some(out) +} + +fn family_word_for_null(family: u32) -> u32 { + u32::from_be_bytes(family.to_ne_bytes()) +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct BpfInsn { + code: u16, + jt: u8, + jf: u8, + k: u32, +} + +#[repr(C)] +struct BpfProgram { + bf_len: u32, + bf_insns: *mut BpfInsn, +} + +fn read_u16_ne(bytes: &[u8]) -> u16 { + u16::from_ne_bytes([bytes[0], bytes[1]]) +} + +fn read_u32_ne(bytes: &[u8]) -> u32 { + u32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) +} + +fn bpf_word_align_with(align: usize, x: usize) -> usize { + (x + (align - 1)) & !(align - 1) +} + +fn parse_bpf_record(buf: &[u8], align: usize) -> Option<(usize, std::ops::Range, u16, u32)> { + let max_shift = std::cmp::min(align, buf.len()); + for shift in 0..max_shift { + let rest = &buf[shift..]; + + let try_ts8 = || -> Option<(usize, std::ops::Range, u16, u32)> { + let base_hdr_len = 18usize; + if rest.len() < base_hdr_len { + return None; + } + let caplen = read_u32_ne(rest.get(8..12)?) as usize; + let datalen = read_u32_ne(rest.get(12..16)?) as usize; + let hdrlen = read_u16_ne(rest.get(16..18)?) as usize; + if hdrlen < base_hdr_len || hdrlen > 512 { + return None; + } + if caplen > datalen { + return None; + } + let pkt_start = shift + hdrlen; + let pkt_end = pkt_start.checked_add(caplen)?; + if pkt_end > buf.len() { + return None; + } + let advance = shift + bpf_word_align_with(align, hdrlen + caplen); + Some((advance, pkt_start..pkt_end, hdrlen as u16, caplen as u32)) + }; + + if let Some(v) = try_ts8() { + return Some(v); + } + + let try_ts16 = || -> Option<(usize, std::ops::Range, u16, u32)> { + let base_hdr_len = 26usize; + if rest.len() < base_hdr_len { + return None; + } + let caplen = read_u32_ne(rest.get(16..20)?) as usize; + let datalen = read_u32_ne(rest.get(20..24)?) as usize; + let hdrlen = read_u16_ne(rest.get(24..26)?) as usize; + if hdrlen < base_hdr_len || hdrlen > 512 { + return None; + } + if caplen > datalen { + return None; + } + let pkt_start = shift + hdrlen; + let pkt_end = pkt_start.checked_add(caplen)?; + if pkt_end > buf.len() { + return None; + } + let advance = shift + bpf_word_align_with(align, hdrlen + caplen); + Some((advance, pkt_start..pkt_end, hdrlen as u16, caplen as u32)) + }; + + if let Some(v) = try_ts16() { + return Some(v); + } + } + None +} + +fn ioc(inout: u32, group: u8, num: u8, len: u32) -> libc::c_ulong { + (inout | ((len & IOCPARM_MASK) << 16) | ((group as u32) << 8) | (num as u32)) as libc::c_ulong +} + +fn io(group: u8, num: u8) -> libc::c_ulong { + ioc(IOC_VOID, group, num, 0) +} + +fn ior(group: u8, num: u8) -> libc::c_ulong { + ioc(IOC_OUT, group, num, mem::size_of::() as u32) +} + +fn iow(group: u8, num: u8) -> libc::c_ulong { + ioc(IOC_IN, group, num, mem::size_of::() as u32) +} + +fn iowr(group: u8, num: u8) -> libc::c_ulong { + ioc(IOC_INOUT, group, num, mem::size_of::() as u32) +} + +unsafe fn ioctl_ptr(fd: libc::c_int, req: libc::c_ulong, arg: *mut T) -> io::Result<()> { + let ret = libc::ioctl(fd, req, arg); + if ret < 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) +} + +unsafe fn ioctl_void(fd: libc::c_int, req: libc::c_ulong) -> io::Result<()> { + let ret = libc::ioctl(fd, req); + if ret < 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) +} + +fn stmt(code: u16, k: u32) -> BpfInsn { + BpfInsn { + code, + jt: 0, + jf: 0, + k, + } +} + +fn jeq(k: u32, jt: u8, jf: u8) -> BpfInsn { + BpfInsn { + code: BPF_JMP | BPF_JEQ | BPF_K, + jt, + jf, + k, + } +} + +fn ja(k: u32) -> BpfInsn { + BpfInsn { + code: BPF_JMP | BPF_JA, + jt: 0, + jf: 0, + k, + } +} + +#[derive(Clone, Copy)] +struct Label(usize); + +struct JeqPatch { + idx: usize, + t: Label, + f: Label, +} + +struct JaPatch { + idx: usize, + target: Label, +} + +struct BpfBuilder { + insns: Vec, + labels: Vec>, + jeq_patches: Vec, + ja_patches: Vec, +} + +impl BpfBuilder { + fn new() -> Self { + Self { + insns: Vec::new(), + labels: Vec::new(), + jeq_patches: Vec::new(), + ja_patches: Vec::new(), + } + } + + fn new_label(&mut self) -> Label { + let idx = self.labels.len(); + self.labels.push(None); + Label(idx) + } + + fn set_label(&mut self, label: Label) { + self.labels[label.0] = Some(self.insns.len()); + } + + fn push(&mut self, insn: BpfInsn) { + self.insns.push(insn); + } + + fn push_jeq(&mut self, k: u32, t: Label, f: Label) { + let idx = self.insns.len(); + self.insns.push(jeq(k, 0, 0)); + self.jeq_patches.push(JeqPatch { idx, t, f }); + } + + fn push_ja(&mut self, target: Label) { + let idx = self.insns.len(); + self.insns.push(ja(0)); + self.ja_patches.push(JaPatch { idx, target }); + } + + fn finish(mut self) -> io::Result> { + for patch in self.jeq_patches { + let JeqPatch { idx, t, f } = patch; + let cur = idx + 1; + let t_pos = + self.labels.get(t.0).and_then(|v| *v).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "unresolved label") + })?; + let f_pos = + self.labels.get(f.0).and_then(|v| *v).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "unresolved label") + })?; + + if t_pos < cur || f_pos < cur { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "backward bpf jump", + )); + } + + let jt: u8 = (t_pos - cur) + .try_into() + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "bpf jump too far"))?; + let jf: u8 = (f_pos - cur) + .try_into() + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "bpf jump too far"))?; + + self.insns[idx].jt = jt; + self.insns[idx].jf = jf; + } + + for patch in self.ja_patches { + let JaPatch { idx, target } = patch; + let cur = idx + 1; + let t_pos = + self.labels.get(target.0).and_then(|v| *v).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "unresolved label") + })?; + if t_pos < cur { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "backward bpf jump", + )); + } + self.insns[idx].k = (t_pos - cur) as u32; + } + + Ok(self.insns) + } +} + +fn build_tcp_filter_ethernet( + src_addr: Option, + dst_addr: SocketAddr, +) -> io::Result> { + if let Some(src) = src_addr { + if src.is_ipv4() != dst_addr.is_ipv4() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "src/dst addr family mismatch", + )); + } + } + + let mut b = BpfBuilder::new(); + let l_check_ipv6 = b.new_label(); + let l_ipv4 = b.new_label(); + let l_ipv6 = b.new_label(); + let l_accept = b.new_label(); + let l_reject = b.new_label(); + + b.push(stmt(BPF_LD | BPF_H | BPF_ABS, ETH_TYPE_OFFSET)); + b.push_jeq(ETHERTYPE_IPV4, l_ipv4, l_check_ipv6); + + b.set_label(l_check_ipv6); + b.push_jeq(ETHERTYPE_IPV6, l_ipv6, l_reject); + + if dst_addr.is_ipv4() { + b.set_label(l_ipv4); + let l_v4_proto_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_B | BPF_ABS, (ETH_HDR_LEN + 9) as u32)); + b.push_jeq(IPPROTO_TCP_U32, l_v4_proto_ok, l_reject); + + b.set_label(l_v4_proto_ok); + let dst_ip = match dst_addr.ip() { + IpAddr::V4(ip) => u32::from(ip), + _ => unreachable!(), + }; + let l_v4_dstip_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_W | BPF_ABS, (ETH_HDR_LEN + 16) as u32)); + b.push_jeq(dst_ip, l_v4_dstip_ok, l_reject); + + b.set_label(l_v4_dstip_ok); + if let Some(src) = src_addr { + let src_ip = match src.ip() { + IpAddr::V4(ip) => u32::from(ip), + _ => unreachable!(), + }; + let l_v4_srcip_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_W | BPF_ABS, (ETH_HDR_LEN + 12) as u32)); + b.push_jeq(src_ip, l_v4_srcip_ok, l_reject); + b.set_label(l_v4_srcip_ok); + } + + b.push(stmt(BPF_LDX | BPF_B | BPF_MSH, ETH_HDR_LEN as u32)); + + let l_v4_dstport_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_H | BPF_IND, (ETH_HDR_LEN + 2) as u32)); + b.push_jeq(dst_addr.port() as u32, l_v4_dstport_ok, l_reject); + + b.set_label(l_v4_dstport_ok); + if let Some(src) = src_addr { + b.push(stmt(BPF_LD | BPF_H | BPF_IND, ETH_HDR_LEN as u32)); + b.push_jeq(src.port() as u32, l_accept, l_reject); + } else { + b.push_ja(l_accept); + } + } else { + b.set_label(l_ipv6); + let l_v6_proto_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_B | BPF_ABS, (ETH_HDR_LEN + 6) as u32)); + b.push_jeq(IPPROTO_TCP_U32, l_v6_proto_ok, l_reject); + + b.set_label(l_v6_proto_ok); + let dst_ip = match dst_addr.ip() { + IpAddr::V6(ip) => ip.octets(), + _ => unreachable!(), + }; + for (i, chunk) in dst_ip.chunks_exact(4).enumerate() { + let off = ETH_HDR_LEN + 24 + (i * 4); + let v = u32::from_be_bytes(chunk.try_into().unwrap()); + let l_v6_dstip_word_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_W | BPF_ABS, off as u32)); + b.push_jeq(v, l_v6_dstip_word_ok, l_reject); + b.set_label(l_v6_dstip_word_ok); + } + + if let Some(src) = src_addr { + let src_ip = match src.ip() { + IpAddr::V6(ip) => ip.octets(), + _ => unreachable!(), + }; + for (i, chunk) in src_ip.chunks_exact(4).enumerate() { + let off = ETH_HDR_LEN + 8 + (i * 4); + let v = u32::from_be_bytes(chunk.try_into().unwrap()); + let l_v6_srcip_word_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_W | BPF_ABS, off as u32)); + b.push_jeq(v, l_v6_srcip_word_ok, l_reject); + b.set_label(l_v6_srcip_word_ok); + } + } + + let l_v6_dstport_ok = b.new_label(); + b.push(stmt( + BPF_LD | BPF_H | BPF_ABS, + (ETH_HDR_LEN + 40 + 2) as u32, + )); + b.push_jeq(dst_addr.port() as u32, l_v6_dstport_ok, l_reject); + + b.set_label(l_v6_dstport_ok); + if let Some(src) = src_addr { + b.push(stmt(BPF_LD | BPF_H | BPF_ABS, (ETH_HDR_LEN + 40) as u32)); + b.push_jeq(src.port() as u32, l_accept, l_reject); + } else { + b.push_ja(l_accept); + } + } + + b.set_label(l_accept); + b.push(stmt(BPF_RET | BPF_K, 0xFFFF)); + + b.set_label(l_reject); + if dst_addr.is_ipv4() { + b.set_label(l_ipv6); + } else { + b.set_label(l_ipv4); + } + b.push(stmt(BPF_RET | BPF_K, 0)); + + b.finish() +} + +fn build_tcp_filter_ip( + base: u32, + src_addr: Option, + dst_addr: SocketAddr, + family_word: Option, +) -> io::Result> { + if let Some(src) = src_addr { + if src.is_ipv4() != dst_addr.is_ipv4() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "src/dst addr family mismatch", + )); + } + } + + let mut b = BpfBuilder::new(); + let l_accept = b.new_label(); + let l_reject = b.new_label(); + + if let Some(family_word) = family_word { + let l_family_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_W | BPF_ABS, 0)); + b.push_jeq(family_word, l_family_ok, l_reject); + b.set_label(l_family_ok); + } + + if dst_addr.is_ipv4() { + let l_v4_proto_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_B | BPF_ABS, base + 9)); + b.push_jeq(IPPROTO_TCP_U32, l_v4_proto_ok, l_reject); + + b.set_label(l_v4_proto_ok); + let dst_ip = match dst_addr.ip() { + IpAddr::V4(ip) => u32::from(ip), + _ => unreachable!(), + }; + let l_v4_dstip_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_W | BPF_ABS, base + 16)); + b.push_jeq(dst_ip, l_v4_dstip_ok, l_reject); + + b.set_label(l_v4_dstip_ok); + if let Some(src) = src_addr { + let src_ip = match src.ip() { + IpAddr::V4(ip) => u32::from(ip), + _ => unreachable!(), + }; + let l_v4_srcip_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_W | BPF_ABS, base + 12)); + b.push_jeq(src_ip, l_v4_srcip_ok, l_reject); + b.set_label(l_v4_srcip_ok); + } + + b.push(stmt(BPF_LDX | BPF_B | BPF_MSH, base)); + + let l_v4_dstport_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_H | BPF_IND, base + 2)); + b.push_jeq(dst_addr.port() as u32, l_v4_dstport_ok, l_reject); + + b.set_label(l_v4_dstport_ok); + if let Some(src) = src_addr { + b.push(stmt(BPF_LD | BPF_H | BPF_IND, base)); + b.push_jeq(src.port() as u32, l_accept, l_reject); + } else { + b.push_ja(l_accept); + } + } else { + let l_v6_proto_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_B | BPF_ABS, base + 6)); + b.push_jeq(IPPROTO_TCP_U32, l_v6_proto_ok, l_reject); + + b.set_label(l_v6_proto_ok); + let dst_ip = match dst_addr.ip() { + IpAddr::V6(ip) => ip.octets(), + _ => unreachable!(), + }; + for (i, chunk) in dst_ip.chunks_exact(4).enumerate() { + let off = base + 24 + (i * 4) as u32; + let v = u32::from_be_bytes(chunk.try_into().unwrap()); + let l_v6_dstip_word_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_W | BPF_ABS, off)); + b.push_jeq(v, l_v6_dstip_word_ok, l_reject); + b.set_label(l_v6_dstip_word_ok); + } + + if let Some(src) = src_addr { + let src_ip = match src.ip() { + IpAddr::V6(ip) => ip.octets(), + _ => unreachable!(), + }; + for (i, chunk) in src_ip.chunks_exact(4).enumerate() { + let off = base + 8 + (i * 4) as u32; + let v = u32::from_be_bytes(chunk.try_into().unwrap()); + let l_v6_srcip_word_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_W | BPF_ABS, off)); + b.push_jeq(v, l_v6_srcip_word_ok, l_reject); + b.set_label(l_v6_srcip_word_ok); + } + } + + let l_v6_dstport_ok = b.new_label(); + b.push(stmt(BPF_LD | BPF_H | BPF_ABS, base + 40 + 2)); + b.push_jeq(dst_addr.port() as u32, l_v6_dstport_ok, l_reject); + + b.set_label(l_v6_dstport_ok); + if let Some(src) = src_addr { + b.push(stmt(BPF_LD | BPF_H | BPF_ABS, base + 40)); + b.push_jeq(src.port() as u32, l_accept, l_reject); + } else { + b.push_ja(l_accept); + } + } + + b.set_label(l_accept); + b.push(stmt(BPF_RET | BPF_K, 0xFFFF)); + + b.set_label(l_reject); + b.push(stmt(BPF_RET | BPF_K, 0)); + + b.finish() +} + +fn build_tcp_filter_utun( + src_addr: Option, + dst_addr: SocketAddr, +) -> io::Result> { + let raw = build_tcp_filter_ip(0, src_addr, dst_addr, None)?; + + let family = if dst_addr.is_ipv4() { + libc::AF_INET as u32 + } else { + libc::AF_INET6 as u32 + }; + let family_hdr = + build_tcp_filter_ip(4, src_addr, dst_addr, Some(family_word_for_null(family)))?; + + if raw.is_empty() { + return Ok(family_hdr); + } + + let mut combined = raw; + if let Some(last) = combined.last_mut() { + if last.code == (BPF_RET | BPF_K) && last.k == 0 { + *last = ja(0); + } else { + combined.push(ja(0)); + } + } + combined.extend(family_hdr); + Ok(combined) +} + +fn build_tcp_filter( + link_type: LinkType, + src_addr: Option, + dst_addr: SocketAddr, +) -> io::Result> { + match link_type { + LinkType::En10Mb => build_tcp_filter_ethernet(src_addr, dst_addr), + LinkType::Raw => build_tcp_filter_ip(0, src_addr, dst_addr, None), + LinkType::Null => { + let family = if dst_addr.is_ipv4() { + libc::AF_INET as u32 + } else { + libc::AF_INET6 as u32 + }; + build_tcp_filter_ip(4, src_addr, dst_addr, Some(family_word_for_null(family))) + } + LinkType::Loop => { + let family = if dst_addr.is_ipv4() { + libc::AF_INET as u32 + } else { + libc::AF_INET6 as u32 + }; + build_tcp_filter_ip(4, src_addr, dst_addr, Some(family)) + } + LinkType::Utun => build_tcp_filter_utun(src_addr, dst_addr), + } +} + +fn open_bpf_device() -> io::Result { + let mut last_err: Option = None; + for i in 0..256 { + let path = format!("/dev/bpf{}", i); + let c_path = CString::new(path.as_str()) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path"))?; + let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_RDWR) }; + if fd >= 0 { + debug!(path, "opened bpf device"); + return Ok(unsafe { OwnedFd::from_raw_fd(fd) }); + } + let err = io::Error::last_os_error(); + if err.raw_os_error() == Some(libc::EBUSY) { + last_err = Some(err); + continue; + } + last_err = Some(err); + } + Err(last_err + .unwrap_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no available /dev/bpf device"))) +} + +fn set_ifreq_name(ifr: &mut libc::ifreq, interface_name: &str) -> io::Result<()> { + let bytes = interface_name.as_bytes(); + let ifnamsiz = libc::IFNAMSIZ as usize; + if bytes.len() >= ifnamsiz { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "interface name too long", + )); + } + for i in 0..ifnamsiz { + ifr.ifr_name[i] = 0; + } + for (i, &b) in bytes.iter().enumerate() { + ifr.ifr_name[i] = b as libc::c_char; + } + Ok(()) +} + +pub struct MacosBpfTun { + fd: OwnedFd, + link_type: LinkType, + stop: Arc, + worker: Option>, + recv_queue: Mutex>>, +} + +impl MacosBpfTun { + pub fn new( + interface_name: &str, + src_addr: Option, + dst_addr: SocketAddr, + ) -> io::Result { + let fd = open_bpf_device()?; + let raw_fd = fd.as_raw_fd(); + + let mut buf_len: libc::c_uint = 0; + unsafe { + ioctl_ptr( + raw_fd, + ior::(BPF_GROUP, BIOCGBLEN_NUM), + &mut buf_len, + ) + }?; + if buf_len == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "bpf buffer length is zero", + )); + } + + let mut desired_buf_len: libc::c_uint = buf_len; + let _ = unsafe { + ioctl_ptr( + raw_fd, + iowr::(BPF_GROUP, BIOCSBLEN_NUM), + &mut desired_buf_len, + ) + }; + + let mut immediate: libc::c_uint = 1; + unsafe { + ioctl_ptr( + raw_fd, + iow::(BPF_GROUP, BIOCIMMEDIATE_NUM), + &mut immediate, + ) + }?; + + let mut seesent: libc::c_uint = 0; + unsafe { + ioctl_ptr( + raw_fd, + iow::(BPF_GROUP, BIOCSSEESENT_NUM), + &mut seesent, + ) + }?; + + let mut hdr_complete: libc::c_uint = 1; + match unsafe { + ioctl_ptr( + raw_fd, + iow::(BPF_GROUP, BIOCSHDRCMPLT_NUM), + &mut hdr_complete, + ) + } { + Ok(()) => {} + Err(e) if e.raw_os_error() == Some(libc::EINVAL) => {} + Err(e) => return Err(e), + } + + let timeout = libc::timeval { + tv_sec: 0, + tv_usec: 200_000, + }; + let mut timeout_mut = timeout; + unsafe { + ioctl_ptr( + raw_fd, + iow::(BPF_GROUP, BIOCSRTIMEOUT_NUM), + &mut timeout_mut, + ) + }?; + + unsafe { ioctl_void(raw_fd, io(BPF_GROUP, BIOCFLUSH_NUM)) }?; + + let mut ifr: libc::ifreq = unsafe { mem::zeroed() }; + set_ifreq_name(&mut ifr, interface_name)?; + unsafe { + ioctl_ptr( + raw_fd, + iow::(BPF_GROUP, BIOCSETIF_NUM), + &mut ifr, + ) + }?; + + let mut dlt: libc::c_uint = 0; + unsafe { + ioctl_ptr( + raw_fd, + ior::(BPF_GROUP, BIOCGDLT_NUM), + &mut dlt, + ) + }?; + + let link_type = if interface_name.starts_with("utun") { + LinkType::Utun + } else { + LinkType::from_dlt(dlt as u32).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("unsupported datalink type {}", dlt), + ) + })? + }; + + let filter = build_tcp_filter(link_type, src_addr, dst_addr)?; + + let mut bpf_insns: Vec = filter; + let mut prog = BpfProgram { + bf_len: bpf_insns + .len() + .try_into() + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "bpf program too long"))?, + bf_insns: bpf_insns.as_mut_ptr(), + }; + unsafe { + ioctl_ptr( + raw_fd, + iow::(BPF_GROUP, BIOCSETF_NUM), + &mut prog, + ) + }?; + + info!( + interface_name, + ?src_addr, + ?dst_addr, + dlt, + link_type = match link_type { + LinkType::En10Mb => "en10mb", + LinkType::Null => "null", + LinkType::Raw => "raw", + LinkType::Loop => "loop", + LinkType::Utun => "utun", + }, + filter_len = bpf_insns.len(), + buf_len, + desired_buf_len, + "MacosBpfTun created" + ); + + let stop = Arc::new(AtomicBool::new(false)); + let (tx, rx) = tokio::sync::mpsc::channel(1024); + let stop_clone = stop.clone(); + let read_fd = raw_fd; + let worker_link_type = link_type; + let worker = std::thread::spawn(move || { + let mut buf = vec![0u8; desired_buf_len as usize]; + let mut wrap_fail_logs_left: u8 = 5; + let mut bad_record_logs_left: u8 = 8; + let mut shifted_record_logs_left: u8 = 8; + let align = mem::size_of::(); + while !stop_clone.load(AtomicOrdering::Relaxed) { + let n = unsafe { + libc::read(read_fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) + }; + if n < 0 { + let err = io::Error::last_os_error(); + if matches!( + err.kind(), + io::ErrorKind::Interrupted | io::ErrorKind::WouldBlock + ) { + continue; + } + warn!(?err, "MacosBpfTun bpf read failed"); + break; + } + if n == 0 { + continue; + } + let mut off = 0usize; + let n = n as usize; + while off < n { + let window = &buf[off..n]; + let Some((advance, pkt_range, hdr_len, cap_len)) = + parse_bpf_record(window, align) + else { + if bad_record_logs_left > 0 { + bad_record_logs_left -= 1; + let preview_len = std::cmp::min(window.len(), 48); + let preview = &window[..preview_len]; + warn!(off, read_len = n, preview = ?preview, "MacosBpfTun failed to parse bpf records"); + } + break; + }; + + let pkt_start = off + pkt_range.start; + let pkt_end = off + pkt_range.end; + let shift = (pkt_range.start as usize).saturating_sub(hdr_len as usize); + if shift != 0 && shifted_record_logs_left > 0 { + shifted_record_logs_left -= 1; + warn!( + off, + record_start = off + shift, + shift, + hdr_len, + cap_len, + read_len = n, + "MacosBpfTun parsed bpf record with non-zero offset" + ); + } + + let packet = &buf[pkt_start..pkt_end]; + let framed = match worker_link_type { + LinkType::En10Mb => Some(packet.to_vec()), + LinkType::Raw => wrap_ip_with_ethernet(packet), + LinkType::Null | LinkType::Loop => { + if packet.len() < 4 { + None + } else { + wrap_ip_with_ethernet(&packet[4..]) + } + } + LinkType::Utun => { + maybe_unwrap_utun_payload(packet).and_then(wrap_ip_with_ethernet) + } + }; + if let Some(framed) = framed { + if tx.blocking_send(framed).is_err() { + return; + } + } else if wrap_fail_logs_left > 0 { + wrap_fail_logs_left -= 1; + warn!( + link_type = match worker_link_type { + LinkType::En10Mb => "en10mb", + LinkType::Null => "null", + LinkType::Raw => "raw", + LinkType::Loop => "loop", + LinkType::Utun => "utun", + }, + packet_len = packet.len(), + "MacosBpfTun failed to wrap packet" + ); + } + if advance == 0 { + break; + } + off = off.saturating_add(advance); + } + } + }); + + Ok(Self { + fd, + link_type, + stop, + worker: Some(worker), + recv_queue: Mutex::new(rx), + }) + } +} + +impl Drop for MacosBpfTun { + fn drop(&mut self) { + self.stop.store(true, AtomicOrdering::Relaxed); + if let Some(worker) = self.worker.take() { + let _ = worker.join(); + } + } +} + +#[async_trait::async_trait] +impl stack::Tun for MacosBpfTun { + async fn recv(&self, packet: &mut BytesMut) -> Result { + let mut rx = self.recv_queue.lock().await; + match rx.recv().await { + Some(data) => { + packet.extend_from_slice(&data); + Ok(data.len()) + } + None => Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "MacosBpfTun channel closed", + )), + } + } + + #[tracing::instrument(ret, skip(self))] + fn try_send(&self, packet: &Bytes) -> Result<(), std::io::Error> { + if packet.len() < ETH_HDR_LEN { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "packet too short", + )); + } + let payload = &packet[ETH_HDR_LEN..]; + let write_all = |ptr: *const u8, len: usize| -> Result<(), std::io::Error> { + let ret = unsafe { libc::write(self.fd.as_raw_fd(), ptr as *const libc::c_void, len) }; + if ret < 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }; + + let mut out_len = 0usize; + let res = match self.link_type { + LinkType::En10Mb => { + out_len = packet.len(); + write_all(packet.as_ptr(), packet.len()) + } + LinkType::Raw => { + out_len = payload.len(); + write_all(payload.as_ptr(), payload.len()) + } + LinkType::Null | LinkType::Loop | LinkType::Utun => { + let family = match payload.first().map(|b| b >> 4) { + Some(4) => libc::AF_INET as u32, + Some(6) => libc::AF_INET6 as u32, + _ => { + warn!( + first_byte = payload.first().copied(), + payload_len = payload.len(), + "MacosBpfTun try_send invalid ip version" + ); + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "invalid ip version", + )); + } + }; + + let primary_hdr = match self.link_type { + LinkType::Null => family.to_ne_bytes(), + LinkType::Loop => family.to_be_bytes(), + LinkType::Utun => family.to_ne_bytes(), + _ => unreachable!(), + }; + + let mut out = vec![0u8; 4 + payload.len()]; + out[..4].copy_from_slice(&primary_hdr); + out[4..].copy_from_slice(payload); + out_len = out.len(); + + match write_all(out.as_ptr(), out.len()) { + Ok(()) => Ok(()), + Err(e) + if matches!(self.link_type, LinkType::Utun) + && e.raw_os_error() == Some(libc::EINVAL) + && primary_hdr != family.to_be_bytes() => + { + let mut out = vec![0u8; 4 + payload.len()]; + out[..4].copy_from_slice(&family.to_be_bytes()); + out[4..].copy_from_slice(payload); + out_len = out.len(); + write_all(out.as_ptr(), out.len()) + } + Err(e) => Err(e), + } + } + }; + + if let Err(err) = res { + warn!( + ?err, + link_type = match self.link_type { + LinkType::En10Mb => "en10mb", + LinkType::Null => "null", + LinkType::Raw => "raw", + LinkType::Loop => "loop", + LinkType::Utun => "utun", + }, + in_len = packet.len(), + out_len, + "MacosBpfTun bpf write failed" + ); + return Err(err); + } + + Ok(()) + } + + fn driver_type(&self) -> &'static str { + "macos_bpf" + } +} diff --git a/easytier/src/tunnel/fake_tcp/netfilter/mod.rs b/easytier/src/tunnel/fake_tcp/netfilter/mod.rs new file mode 100644 index 00000000..5ed33d7a --- /dev/null +++ b/easytier/src/tunnel/fake_tcp/netfilter/mod.rs @@ -0,0 +1,87 @@ +pub mod pnet; + +use std::{net::SocketAddr, sync::Arc}; + +cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + pub mod linux_bpf; + + pub fn create_tun( + interface_name: &str, + src_addr: Option, + dst_addr: SocketAddr, + ) -> Arc { + match linux_bpf::LinuxBpfTun::new(interface_name, src_addr, dst_addr) { + Ok(tun) => Arc::new(tun), + Err(e) => { + tracing::warn!( + ?e, + interface_name, + "LinuxBpfTun init failed, falling back to PnetTun" + ); + Arc::new(pnet::PnetTun::new( + interface_name, + pnet::create_packet_filter(src_addr, dst_addr), + )) + } + } + } + } else if #[cfg(target_os = "macos")] { + pub mod macos_bpf; + + pub fn create_tun( + interface_name: &str, + src_addr: Option, + dst_addr: SocketAddr, + ) -> Arc { + match macos_bpf::MacosBpfTun::new(interface_name, src_addr, dst_addr) { + Ok(tun) => Arc::new(tun), + Err(e) => { + tracing::warn!( + ?e, + interface_name, + "MacosBpfTun init failed, falling back to PnetTun" + ); + Arc::new(pnet::PnetTun::new( + interface_name, + pnet::create_packet_filter(src_addr, dst_addr), + )) + } + } + } + } else if #[cfg(all(windows, any(target_arch = "x86_64", target_arch = "x86")))] { + pub mod windivert; + + pub fn create_tun( + _interface_name: &str, + _src_addr: Option, + local_addr: SocketAddr, + ) -> Arc { + match windivert::WinDivertTun::new(local_addr) { + Ok(tun) => Arc::new(tun), + Err(e) => { + tracing::warn!( + ?e, + ?local_addr, + "WinDivertTun init failed, falling back to PnetTun" + ); + Arc::new(pnet::PnetTun::new( + local_addr.to_string().as_str(), + pnet::create_packet_filter(None, local_addr), + )) + } + } + } + } else { + pub fn create_tun( + interface_name: &str, + src_addr: Option, + dst_addr: SocketAddr, + ) -> Arc { + Arc::new(pnet::PnetTun::new( + interface_name, + pnet::create_packet_filter(src_addr, dst_addr), + )) + } + } +} diff --git a/easytier/src/tunnel/fake_tcp/netfilter/pnet.rs b/easytier/src/tunnel/fake_tcp/netfilter/pnet.rs new file mode 100644 index 00000000..d7246a0c --- /dev/null +++ b/easytier/src/tunnel/fake_tcp/netfilter/pnet.rs @@ -0,0 +1,304 @@ +use std::{ + net::{IpAddr, SocketAddr}, + sync::{ + atomic::{AtomicU32, Ordering}, + Arc, Weak, + }, +}; + +use bytes::{Bytes, BytesMut}; +use dashmap::DashMap; +use once_cell::sync::Lazy; +use pnet::{ + datalink::{self, DataLinkSender, NetworkInterface}, + packet::{ethernet::EtherTypes, ip::IpNextHeaderProtocols, ipv6::Ipv6Packet}, +}; +use tokio::sync::Mutex; + +use crate::tunnel::fake_tcp::stack; + +type PacketFilter = Box bool + Send + Sync>; + +fn filter_tcp_packet( + packet: &[u8], + src_addr: Option<&SocketAddr>, + dst_addr: Option<&SocketAddr>, +) -> bool { + use pnet::packet::ethernet::EthernetPacket; + use pnet::packet::ipv4::Ipv4Packet; + use pnet::packet::tcp::TcpPacket; + use pnet::packet::Packet; + + let ethernet = if let Some(ethernet) = EthernetPacket::new(packet) { + ethernet + } else { + return false; + }; + + match ethernet.get_ethertype() { + EtherTypes::Ipv4 => { + let ipv4 = if let Some(ipv4) = Ipv4Packet::new(ethernet.payload()) { + ipv4 + } else { + return false; + }; + + if ipv4.get_next_level_protocol() != IpNextHeaderProtocols::Tcp { + return false; + } + + let tcp = if let Some(tcp) = TcpPacket::new(ipv4.payload()) { + tcp + } else { + return false; + }; + + if let Some(src_addr) = src_addr { + if IpAddr::V4(ipv4.get_source()) != src_addr.ip() { + return false; + } + if tcp.get_source() != src_addr.port() { + return false; + } + } + + if let Some(dst_addr) = dst_addr { + if IpAddr::V4(ipv4.get_destination()) != dst_addr.ip() { + return false; + } + if tcp.get_destination() != dst_addr.port() { + return false; + } + } + + tracing::trace!( + ?tcp, + "FakeTcpTunnelListener packet matched filter, dispatching, src_addr: {:?}, dst_addr: {:?}, packet_src_ip: {:?}, packet_dst_ip: {:?}, packet_src_port: {:?}, packet_dst_port: {:?}", + src_addr, + dst_addr, + ipv4.get_source(), + ipv4.get_destination(), + tcp.get_source(), + tcp.get_destination(), + ); + } + EtherTypes::Ipv6 => { + let ipv6 = if let Some(ipv6) = Ipv6Packet::new(ethernet.payload()) { + ipv6 + } else { + return false; + }; + + if ipv6.get_next_header() != IpNextHeaderProtocols::Tcp { + return false; + } + + let tcp = if let Some(tcp) = TcpPacket::new(ipv6.payload()) { + tcp + } else { + return false; + }; + + if let Some(src_addr) = src_addr { + if IpAddr::V6(ipv6.get_source()) != src_addr.ip() { + return false; + } + if tcp.get_source() != src_addr.port() { + return false; + } + } + + if let Some(dst_addr) = dst_addr { + if IpAddr::V6(ipv6.get_destination()) != dst_addr.ip() { + return false; + } + if tcp.get_destination() != dst_addr.port() { + return false; + } + } + + tracing::trace!( + ?tcp, + "FakeTcpTunnelListener packet matched filter, dispatching" + ); + } + _ => return false, + } + + true +} + +pub fn create_packet_filter(src_addr: Option, dst_addr: SocketAddr) -> PacketFilter { + Box::new(move |packet: &[u8]| -> bool { + filter_tcp_packet(packet, src_addr.as_ref(), Some(&dst_addr)) + }) +} + +struct Subscriber { + filter: PacketFilter, + sender: tokio::sync::mpsc::Sender>, +} + +struct InterfaceWorker { + tx: Mutex>, + subscribers: Arc>, +} + +impl InterfaceWorker { + fn new(interface: NetworkInterface) -> Arc { + let (tx, mut rx) = match datalink::channel(&interface, Default::default()) { + Ok(pnet::datalink::Channel::Ethernet(tx, rx)) => (tx, rx), + Ok(_) => panic!("Unhandled channel type"), + Err(e) => panic!( + "An error occurred when creating the datalink channel: {}", + e + ), + }; + + let subscribers = Arc::new(DashMap::::new()); + let subscribers_clone = subscribers.clone(); + + std::thread::spawn(move || { + loop { + match rx.next() { + Ok(packet) => { + // Iterate over subscribers and send packet if filter matches + // Note: DashMap iteration might be slow if many subscribers, but usually few per interface. + // For high performance we might need a better structure or read-copy-update. + for r in subscribers_clone.iter() { + let subscriber = r.value(); + if (subscriber.filter)(packet) { + tracing::trace!( + ?packet, + "InterfaceWorker packet matched filter, dispatching" + ); + // Try send, ignore errors (best effort) + let _ = subscriber.sender.try_send(packet.to_vec()); + } + } + } + Err(e) => { + tracing::error!("InterfaceWorker read error: {}", e); + // If interface goes down, we might need to handle it. + // For now just break and maybe the worker is dead. + break; + } + } + } + }); + + Arc::new(Self { + tx: Mutex::new(tx), + subscribers, + }) + } + + fn subscribe(&self, filter: PacketFilter, sender: tokio::sync::mpsc::Sender>) -> u32 { + static ID_GEN: AtomicU32 = AtomicU32::new(0); + let id = ID_GEN.fetch_add(1, Ordering::Relaxed); + self.subscribers.insert(id, Subscriber { filter, sender }); + id + } + + fn unsubscribe(&self, id: u32) { + self.subscribers.remove(&id); + } +} + +static INTERFACE_MANAGERS: Lazy>> = Lazy::new(DashMap::new); + +fn get_or_create_worker(interface_name: &str) -> Arc { + // Check if we have an active worker + if let Some(worker) = INTERFACE_MANAGERS + .get(interface_name) + .and_then(|w| w.upgrade()) + { + return worker; + } + + // Need to create new worker. + // Lock effectively by using entry API? DashMap entry API might not be enough for complex init. + // Let's use a double-check locking style or just accept race condition (creating two workers and one wins). + // DashMap doesn't support easy "compute_if_absent" with async or heavy logic without blocking the map shard. + + // But creation is rare. + // Let's find interface first. + let interfaces = datalink::interfaces(); + let interface = interfaces + .into_iter() + .find(|iface| iface.name == interface_name) + .expect("Network interface not found"); + + let worker = InterfaceWorker::new(interface); + INTERFACE_MANAGERS.insert(interface_name.to_string(), Arc::downgrade(&worker)); + worker +} + +pub struct PnetTun { + worker: Arc, + subscription_id: u32, + recv_queue: Mutex>>, +} + +impl PnetTun { + pub fn new(interface_name: &str, filter: PacketFilter) -> Self { + tracing::debug!(interface_name, "Creating new PnetTun"); + let worker = get_or_create_worker(interface_name); + let (tx, rx) = tokio::sync::mpsc::channel(1024); + let id = worker.subscribe(filter, tx); + + Self { + worker, + subscription_id: id, + recv_queue: Mutex::new(rx), + } + } +} + +impl Drop for PnetTun { + fn drop(&mut self) { + tracing::debug!(subscription_id = self.subscription_id, "Dropping PnetTun"); + self.worker.unsubscribe(self.subscription_id); + } +} + +#[async_trait::async_trait] +impl stack::Tun for PnetTun { + async fn recv(&self, packet: &mut BytesMut) -> Result { + let mut rx = self.recv_queue.lock().await; + match rx.recv().await { + Some(data) => { + tracing::trace!(?data, "PnetTun received packet"); + packet.extend_from_slice(&data); + Ok(data.len()) + } + None => { + tracing::warn!("PnetTun recv channel closed"); + Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "PnetTun channel closed", + )) + } + } + } + + fn try_send(&self, packet: &Bytes) -> Result<(), std::io::Error> { + tracing::trace!(len = packet.len(), "PnetTun try_sending packet"); + // We need async lock for tx. + // try_send is sync. We can use try_lock if available or blocking lock. + // tokio::sync::Mutex::try_lock is available. + if let Ok(mut tx) = self.worker.tx.try_lock() { + tx.send_to(packet, None) + .ok_or(std::io::Error::other("send_to failed"))? + } else { + Err(std::io::Error::new( + std::io::ErrorKind::WouldBlock, + "PnetTun tx lock busy", + )) + } + } + + fn driver_type(&self) -> &'static str { + "pnet" + } +} diff --git a/easytier/src/tunnel/fake_tcp/netfilter/windivert.rs b/easytier/src/tunnel/fake_tcp/netfilter/windivert.rs new file mode 100644 index 00000000..5aac0dcf --- /dev/null +++ b/easytier/src/tunnel/fake_tcp/netfilter/windivert.rs @@ -0,0 +1,196 @@ +use std::cell::UnsafeCell; +use std::io; +use std::net::SocketAddr; +use std::sync::Arc; + +use anyhow::Context as _; +use bytes::{Bytes, BytesMut}; +use tokio::sync::Mutex; +use windivert::error::WinDivertError; +use windivert::packet::WinDivertPacket; +use windivert::prelude::{WinDivertFlags, WinDivertShutdownMode}; +use windivert::{layer, WinDivert}; + +use crate::tunnel::fake_tcp::stack; + +struct WinDivertReader { + inner: UnsafeCell>, +} + +unsafe impl Send for WinDivertReader {} +unsafe impl Sync for WinDivertReader {} + +impl WinDivertReader { + fn new(inner: WinDivert) -> Self { + Self { + inner: UnsafeCell::new(inner), + } + } + + fn recv<'a>( + &self, + buffer: Option<&'a mut [u8]>, + ) -> Result, WinDivertError> { + let inner = unsafe { &*self.inner.get() }; + inner.recv(buffer) + } + + fn shutdown(&self) -> anyhow::Result<()> { + let inner = unsafe { &mut *self.inner.get() }; + inner + .shutdown(WinDivertShutdownMode::Recv) + .with_context(|| "WinDivertReader shutdown failed")?; + Ok(()) + } + + fn close(&self) -> anyhow::Result<()> { + let inner = unsafe { &mut *self.inner.get() }; + inner + .close(windivert::CloseAction::Nothing) + .with_context(|| "WinDivertReader close failed")?; + Ok(()) + } +} + +impl Drop for WinDivertReader { + fn drop(&mut self) { + if let Err(e) = self.close() { + tracing::error!("WinDivertReader close failed: {:?}", e); + } + } +} + +pub struct WinDivertTun { + recv_queue: Mutex>>, + sender: Arc>>, + reader: Arc, +} + +impl Drop for WinDivertTun { + fn drop(&mut self) { + if let Ok(mut sender) = self.sender.lock() { + if let Err(e) = sender.close(windivert::CloseAction::Nothing) { + tracing::error!("WinDivertSender close failed: {:?}", e); + } + } + if let Err(e) = self.reader.shutdown() { + tracing::error!("WinDivertReader shutdown failed: {:?}", e); + } + } +} + +impl WinDivertTun { + pub fn new(local_addr: SocketAddr) -> io::Result { + let (tx, rx) = tokio::sync::mpsc::channel(1024); + + let ip_filter = match local_addr { + SocketAddr::V4(addr) => format!("ip.DstAddr == {}", addr.ip()), + SocketAddr::V6(addr) => format!("ipv6.DstAddr == {}", addr.ip()), + }; + // Filter: DstIP == LocalIP AND TCP. + let filter = format!("{} and tcp", ip_filter); + + // Sniff mode: 1 (WINDIVERT_FLAG_SNIFF) + // Layer: Network (0) + // Priority: 0 + let flags = WinDivertFlags::default().set_sniff(); + let reader = WinDivert::network(&filter, 0, flags) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + let reader = Arc::new(WinDivertReader::new(reader)); + let reader_clone = reader.clone(); + + std::thread::spawn(move || { + let reader = reader_clone; + let mut buffer = vec![0u8; 65536]; + loop { + match reader.recv(Some(&mut buffer)) { + Ok(packet) => { + let data = &packet.data; + + let mut eth_data = vec![0u8; 14 + data.len()]; + // Set EtherType + if data.len() > 0 && data[0] >> 4 == 4 { + eth_data[12] = 0x08; + eth_data[13] = 0x00; + } else { + eth_data[12] = 0x86; + eth_data[13] = 0xDD; + } + eth_data[14..].copy_from_slice(data); + + if let Err(_) = tx.blocking_send(eth_data) { + break; + } + } + Err(_) => { + // log error? + break; + } + } + } + }); + + // Sender: non-sniff, empty filter? + // Use "false" to avoid capturing anything. + // Flags: 0 + let sender = WinDivert::network("false", 0, WinDivertFlags::default()) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + Ok(Self { + recv_queue: Mutex::new(rx), + sender: Arc::new(std::sync::Mutex::new(sender)), + reader, + }) + } +} + +#[async_trait::async_trait] +impl stack::Tun for WinDivertTun { + async fn recv(&self, packet: &mut BytesMut) -> Result { + let mut rx = self.recv_queue.lock().await; + match rx.recv().await { + Some(data) => { + packet.extend_from_slice(&data); + Ok(data.len()) + } + None => Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "Channel closed", + )), + } + } + + fn try_send(&self, packet: &Bytes) -> Result<(), std::io::Error> { + // Strip ethernet header + if packet.len() < 14 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Packet too short", + )); + } + let ip_data = &packet[14..]; + + let Ok(sender) = self.sender.try_lock() else { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "WinDivert sender lock failed", + )); + }; + + let mut pkt = unsafe { WinDivertPacket::::new(ip_data.to_vec()) }; + pkt.address.set_outbound(true); + + sender.send(&pkt).map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("WinDivert send failed: {}", e), + ) + })?; + + Ok(()) + } + + fn driver_type(&self) -> &'static str { + "windivert" + } +} diff --git a/easytier/src/tunnel/fake_tcp/packet.rs b/easytier/src/tunnel/fake_tcp/packet.rs new file mode 100644 index 00000000..0651fd70 --- /dev/null +++ b/easytier/src/tunnel/fake_tcp/packet.rs @@ -0,0 +1,159 @@ +use bytes::{Bytes, BytesMut}; +use pnet::packet::ethernet::{EtherTypes, EthernetPacket, MutableEthernetPacket}; +use pnet::packet::{ip, ipv4, ipv6, tcp}; +use pnet::util::MacAddr; +use std::convert::TryInto; +use std::net::{IpAddr, SocketAddr}; + +const IPV4_HEADER_LEN: usize = 20; +const IPV6_HEADER_LEN: usize = 40; +const TCP_HEADER_LEN: usize = 20; +pub const MAX_PACKET_LEN: usize = 1500; + +#[derive(Debug)] +pub enum IPPacket<'p> { + V4(ipv4::Ipv4Packet<'p>), + V6(ipv6::Ipv6Packet<'p>), +} + +impl IPPacket<'_> { + pub fn get_source(&self) -> IpAddr { + match self { + IPPacket::V4(p) => IpAddr::V4(p.get_source()), + IPPacket::V6(p) => IpAddr::V6(p.get_source()), + } + } + + pub fn get_destination(&self) -> IpAddr { + match self { + IPPacket::V4(p) => IpAddr::V4(p.get_destination()), + IPPacket::V6(p) => IpAddr::V6(p.get_destination()), + } + } +} + +const ETH_HDR_LEN: usize = 14; + +#[allow(clippy::too_many_arguments)] +pub fn build_tcp_packet( + src_mac: MacAddr, + dst_mac: MacAddr, + local_addr: SocketAddr, + remote_addr: SocketAddr, + seq: u32, + ack: u32, + flags: u8, + payload: Option<&[u8]>, +) -> Bytes { + let ip_header_len = match local_addr { + SocketAddr::V4(_) => IPV4_HEADER_LEN, + SocketAddr::V6(_) => IPV6_HEADER_LEN, + }; + let wscale = (flags & tcp::TcpFlags::SYN) != 0; + let tcp_header_len = TCP_HEADER_LEN + if wscale { 4 } else { 0 }; // nop + wscale + let tcp_total_len = tcp_header_len + payload.map_or(0, |payload| payload.len()); + let total_len = ip_header_len + tcp_total_len; + let mut buf = BytesMut::zeroed(ETH_HDR_LEN + total_len); + + let mut eth_buf = buf.split_to(ETH_HDR_LEN); + let mut ip_buf = buf.split_to(ip_header_len); + let mut tcp_buf = buf.split_to(tcp_total_len); + assert_eq!(0, buf.len()); + + let mut tcp = tcp::MutableTcpPacket::new(&mut tcp_buf).unwrap(); + tcp.set_window(0xffff); + tcp.set_source(local_addr.port()); + tcp.set_destination(remote_addr.port()); + tcp.set_sequence(seq); + tcp.set_acknowledgement(ack); + tcp.set_flags(flags); + tcp.set_data_offset(TCP_HEADER_LEN as u8 / 4 + if wscale { 1 } else { 0 }); + if wscale { + let wscale = tcp::TcpOption::wscale(14); + tcp.set_options(&[tcp::TcpOption::nop(), wscale]); + } + + if let Some(payload) = payload { + tcp.set_payload(payload); + } + + let mut ethernet = MutableEthernetPacket::new(&mut eth_buf).unwrap(); + ethernet.set_destination(dst_mac); + ethernet.set_source(src_mac); + ethernet.set_ethertype(EtherTypes::Ipv4); + + match (local_addr, remote_addr) { + (SocketAddr::V4(local), SocketAddr::V4(remote)) => { + let mut v4 = ipv4::MutableIpv4Packet::new(&mut ip_buf).unwrap(); + v4.set_version(4); + v4.set_header_length(IPV4_HEADER_LEN as u8 / 4); + v4.set_next_level_protocol(ip::IpNextHeaderProtocols::Tcp); + v4.set_ttl(64); + v4.set_source(*local.ip()); + v4.set_destination(*remote.ip()); + v4.set_total_length(total_len.try_into().unwrap()); + v4.set_flags(ipv4::Ipv4Flags::DontFragment); + + tcp.set_checksum(tcp::ipv4_checksum( + &tcp.to_immutable(), + &v4.get_source(), + &v4.get_destination(), + )); + + v4.set_checksum(ipv4::checksum(&v4.to_immutable())); + } + (SocketAddr::V6(local), SocketAddr::V6(remote)) => { + let mut v6 = ipv6::MutableIpv6Packet::new(&mut ip_buf).unwrap(); + v6.set_version(6); + v6.set_payload_length(tcp_total_len.try_into().unwrap()); + v6.set_next_header(ip::IpNextHeaderProtocols::Tcp); + v6.set_hop_limit(64); + v6.set_source(*local.ip()); + v6.set_destination(*remote.ip()); + + tcp.set_checksum(tcp::ipv6_checksum( + &tcp.to_immutable(), + &v6.get_source(), + &v6.get_destination(), + )); + } + _ => unreachable!(), + }; + + ip_buf.unsplit(tcp_buf); + eth_buf.unsplit(ip_buf); + eth_buf.freeze() +} + +#[tracing::instrument(ret)] +pub fn parse_ip_packet( + buf: &Bytes, +) -> Option<(MacAddr, MacAddr, IPPacket<'_>, tcp::TcpPacket<'_>)> { + let eth = EthernetPacket::new(buf).unwrap(); + let src_mac = eth.get_source(); + let dst_mac = eth.get_destination(); + + tracing::trace!("Parsing IP packet: {:?}", eth); + + let buf = &buf[ETH_HDR_LEN..]; + if buf[0] >> 4 == 4 { + let v4 = ipv4::Ipv4Packet::new(buf).unwrap(); + if v4.get_next_level_protocol() != ip::IpNextHeaderProtocols::Tcp { + return None; + } + + let tcp = tcp::TcpPacket::new(&buf[IPV4_HEADER_LEN..]).unwrap(); + Some((src_mac, dst_mac, IPPacket::V4(v4), tcp)) + } else if buf[0] >> 4 == 6 { + let v6 = ipv6::Ipv6Packet::new(buf).unwrap(); + if v6.get_next_header() != ip::IpNextHeaderProtocols::Tcp { + return None; + } + + let tcp = tcp::TcpPacket::new(&buf[IPV6_HEADER_LEN..]).unwrap(); + Some((src_mac, dst_mac, IPPacket::V6(v6), tcp)) + } else { + tracing::trace!("Invalid IP version: {}", buf[0] >> 4); + None + } +} diff --git a/easytier/src/tunnel/fake_tcp/stack.rs b/easytier/src/tunnel/fake_tcp/stack.rs new file mode 100644 index 00000000..2392967e --- /dev/null +++ b/easytier/src/tunnel/fake_tcp/stack.rs @@ -0,0 +1,561 @@ +//! A minimum, userspace TCP based datagram stack +//! +//! # Overview +//! +//! `fake-tcp` is a reusable library that implements a minimum TCP stack in +//! user space using the Tun interface. It allows programs to send datagrams +//! as if they are part of a TCP connection. `fake-tcp` has been tested to +//! be able to pass through a variety of NAT and stateful firewalls while +//! fully preserves certain desirable behavior such as out of order delivery +//! and no congestion/flow controls. +//! +//! # Core Concepts +//! +//! The core of the `fake-tcp` crate compose of two structures. [`Stack`] and +//! [`Socket`]. +//! +//! ## [`Stack`] +//! +//! [`Stack`] represents a virtual TCP stack that operates at +//! Layer 3. It is responsible for: +//! +//! * TCP active and passive open and handshake +//! * `RST` handling +//! * Interact with the Tun interface at Layer 3 +//! * Distribute incoming datagrams to corresponding [`Socket`] +//! +//! ## [`Socket`] +//! +//! [`Socket`] represents a TCP connection. It registers the identifying +//! tuple `(src_ip, src_port, dest_ip, dest_port)` inside the [`Stack`] so +//! so that incoming packets can be distributed to the right [`Socket`] with +//! using a channel. It is also what the client should use for +//! sending/receiving datagrams. +//! +//! # Examples +//! +//! Please see [`client.rs`](https://github.com/dndx/phantun/blob/main/phantun/src/bin/client.rs) +//! and [`server.rs`](https://github.com/dndx/phantun/blob/main/phantun/src/bin/server.rs) files +//! from the `phantun` crate for how to use this library in client/server mode, respectively. + +use crate::common::scoped_task::ScopedTask; + +use super::packet::*; +use bytes::{Bytes, BytesMut}; +use crossbeam::atomic::AtomicCell; +use pnet::packet::tcp::TcpOptionNumbers; +use pnet::packet::{tcp, Packet}; +use pnet::util::MacAddr; +use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::sync::{ + atomic::{AtomicU32, Ordering}, + Arc, RwLock, +}; +use tokio::sync::broadcast; +use tokio::sync::mpsc; +use tokio::time; +use tracing::{info, trace, warn}; + +const TIMEOUT: time::Duration = time::Duration::from_secs(1); +const RETRIES: usize = 6; +const MPMC_BUFFER_LEN: usize = 512; +const MPSC_BUFFER_LEN: usize = 128; +const MAX_UNACKED_LEN: u32 = 128 * 1024 * 1024; // 128MB + +#[async_trait::async_trait] +pub trait Tun: Send + Sync + 'static { + async fn recv(&self, packet: &mut BytesMut) -> Result; + fn try_send(&self, packet: &Bytes) -> Result<(), std::io::Error>; + fn driver_type(&self) -> &'static str; +} + +#[derive(Hash, Eq, PartialEq, Clone, Debug)] +struct AddrTuple { + local_addr: SocketAddr, + remote_addr: SocketAddr, +} + +impl AddrTuple { + fn new(local_addr: SocketAddr, remote_addr: SocketAddr) -> AddrTuple { + AddrTuple { + local_addr, + remote_addr, + } + } +} + +struct Shared { + tuples: RwLock>>, + listening: RwLock>, + tun: Arc, + ready: mpsc::Sender, + tuples_purge: broadcast::Sender, +} + +pub struct Stack { + shared: Arc, + local_ip: Ipv4Addr, + local_ip6: Option, + local_mac: MacAddr, + ready: mpsc::Receiver, + reader_task: ScopedTask<()>, +} + +#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug)] +pub enum State { + Idle, + SynSent, + SynReceived, + Established, +} + +pub struct Socket { + shared: Arc, + tun: Arc, + incoming: flume::Receiver, + local_addr: SocketAddr, + remote_addr: SocketAddr, + local_mac: MacAddr, + remote_mac: AtomicCell>, + seq: AtomicU32, + ack: AtomicU32, + last_ack: AtomicU32, + state: AtomicCell, +} + +/// A socket that represents a unique TCP connection between a server and client. +/// +/// The `Socket` object itself satisfies `Sync` and `Send`, which means it can +/// be safely called within an async future. +/// +/// To close a TCP connection that is no longer needed, simply drop this object +/// out of scope. +impl Socket { + #[allow(clippy::too_many_arguments)] + fn new( + shared: Arc, + tun: Arc, + local_addr: SocketAddr, + remote_addr: SocketAddr, + local_mac: MacAddr, + remote_mac: Option, + ack: Option, + state: State, + ) -> (Socket, flume::Sender) { + let (incoming_tx, incoming_rx) = flume::bounded(MPMC_BUFFER_LEN); + + ( + Socket { + shared, + tun, + incoming: incoming_rx, + local_addr, + remote_addr, + local_mac, + remote_mac: AtomicCell::new(remote_mac), + seq: AtomicU32::new(0), + ack: AtomicU32::new(ack.unwrap_or(0)), + last_ack: AtomicU32::new(ack.unwrap_or(0)), + state: AtomicCell::new(state), + }, + incoming_tx, + ) + } + + fn build_tcp_packet(&self, flags: u8, payload: Option<&[u8]>) -> Bytes { + let ack = self.ack.load(Ordering::Relaxed); + self.last_ack.store(ack, Ordering::Relaxed); + + build_tcp_packet( + self.local_mac, + self.remote_mac.load().unwrap_or(MacAddr::zero()), + self.local_addr, + self.remote_addr, + self.seq.load(Ordering::Relaxed), + ack, + flags, + payload, + ) + } + + /// Sends a datagram to the other end. + /// + /// This method takes `&self`, and it can be called safely by multiple threads + /// at the same time. + /// + /// A return of `None` means the Tun socket returned an error + /// and this socket must be closed. + pub fn try_send(&self, payload: &[u8]) -> Option<()> { + match self.state.load() { + State::Established => { + let buf = self.build_tcp_packet(tcp::TcpFlags::ACK, Some(payload)); + self.seq.fetch_add(payload.len() as u32, Ordering::Relaxed); + self.tun.try_send(&buf).ok().and(Some(())) + } + _ => unreachable!(), + } + } + + pub fn close(&self) { + if self.state.load() != State::Idle { + let buf = self.build_tcp_packet(tcp::TcpFlags::RST, None); + let _ = self.tun.try_send(&buf); + self.state.store(State::Idle); + } + } + + pub async fn recv_bytes(&self) -> Option> { + let mut buf = [0u8; 2048]; + self.recv(&mut buf).await.map(|size| buf[..size].to_vec()) + } + + /// Attempt to receive a datagram from the other end. + /// + /// This method takes `&self`, and it can be called safely by multiple threads + /// at the same time. + /// + /// A return of `None` means the TCP connection is broken + /// and this socket must be closed. + pub async fn recv(&self, buf: &mut [u8]) -> Option { + tracing::trace!( + "Socket recv called, local_addr: {:?}, remote_addr: {:?}", + self.local_addr, + self.remote_addr + ); + loop { + match self.state.load() { + State::Established => { + let Ok(raw_buf) = self.incoming.recv_async().await else { + info!("Connection {} recv error", self); + return None; + }; + + let (src_mac, dst_mac, _v4_packet, tcp_packet) = + parse_ip_packet(&raw_buf).unwrap(); + + tracing::trace!( + "Socket received TCP packet from {}({:?}) to {}({:?}): {:?}", + self.remote_addr, + src_mac, + self.local_addr, + dst_mac, + tcp_packet + ); + + self.remote_mac.store(Some(src_mac)); + + if (tcp_packet.get_flags() & tcp::TcpFlags::RST) != 0 { + info!("Connection {} reset by peer", self); + return None; + } + + if (tcp_packet.get_flags() & tcp::TcpFlags::ACK) != 0 + && tcp_packet.payload().is_empty() + { + self.seq + .store(tcp_packet.get_acknowledgement(), Ordering::Relaxed); + } + + let payload = tcp_packet.payload(); + + let new_ack = tcp_packet.get_sequence().wrapping_add(payload.len() as u32); + self.ack.store(new_ack, Ordering::Relaxed); + + for opt in tcp_packet.get_options_iter() { + if opt.get_number() == TcpOptionNumbers::SACK { + // SACK 选项类型为 5 + let payload = opt.payload(); + for chunk in payload.chunks(8) { + if chunk.len() != 8 { + continue; + } + let left = tcp_packet.get_acknowledgement(); + let right = u32::from_be_bytes(chunk[0..4].try_into().unwrap()); + let len = right.wrapping_sub(left); + + let sack_end = u32::from_be_bytes(chunk[4..8].try_into().unwrap()); + if len == 0 || sack_end <= left { + continue; + } + + let send_len = std::cmp::min(len, 1400) as usize; + let data = vec![0u8; send_len]; + + let buf = build_tcp_packet( + self.local_mac, + self.remote_mac.load().unwrap_or(MacAddr::zero()), + self.local_addr, + self.remote_addr, + left, + self.ack.load(Ordering::Relaxed), + tcp::TcpFlags::ACK, + Some(&data), + ); + + if let Err(e) = self.tun.try_send(&buf) { + tracing::error!("Failed to send SACK response: {}", e); + } + break; + } + } + } + + if payload.is_empty() { + continue; + } + + if payload.len() >= buf.len() { + tracing::warn!( + "Payload len {} > buf len {}, tcp: {:?}, payload: {:?}", + payload.len(), + buf.len(), + tcp_packet, + payload + ); + continue; + } + + buf[..payload.len()].copy_from_slice(payload); + + return Some(payload.len()); + } + State::SynSent => { + let Ok(Ok(buf)) = time::timeout(TIMEOUT, self.incoming.recv_async()).await + else { + info!("Waiting for client SYN + ACK timed out"); + return None; + }; + let (src_mac, _dst_mac, _v4_packet, tcp_packet) = + parse_ip_packet(&buf).unwrap(); + + if (tcp_packet.get_flags() & tcp::TcpFlags::RST) != 0 { + tracing::trace!("Connection {} reset by peer", self); + return None; + } + + let expected_flag = tcp::TcpFlags::SYN | tcp::TcpFlags::ACK; + if (tcp_packet.get_flags() & expected_flag) == expected_flag { + // found our SYN + ACK + self.seq + .store(tcp_packet.get_acknowledgement(), Ordering::Relaxed); + self.ack + .store(tcp_packet.get_sequence() + 1, Ordering::Relaxed); + self.remote_mac.store(Some(src_mac)); + self.state.store(State::Established); + return Some(0); + } + } + + _ => unreachable!(), + } + } + } + + pub fn local_addr(&self) -> SocketAddr { + self.local_addr + } + + pub fn remote_addr(&self) -> SocketAddr { + self.remote_addr + } +} + +impl Drop for Socket { + /// Drop the socket and close the TCP connection + fn drop(&mut self) { + let tuple = AddrTuple::new(self.local_addr, self.remote_addr); + // dissociates ourself from the dispatch map + assert!(self.shared.tuples.write().unwrap().remove(&tuple).is_some()); + // purge cache + let _ = self.shared.tuples_purge.send(tuple); + + let buf = build_tcp_packet( + self.local_mac, + self.remote_mac.load().unwrap_or(MacAddr::zero()), + self.local_addr, + self.remote_addr, + self.seq.load(Ordering::Relaxed), + 0, + tcp::TcpFlags::RST, + None, + ); + if let Err(e) = self.tun.try_send(&buf) { + warn!("Unable to send RST to remote end: {}", e); + } + + info!("Fake TCP connection to {} closed", self); + } +} + +impl fmt::Display for Socket { + /// User-friendly string representation of the socket + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "(Fake TCP connection from {} to {})", + self.local_addr, self.remote_addr + ) + } +} + +/// A userspace TCP state machine +impl Stack { + /// Create a new stack, `tun` is an array of [`Tun`](tokio_tun::Tun). + /// When more than one [`Tun`](tokio_tun::Tun) object is passed in, same amount + /// of reader will be spawned later. This allows user to utilize the performance + /// benefit of Multiqueue Tun support on machines with SMP. + pub fn new( + tun: Arc, + local_ip: Ipv4Addr, + local_ip6: Option, + local_mac: Option, + ) -> Stack { + let (ready_tx, ready_rx) = mpsc::channel(MPSC_BUFFER_LEN); + let (tuples_purge_tx, _tuples_purge_rx) = broadcast::channel(16); + let shared = Arc::new(Shared { + tuples: RwLock::new(HashMap::new()), + tun: tun.clone(), + listening: RwLock::new(HashSet::new()), + ready: ready_tx, + tuples_purge: tuples_purge_tx.clone(), + }); + + let t = tokio::spawn(Stack::reader_task( + tun, + shared.clone(), + tuples_purge_tx.subscribe(), + )); + + Stack { + shared, + local_ip, + local_ip6, + local_mac: local_mac.unwrap_or(MacAddr::zero()), + ready: ready_rx, + reader_task: t.into(), + } + } + + /// Returns the driver type of the stack. + pub fn driver_type(&self) -> &'static str { + self.shared.tun.driver_type() + } + + /// Listens for incoming connections on the given `port`. + pub fn listen(&mut self, port: u16) { + assert!(self.shared.listening.write().unwrap().insert(port)); + } + + /// Accepts an incoming connection. + pub async fn accept(&mut self) -> Socket { + self.ready.recv().await.unwrap() + } + + pub async fn alloc_established_socket( + &mut self, + local_addr: SocketAddr, + remote_addr: SocketAddr, + state: State, + ) -> Socket { + let tuple = AddrTuple::new(local_addr, remote_addr); + let mut tuples = self.shared.tuples.write().unwrap(); + let (sock, incoming) = Socket::new( + self.shared.clone(), + // self.shared.tun.choose(&mut rng).unwrap().clone(), + self.shared.tun.clone(), // Simplification: just use the first tun + local_addr, + remote_addr, + self.local_mac, + None, + Some(0), // Initial ACK + state, + ); + assert!(tuples.insert(tuple, incoming).is_none()); + sock + } + + async fn reader_task( + tun: Arc, + shared: Arc, + mut tuples_purge: broadcast::Receiver, + ) { + let mut tuples: HashMap> = HashMap::new(); + + loop { + let mut buf = BytesMut::new(); + + tokio::select! { + size = tun.recv(&mut buf) => { + let size = size.unwrap(); + tracing::trace!(len = size, ?buf, "PnetTun received packet"); + let buf = buf.split().freeze(); + + match parse_ip_packet(&buf) { + Some((_src_mac, _dst_mac, ip_packet, tcp_packet)) => { + let local_addr = SocketAddr::new( + ip_packet.get_destination(), + tcp_packet.get_destination(), + ); + let remote_addr = SocketAddr::new( + ip_packet.get_source(), + tcp_packet.get_source(), + ); + + let tuple = AddrTuple::new(local_addr, remote_addr); + if let Some(c) = tuples.get(&tuple) { + if c.send_async(buf).await.is_err() { + trace!("Cache hit, but receiver already closed, dropping packet"); + } + + continue; + + // If not Ok, receiver has been closed and just fall through to the slow + // path below + } else { + trace!("Cache miss, checking the shared tuples table for connection"); + let sender = { + let tuples = shared.tuples.read().unwrap(); + tuples.get(&tuple).cloned() + }; + + if let Some(c) = sender { + trace!("Storing connection information into local tuples"); + tuples.insert(tuple, c.clone()); + if let Err(e) = c.send_async(buf).await { + trace!("Error sending packet to connection: {:?}", e); + } + continue; + } + } + + if tcp_packet.get_flags() == tcp::TcpFlags::SYN + && shared + .listening + .read() + .unwrap() + .contains(&tcp_packet.get_destination()) + { + trace!(?tcp_packet, "Received SYN packet for port {}, ignoring", tcp_packet.get_destination()); + continue; + } else if (tcp_packet.get_flags() & tcp::TcpFlags::RST) == 0 { + info!("Unknown RST TCP packet from {}, ignoring", remote_addr); + continue; + } + } + None => { + trace!("Dropping packet with no IP/TCP header"); + continue; + } + } + }, + tuple = tuples_purge.recv() => { + let tuple = tuple.unwrap(); + tuples.remove(&tuple); + trace!("Removed cached tuple: {:?}", tuple); + } + } + } + } +} diff --git a/easytier/src/tunnel/mod.rs b/easytier/src/tunnel/mod.rs index 5139f9af..20c38e09 100644 --- a/easytier/src/tunnel/mod.rs +++ b/easytier/src/tunnel/mod.rs @@ -15,6 +15,7 @@ use self::packet_def::ZCPacket; pub mod buf; pub mod common; +pub mod fake_tcp; pub mod filter; pub mod mpsc; pub mod packet_def; @@ -23,8 +24,14 @@ pub mod stats; pub mod tcp; pub mod udp; -pub const PROTO_PORT_OFFSET: &[(&str, u16)] = - &[("tcp", 0), ("udp", 0), ("wg", 1), ("ws", 1), ("wss", 2)]; +pub const PROTO_PORT_OFFSET: &[(&str, u16)] = &[ + ("tcp", 0), + ("udp", 0), + ("wg", 1), + ("ws", 1), + ("wss", 2), + ("faketcp", 3), +]; #[cfg(feature = "wireguard")] pub mod wireguard; @@ -139,7 +146,9 @@ pub trait TunnelConnector: Send { pub fn build_url_from_socket_addr(addr: &String, scheme: &str) -> url::Url { if let Ok(sock_addr) = addr.parse::() { - let mut ret_url = url::Url::parse(format!("{}://0.0.0.0", scheme).as_str()).unwrap(); + let url_str = format!("{}://0.0.0.0", scheme); + let mut ret_url = url::Url::parse(url_str.as_str()) + .unwrap_or_else(|_| panic!("invalid url: {}", url_str)); ret_url.set_ip_host(sock_addr.ip()).unwrap(); ret_url.set_port(Some(sock_addr.port())).unwrap(); ret_url @@ -200,6 +209,7 @@ fn default_port(scheme: &str) -> Option { "udp" => Some(11010), "ws" => Some(11011), "wss" => Some(11012), + "faketcp" => Some(11013), "quic" => Some(11012), "wg" => Some(11011), _ => None, diff --git a/easytier/third_party/arm64/WinDivert64.sys b/easytier/third_party/arm64/WinDivert64.sys new file mode 100644 index 00000000..ea9865f3 --- /dev/null +++ b/easytier/third_party/arm64/WinDivert64.sys @@ -0,0 +1 @@ +WinDivert doesn't support aarch64, this is a placeholder file to make tauri happy. diff --git a/easytier/third_party/i686/WinDivert32.sys b/easytier/third_party/i686/WinDivert32.sys new file mode 100644 index 0000000000000000000000000000000000000000..d06738cbb78351cc57754fd484b77fac0df52cea GIT binary patch literal 79792 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%0DB0KXuLbP(03=gskd! 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�fDFaN&0OjG}#mY093SN>I=&rkPX=@)ne0>Ce<3;wQg-e3KWzfbe5=9r)Ee=ZMy)^}b&;%D=Wr5V{xe~-hn)XvY3e=ZNxJf5%r|GGR( zQ@aBA4&ET=7~Zk=1bu~97g1mVY>3bR8Q+Rvmsk`0n0))z1li)wAm{E^xIU->M-lM| zB0?ABYzB6j2)J_#o9{3Tb!hvb!E*yG{Xru8nejf!Wk{o(-Ph$>mKXP1_FI-yrX=NW z@Ewusy|&@uA|ccAnq!}@xuC`StqUEv)?T+PwA=DQbJ_9K)&RQ;4kpJ|9QZi#dYqcK zQPebZW3!lsS(Q|h3O>0#Rlz%={LteK#x=1$t>WD-ocmPD53?Mg8CS>NeBPKO9~Xaf zaJ1#Mpyf98b3*nPSU28u#na1L7VFm5S7&2OV}+j`Xzi~U4b#u8b5=9mm}1Rk5`QYw zLC_+5kKO*@!6@I_+rd{`Wn#?=E-cb_-Qv3Hw$qM=B36t=npNDzH%}KNo^6*2Ir1n- z_teXEpFTcYFr`VNnIAx1G)!(dZ~x$IWx4-}vxmKLsiwOZy|+vhQt&!?z;#LTHk{sJ z)Lw3T+5N4hZWp%PjZo0f3w^|;bN5m#9D}G4Q1XJmnZWN#@SD~Tew$&)Y#A@OE<=L! zW>D4#%CZ=x|Jon;0Mg8d1I~f{4}NUz4DBAd_hV6&F{WjzvX|rdvsnPy2m=k*=~F)7 z$d4*o4g9!LgTm+@0RgTn?HmnUsTd-Dwgv{R7UJ#soh&{Cf)6l94c^fJKt2HkqIk!* zd%3_MxL)-E1mZIIX4H`byB=jAmm4&!M)mffd&uG7Ivl6rBXL0&$-01vp2hrm4z zt~1Sdco3%VA1$cyod4khSasUH3O9IMR z;JQDSF~-;R_hsJjbMA9-(xXBs)F3Jyp!EAK+zSx`;8%&D9nUa$iwA)KF9Ov9UuKqO z_VBG1xQ7vc>pvQX6#$sfEDQs~$^mQ+pRh0tK2tKn`u*8y#iWZw6o4`{S{Ths4no28 z3IME|`8$l!1`Pjy`GNZ*3i7+_z_=m4$u$g<127)&E%Xg$`BeA9X|Y;EX#i6MdJC3G zPVieMi9o>rUrV2~9=QAQWBu*fAEEzu_rHk))8{K~7%w0{2M;nh!O5aoA1=HQ#W-kr zm~k8v{13j2i;r!G;WL=vpga73kGOw3I|cX09-OTzDVv@TGyS^^V6Yx(1aoBOQwYv> z@d4QQ9GEURH)aT619M=901N`K-+lh?%-S?Rr`T=j0cK#I+LuDJ4|WKmdIft^?C3OV zkT0SKE;FIf9VxUhYH$#+cSh^=0s?})Jm?hb;NSo!D&5cC!xvEt;Knq{4=`TVm4W)< z)Bx|5-T}tJG|S*1YY$(FH~bia1j<%=gjfW7`2z?5huMWtyr@1=1|D916a*rM$t-xL z9U~eom}U{|YZOGMMIqwnlmNttc5n~33}5$yYe1|)IE_XLqFd8~y(mB}moS?JJJG0g z%8!T;tZY`EKDHDe3aIQwvGQC;@uGuQH^Wdv0Q(7Uj5J#B5Ki?rpiw~I5r`Z%>oCyP zw?@o>lAoMz-Ifv>P6?w2(`MS=$+Z^PS8&W0k;b?R3=cpIa0OBVy+VNNHw0gWQ9#R2 zm{(Br!+j0>C|>?X>k)_`dT^LOEhs>ZvL3W&i&#O?^9l{8(kNy@c61Lff17YhIK|s8 zgc=015xgY=)Uc%lP&~rstFao?08i1PtSK~~U|OID=(j;|I2d{w!c@;f%{w3f;RKJk zL2yXakAi@ZL|A%L!)C_Bl0w%H_W>+ojAk*T3?79a;o`r4#)29~XFON*>vP}Bt|8&} zY$J-P2={CfIIP<@@hso7$NwPX<8AMhGuqZ;6J@VAaW7Lks)CQ zU|fR3>9!PKKoNxo0b4p3?hr)#R-zxZf)WKGN*JZa0l|@aVPU~uR3Hcj(+M6BqV#*2 zEhR8GVzvNrkx^n61j`qgL#7@<-T{x(hUzR3*(y~JF|50xQCi7_<*h}JYEm^qLisv>+ML;Sr0 zg6L{tQNWMnKsb5(d{em&L9kbgK(Nh$=`$egKS2y&7Z`X`KR-JScQQ1#FtnYP{@+={ z|0IkF!41$!1GkLDAQ^t2^*nID-@rLpegOh}NCXT|16QyQY&fTdA#k4t zeBzl8Z0X?tF$DgFX~KeM<-z*{q`_Yd_^bHs?E`RzHXMsL175o}D8qtPHZRaMGoLhO zEVy(8>+-eWuMyD7cA%~m$RAn@zAeG2WGhD18kV_ zoN46?;E{~JdVj|Pm%+~l0$SYwp4EVMD!7Nwvcp_~aic&_aiCQf#z=Sq_y({tq=0)O z!0!zxcLq2d0M7yehb%y>1Pz|I1a0X9m=~ke4Q2}%ca}N4a0EDL3_i{9VCF3oT8IUp z9y|`yC(eTaKLh4)_*WjO0X$LQ7k*QQ1bEg0;Pz!a1)uZ}0c9}fd>MTO>+)Y<(uiZA zr?cY*^Be*_M**D*U!lPd95;gLvigP#<_}EQ%-F(r@LtvL;$yS)nInV&4r_oT81OC} z@P|I9XTN*)_u~(m0dffQ^G94ib9IKN@O%Yta{x5bzEeCa>0s>$aDf#8D23%Y4D`zb zP&O-{oGfKs@{MO7KjfLffOJJ3V}923D9O}2ecd72jxKrp&sZpGzuY6 zY$zTS9}0`oLfN34P+q8=sB}~=>IA9^bq&>tdVqR|a>5niiwOIOz9c6N1Bj_%!?>{4xA_d_Dd-o{b5GB>#|mx)-$bP zEeUOmw!ZcO?IP`R?KU&Mwf>L7iruHl6!AFLb`>u#suxNb(kP z4*4({p^HFBBSD-Ba)8{RATU-fkRoGDR-yt>VW>D%GAb37g(^hVgHgGO>Ozg85NHmx z2wD}LgDys2Kwm|FLbGE$up6*lU<4N9q;Xof&A1F)F77n$8m$>=TURCF3T1D%D=Ll>Zr z0M3@6%h2WMYV<{P9l8;4xdq*Z?m%~=AE5it1Lz_2EA%jW6g`ffL?bXP7!C{%h7Tiz z5yePgq%m?BMT`mt!eB8(j21>0V~8=uSYWI%_Lx-|SByKx8{>xwz=UAvnDv-wOgttT zvjvliNyB7dvM_m=0?ZLiF{T7lhAGEXV=jXK<+KrV71M%g!*pP}F%K|(m;uZX<`rfb zGm5dsI$}5DcH+8m1%xs}IiVViSskH~a21SO8=(Wtq6dUN!T@22U`R9tV`WXWCvG67 z6Q3|<6DP@ww272QIz%cX$!R!g_-gFZIHqw?qe-J(V?aYd6VlYu)YdZ8TBmhHt6ED> zdzW^jPLB>RS%#cM-bcO#&zles`vG5zAzqXeiiomAg`nbr7C8$f^(By0L9`;8gf>BY zp(D^+(YfeT=o<7aN1QMeOavwtlY-fa$p^h|#)x4}K#yFpUf6Zm)7Tm;6K(-+92bng zfWL?Tgcl$v5sU~Pgg79ZF9lSzwH%R=jb)}$7%wy?IMHc4Ag+g^K< z_A%}A+6~%6+VVQ;IwTzn9Uq+vo%1?3bn3}X%rS4zEfuBv^w{dqLnN$`CO`f8sG>8SxUamDo*`A}Nz>NvpxAMUnQAnm{i*Nn)BC zHO;kFYx#mPD$}aass|dWL#tcsfmWZ^fYy-KE1;c5wFI?gwNcuJU~XA!J88RVQ?vuL zBemnSleM>N@7CV0eHZj}q0V9*X`N7@ua4?m1U+RV3y_zPW63+n>EvuM8=t}BorXZ< z0evM6VIT`A2RaY+GUnh2GyzRP%qVsgFNzqKps}1l2F^hoIHR!gQ`Kb zF)Ygy)N^26#!)P2UbFyO3@wFLL7Ss(&`xMKASq$!STG}Vz%0A~=3F;g8l!;0U`QAv zFoOaxp^ zp}kBS($>@t(hk?&sJ#!^g<|baZDF0II_^3l!19#poYkqng$*ag~$lm02|aD0rVt6o6PD9!M0DhOm$+Ht>FAhiol7W^(C>Zu1 z`jml~8iI)l0VUcMuyHO=F-`f%i)3diNVMAxKs%U_NYp}zmyOds#S}?lW?@3IA|O{b zE@d_(3o?+Ly63b3z(UZOn?L!$v4uFtbl-xC~k<8joE>P zO_0e%A0-Zn!8aT{0w#FX`rw2KB#)*etQq z$Z+Q+ooJ5w%ZRa4Or^ zuzOPy>rBgvcTad6h>*4}-rej|@p8QV4MiVq)=)MeFf4CC~SduGiRcYvDcp$5}il)DJ8`_P#xkZT3#}Q3-ve zcvgkUl|iN36KSH&o_Uv&nwM?V*(Tpy_b~q{&W}$ze^+;JdqZ*_@0ua?YfL~ik%trN zpoAJo5{$Iu0u~V#p#pxcr8=4K*k3(9rSvxOXj~g~q?{o(k_(Z@uPiJ;4B4QCaCJfW zo)uySU(iMvAjLw5Xb`agUw}RrSk+Qkc~QvBk0oO0 z=^M9KObgw+c36vAEwe;Ed0KY#P3=Xxych)%fDpvFNWA854HlCxW zRI0}Kx3s@EeyHBmx2~Z9%d8TU`OxL^f+DW$de-eHc+}s!YTODSIB;(6wbKn)^>(uI z1SN=NOdR+UF2YUw@4a(2g=8qq0k59RguTjTKG;VnEf5yn_31p*%_5a!e1a++gB;6F z$nJ9=Pg_IpIFw^oGjhzY?@-X?y8)*UFWg`I((jq$$jOl>%0vy$gbM#ncQ2{FCNHi! z4jSU)fU{lJ7o?0A+TAj=|0SNkfmlxfel%9Hp_ zB_TTKmXoIgkJSq?~twLB|IQk`(Ue@G4tBU$OdZwKx7qthx5F&=VeK!_I1P^StfBI1OIH5}xc6 zcbk$D&`xBIc>0;_)1HB%ehNH`EvetI{=pvW5%1>Xsw^iO*1hOcFDiNvba&|MeVG%w zwddaO?r*xcKl^P{uhcP~h_edP2fNNs#Xm!~zkQjgv^QkO*9*e8zT94jKcr@bPNN(9 zo8SZ^Sus{#YSpU!wUi#42C&*>(u7j&@wvF#lRpd6@mSS^v~w77Y_adgP>4DrN8C7YjDUR#hLq1z?0 zNNLhFPgMDZ(TBG+oxWCzYgpf;S!{ULG}<=#sZ|GUEGUiN#`2=-IJ{n$$zP;-!mrA@2<{b2Lp)#Hc{%0Xlbx^Djt)-i$3&z6uow#%woq~@ z3y{9}-|NU(6^eejC+90J(^OqiS3g7=zuR@url&ut(3D?Qs35FDHBk@$34I=_}B# zEPc#GTx=ozxCi;M)!1s;p=h5v8+_$fCi~{eRi6V8Z@y#|s_i|&E6wg;ZFIXur# z9ntW#x%f(Mp;E`H_~ZejjV|3#BAWzIxh&sQF6J&r@orr^vByU*+##JS4f=^?(0@y- z0hbJDQw$pYU7KR?;P$&q=Ja>9sT!m*txe_sq)mapXyhnu3))CT9-7<7X?JTE>~3uX z?p6Zg!tF0hs`OW{|BzO6QYGo#4w*Yp%BGEv?zG+$Sy8g-$_3=D$Ek|^G?oaJgexZm zHt3pP;TuW-p|7=8>|>xWaX9(Gx-e~B&Wxer@s}iSht5i#?MoEKdv#>%Yu{4dguS6W zRD5_rDkc{lSC#Fs==d44VlBRP8VxdMvWpGwY9f1YyXGUyQoCJ z7e}R5Utju$`}M`PB?bmTWp=5T!h0(ArFLYtunCc9JyEH=*;=BYSCCDJ^uV=;}W)>9v0qC%kyti zIk94U#h-=(cCQG-ZVLvTMjpbfJCw@`CJB3cL%FZp^4NFEAo&e^6(?)mO9XECIbo zPg$|m_-f;!rsX7oSEk3vLcgdp0azC?JVFkL4S0mWenP+1Xp5&cnjpmgU8AwHvBDaS z8Co%|&d% zeaQAHq7o#`sS^@P}YF0e(R>!vsV5Fah#&P3mHm95Xs?wsA1 z$;Q<`#&uOMXSZ8IH$F1!dc5Ag;_=;NuLH+h?d?~@zxc4o=T1iG)cz?G)RX6nQzY+P zl5QuTwq1KBy(V=pXBKVOUgT(&!BO+iLd%Zl<7;VUlMiZ=d0)N87O&Z>Oce2A4Zk;r zQWN%noN;D(I~`Y%m2>#ZxUzcXq7@1OE>F)ztht}^@Pv2Qx)({JTf|X`AGVy$kgtVs?94Wu=a|Q9ZI$*+&VI)ZDf4i8-F%&olQ%X_g#(Mkgdn_ za8=xb;syb24P0(tzDnw%1bf5c4U$>;nQ_jm3hgOhZS=XvOq*fK{^JJ_MXgn`vDhai_FB~scPq_R<>P{1#Mf$W0Z z(*S7$Dm@Cd1d@QET!O;(eiUg3J0n|rTL(LP>2LWjs9DXG5mRnZ}Xi& zr9CweIP-LmK#()$3*r@j>BnCcN1nQ;61yck2AM^cPOHg_m$6SjcdJ0h?(JEZx`?jD zEJtseBvrg@Y%#;$cyB~GH&Ns*vW)X{C)zej?V8tR!Q9FeGLm(VPq561&QQLn&7;Q4 zTN_=j_1twcTS;?V_~6-qUjocZOuMqBVdyytqX5&6&#;LgYTetbS zoRY7_Irh~U=Otds$geGzE_08qUipES+|%}AQ|dOy(A%mR7FScX?g$;qt6BE-j-7O~ zJ2iI|m;ET~vnf=`#MMoUnyZz>jwq=cKY7@*d+h--US|R1t883_JC%9qzJ+^B@r47t zLLH)wP!>H==9Z^pg-n>hlBwg@IaY0sJR7V!&~MIOu(C%O2+&#m05lMhd^iIQ0lbeA=cb*C9ShJ1TUj zqtrm+h}m{Mk`VI}rBC8VWltV$See+i9S54SJDAiaT%P*0Kq(IQ*-IkApFNfh*4i0R#EXmA3}s4IFqg z$OJMfFeuPVA5%+)1jrPzxz_Fn3_mF^hP?~4PdJKk6X~z8OV}O$h|C|J)-^C#9!nUe0I>w{Lh+*$Hs z*zhSjSmNa(%Z!Hg6lu+cddH=nAJohLa=N;JvK-sjTRfrlv>v)pSKy^G=_-OKZflHt z!Jof`t*I~&?N(0YR5oKjKJ^N^|E@^=R@Ky}w!_Ky6!w2c`b3^{7zD-JQGoL72N?t(a-dg93h*EM^$j&tki`)f>3W&wLQ2JBt$w7ug; zq>i4mBIf#$X^K3Ysx;>wYwHv*vhCe0_dX zrUETzm@=NF^P4i*ho#V@mty|XqNSY)B}esloZGgjrD5OYv<%v77mZ+~L0p)N;sLqn z0-P?}r;Jlv2a_rV2UZBPty#BC8XLOtf_y1?HP;a-^?(>P51Y1BXM#?l+q)%h@s-_D z?oV0;)@_k$`XYKG-N11}^G)V1gtwxj?CBL^?ZcVZ45dhJd1aZeUCegG(gqCt7hjjPFdIsVDE6ubH6{S|rcm#o+8-Sc+gl~*SosRl2$w+Xo?=AT$ehSh5Wn819Va)~VN?P{d zJG83JCy&yVFP7~LoIGZlr88p8OFdl`G|Jke$J!)XXFV`p`lZCn!({hG;|gQDfGeZ@ zDGKSyq=4`~NuCYP+tqH2daLN0jM}f-L>O$Q7PZT7&#Y}K?W;LW_$c*Af8+6&63<_1 zyF46kPwO=Y+~ynIt4rsZ>{@Rb>hmIy3b><d>QQ8n=WB-}U`y(b(!B9@r`|mU2uGf)Q+$l!rt8b0bq6kU3mSn#BOpgK%s-j6JY3 zC?pe-nFE6Twr?RUCZq^sjT8p90_Lob)D5idz6Y|HAj4@ps10dCqyj<#ZjPx6_}%&p z*g)6|oN)pfrYZoU&ol~bpC9BiukD*l$G6$|JB0MvUVu>8KUwL8-GA*zyK(uP?m^Ur za^}OTLL23tm9G|#DSkdxdV3FVv^KtgdhdM>biah!MR$@T$1c>m_n{$5K496<4!`u9 z?;^L7;)+>V?_(Q;UL4@CKk>COHZwr^PR7gffIg#@DrY`wYPwX^f4;jlJTQw!8~ezv z`2NnBwwGd;gjLqRZ8a)D42-ixTyVOQb}7<6jlQ4TYGvbb!Cm`KA#ut#a{1RkADP(7 zqGkCcQNZ(Mxgk%u-sOeZ>Mi+KW#0*|>k(BCT3_18T0WG_k%(PcUshkD%I*G|KIuf=v*I2rIWUEG z=TKXnce{pu{bJ_NZLf1=s>!Dc&)C>^T3op#GCHB1*k|e20!heeI=H zF{jt*DFiWZJ52A}#+evY`(&)@;(l%=QNM(PdE_7w=M_SnoH6T)nYzq>qwZhY%$#~J8N-e+_K8zo-AV1+VP~nz>&sg+FkMx4;Yikb_Yg{D1j6Kly(V+vCDyt$@U0grr;dC>FX;sDk zZ4;@uh>xjxuUBSEAn_WL!IJ&p=vF zO;*}JWr@G1zRT0o&sE^#l*CZN_JOnDGz4Y#8 zFw?WM2@kdoQCcvjf%}eo9W~AvS^rWobqV(l&p!U!a_31x2}{y$=`(pv>hpQ6Jr^$2 z#_epJ^MR!%pY`gnv z_5))XJGbW^wz?ZSkaOBX(o}qb(AXLs1QDkoFnDKk|HVSDR~E^7%bhU}Jf`n+Fkxy^ zES3w-hB&))OGYHPb6`rY4>_8rtu)I*#SAzeKVB2HLDl`>KC=%K@0RQk?b_z+`z9yy zb&QZ~hwTb2+4FB6o}(x}Icv2OdA0ittCW6Pk!6Og!Hmu90yeY#2b=lZ_~xw5d_Wey zCtjQr7_y9ySiJgSME#cd;va1$d;Cu}^S6OdvuT3~LYqGtOg7}-HJJaoIH`f2kt2xN z|HtE`vv~ie^)!W>blZ7bNE>93+x#i*~GqX2J zS3QrIsy!XN;d*VexJ6(1J}ZjJ(8{GR%`B4yLq|~!l1{v(y>(eh?njt3`!_D%tke~L zyj$YYo%paVOn0IL1??Wn-tSiuu(9;n*?MSo)Rs*KPqHrD?(e1wO61w#UWHeMZX%5B zm*0ff;eFDflsJMY-EwB)rq#L&3TctzUdjAOt@xYq4mK9)HUrE9#O$@GM732=qMAJu z$XfFMubDX}zTX+xIXPMd79b~TCTEJ18O1XvL5l@apDkiR$+AeNpYJ7G32mLij4c=4 zEAAWR-2Qb{rohY1H_sc9tjGIu<_a3v<6=on->dKydY{YArBw`lY0Sv1a_)Ct z)iySMV1*31Sy_hvi|JT}zLBtvb!hIEg076i<7`LL1m*EA>m_`B9;zw!eDGZ)a<5dO zXPt$r<&v#hTSb(%xv@-{_BWmK3yo0p7twnD&N2z5c|6(J>9oK)8*RK)0q^-SQx~1< z0&%+{*Q{Vp!A#VS@Mzk1`szEQdS!xhUd9q{Nmkk_y>g9S5Y=15u2y>Rk!@39)bV}B z?`saqXz%P0V&N4Duq2((Uu*c5UN?Lt?K3m?LD|4dqwl0Nzufq=_oGB;aG@7OryP1eF#<2QfoyyDg>CUk`7^i>W4A=AQxiOL}$MbC6rlGsp*%p?H9 z!J?nA=YKf1!Sws!%ghvEVnq~0fjq!DQ-u5b4LcHSCVVepMJ;56Nl0KT0E@$-u`Xbh z^}Un@g=T@WKWMn7KD&zA^{GC*)70t9a8t*r4b2(nhF7KZqoj4BUG6);v8$4Ou?HVN4$-l9clrC$`nqYSmx0cyAbObD2G*cANE*uU!?p zmnCfFDacsqASB@$wE?=)vNztEj~_EcZ%8aLRfpa~)g}*7{_NeAUN? zMaVmRC%OARue#sy@Vzr4MuKbU=DH|)?d~3{_ zLcWsT4400G5tEml2B{yWM9r%L<)nqxk4X)-bJOeY9p7f!yGfO=1G`m1ak1Vr>m+_d zE4R)l(UPM z-In4V7#!q1XD4qZ)r%G!7VJX@$!xTcVA}L*iwNQ2#T2uk`fNFPHIjF5WSF$QAC=}U zZ4F*C5CxtI3<(Ybuaf}#WzuK}g@yO@K)4eRPN5MYfCbLn#eiF|tH-#75_bPr2nGS3 zX@b$e5^M@i?Sqs16b7}Rf1cC;u=^;@*yIoM18+9?tAyS{L$5*8+odnM9tAEI&YUtj z?QnJ|r^B`7<(G@zMn#F8=~fm@?PqgOn(udheQFn>X|f69Da9W)(#ZZ`0A<5pjeVK# zzLJAdBoed_vTs4%x>Z`Kp~#`%K79AJZjB_4#re#uUTpWJ&~pL3ftxlMEH0EcyWz|x z^JK@`vP^xut#8Q}*Hw6^<=&SWl4}+)yip_7CR2XaUM?=~2FoR_t5yz+yHPUao`^h7 zEzKp%I<~at9PqxdcTgsBzfGjr^X;Q!h3#BBde$u6 z?u1hl6$`SLS`BSZj&rYW3Dxvy;?hm)h`aqUcHs+)&36bA+Y)A$I4yuvR~gwmAfF5l z#?7Rtb1}2B%`E7_HF73k1`^F?*E@QVQj5&C9TnItyJh3F1XWE>wRP$YdyNH;!!?-Yo3@} z2q7%QsdEEX0e#Z1D`p_hcma*SxsmsBE*E>=sqxUpB=a2gt82pZpI4U>4U--u zK0ohfE4!g9x{jMGa>1DADQ&ujLyAG^)>~!5&Wkt;b3H?kRk~G)W<9%+?cGEOY@}~Z zLKX_5vdU8`M5pfEJx9~NHEFnn$q)0fSxq6!ec82*yORr8w)5To;%9QwIF4x%c>`5| z`&s;8<+-P%45pSCz45j?_Py+W2bAyM@IIdRIuKkBd8)F1#w6 zDAH*v_=s8I*$VA*?`@Xr);aFf7fy`J-LOos_XVx!)yk3H;WZA5+`@>7+bz7g_7V(H-e|Uef zKl5gj{9E=1xPkEGW)@VM)dP(D>e(%7%4{MDY8ZjGT*S#;*&GZ{n#qucctb!Y1rr zx}Z1Ky|25_SLlJ~IvRQPQ27V(ubp_G&>1 zOJ7dH$%5i#6qoaHr5DVuZ)Nk9y)v{%Tx=IpsMP9ux*S`{&<4e0eo064FRIJMCcKMN z@ZJ^T>(@a0Ouaieyg)@3Ga%k9eph~_`eejsRP;#Jft^z8vfWSAzC$m(K7}#OcXnM> z`dC|Oy&^=vinAPfn}Q$M?wc=Ws^qi+--N-Bxv9Y zUObwY_ZOknfKJ2lryYcN{tKsKT`VjM>Kx$ojQADlCy(pVA z#e8YV2b=LaHYY;w-L~Aj^J(|$_s4J{n>jU89fmu@aG^q9{1-S1pY_Mq(pldWXa&|#2nN5TRZ0s+c{|#H)!?uybzmj8)}IJ zi=-H^f&}5Q87l-f)MoNH=Cy-{AYWVEmJ$*iMx_VSqSXB8fp7%m8(Bh3EIdoNz$;q9 z!OL3>5PA?dOu$0M$)4Y&rNd{yX7b)U#~g3$8am|a>33T@BW?BFn>iqP$Pum}#bO1m zgjN)o6_}(Lea|xq_|;A`jC2Ss*gM>7+EM~_0h9&EVp$7o2UQ5KipDT3vCB-Cfgy&^ z)tS%S14FFyCqu06HDiV6!A<{3J2Gr)W<`T0YGkATK`G~lbQOa4>5B#F6Ynu5&nMhQ zB)5(_?SDe%JM3&N%Bm@(q*qzAV0FTwmYQV!`o>GQlP@>c@T8httl5(C*@YwXq>B2Z z+H{}8%F@@9w^5B0$YD#->Fbt0|2lO~^!3fJHU4^GJDl>y%LC1_Sf3uo9UG1fN~!ZN z=J1t2RhOOGQ^dAXH(hl5G3Te*Lgxe)B^Z`CHJsdeGGpTn&7BW7l!vZ9uiVhwwrS}K zfie#@)_i3w`iDLM$?^ zbNRxH0RoLzc%*$kEKs*j@4o&yZqV^HXLpg^>E`9s^$&*EK3gGOdCIGz|FmnoW!{Ek zk}E$d8n2gO+q)9|&|QC^a&hB`F}@cU##CQ1xb~c~v(bh<26r2udp*SOifC56UTi2n zgkJ2^I%w)@&gHIbyob#_GwZ=U%w3ttxGjMtCbav9jDt>i`zg0uW_`xB#JwU52J#Q~ zM82+Xh?QDrE;L?VB-0d{;v3T1Eq2&VsvtKf`ik31+DC>fym_!;R>zC zR3vvsi@l+$9H7f%5;=N^Ez!+Z0UG|73H*9<)XXpXqKYSoX)8ZWHtEwm3JX3QS1biC|1OX*cAS+vqDVstukk# zI3N~TkkART%(%Xt4&@YMK>_`P{8R8#;``Eo3CWFG06fo`r2-RD_@9SH9p^mGgEEDT z3q<2ZJo(b5RyVmz+N(s}_Kf%C%(S)%up2+PJYEr=#BgH3uZoNkq#7Qk9{Fu0J9oM= zLas>g%AtdbQ8AZdjlbT#=O~^rWh|Ch))aEP^!SMr$28Z6Ex_xPHumBOeRW2$@{#m` zExN-tXpi)dH$x7lIr^&{$bgKO?5g&>v_6zDcJac^e3_T-Pg5rk?`7vYYTfQim80G4 zF0f^jdW(OOJ1m*G|LcuCc1<`*`46lW)Ma0hw!wDLwi%aCn;W2cw8*bo%Z{Yb->+wWQDM|d%9i{OLB!^E{6|vcHN)$Hwx+ov5 zkvJ+*^7de&2#9}(OnhfJGcyR2h&%_d!5_jTB4EEuq#bPD{u(ApMB1=%&j<@2IK7;R z)MZITY5_Aw1ak8Yb+R)TGZJX#pU*BUhg2XMX03~>+^gHa=aXamF&&x9N;NN78f&wpxpI}a zhtQXGpO|xF4u(hv)ondt;H&ZW+!MT2RTsJPG`kszDo7pQ`LxS>xQo9c&uau(sgxJ;kZv~$D@c8`Nm@^8Odt3AQ^y!nLv?a2-mz#z99v0;~Fvo3A zMGjZ6mnZ1$`PhDA;dS|?cGVo)G(O0P=^@)z<*4j#TXTEa~?+f3h71ok0<>qTXJW-XuWl|jVI5+UL zqB+&=D!JhK^NQPX9=Y8L4;@qjZ-h1@Ewb12ha^50?jEd)cN-99s;UNOQ{G4WA4+H! z_q%g6X87fr8+Pd$6Cbk0>S|V3yO5#U8Exs8T-^ourZC@>0ZiyODm$Wo?V)aV1mXHhaOLdYT>1(Eq*HGUr6^rE%95HoqJDTa+bK~NhTT7Y5`fK(m z$KB0d=iSDcV<~b@H&E(A?y1NWv9AJemQKjlh38o*sc&Jk+{dZB|GKF69{NLZ4y6Al z>!|KjCN1%BX$~6-Q=mfP0HI{l#N+IfJJbo8Tu(c<<*IwW%oIEHu|e+s3VjLr>n1bK zJba!FJ;!xlTe;tPIIqX%`BCQ@uDw@F@yfSDeu!ZmC=zx6p7p6#Gccj zwbQ=wD##JCFR(4JPOsC5Dq6Fyj;hC)piZ171h*E%6RG z$<}|_){LJ#9a#nq{IRv415>}ra^RS2s%N5A2*C@RTZ}tQdaQ~FsCZXbH&=5TZ9)%5 z{~GU;HT{UwyYwtRb6tygld=3PzMgyMBM0Wt1q!A5dK+6;J`V&5louO2p9H*K-qKxK zZ*tSVD!b@lr&xnsMFJ1gNKN|J(eMJJE9dEb5-onIIg%0=Zj98n9&k;w&`e$HP?mG_ z6xWgF+b@=mAj_7UMcBVq6pI@#U%l{#W9@=X3(AkwjxcX2T3qrD%W7FWb=s+Bf86FO zPeHeb$G;e8Tbdu(6|>~S0%U#DqgBdNXe<8D>xxfe!$0YLcC6P{sf+p&U6N-bA7=Kl z)HvX2)W(x67i9Trjr<040;xWpu^psB&a)MT`qk|mwSvvu%2%04dt$`*y|y^A(F{LU zo_=y5fYnxI>$|EkPPfw+4{J;?6UwWO*O! zsiCd9P$Nt&u7Bv@+mXAD;*5iM=BI^7L<)Z=DvILFU z-&B(9EN(r_l5>tzd$i+{-X_cb>>Iu5Dh&@+ueX|~H#y~OJ{~%FYEtM{C&E$gx*XSA zvkL=Xu4cUPs~p?5`%2B?3@yUy1mTV*Q}r@M@^54T?~} zMGV^U`tHK+Wp>dd52g)`$ERr2Q<5i2#_*SL%e?PrX`Wxa5mOq!IyRM0BH#8)<&dZR zgLMmUxs86=X?LsZ)h5T4Zy#RVapcMFU1vxtHhIrOsJz$lsv$d3S8<9pPs%sTHS}HE z6%k{hhpa$9%^c&J*GEonP$HDvc$sAM%sA*ey0b0?aKWxnPmQ)En<_&*4I+U_p#cKM5$HTFKY*T)_D zeDy}Lt+U@jV@sbtraev>dIGOn0^J^6+?yp4mB?6pDt%Ghb&WEnN5&FG*Ox1NIP^=1 z>w0oN&q=eC?&t-}niXT?4N4|XnQ~)iz)$YZ#Ls4IrV=ggx9><_{JZIj%+c&`F+qW` zLbp|Stq)+W$~h`1TlBv7XV8&W@yQ2lL~l!(ALkI2u&-F{y6vl6d)%%5d16JG$JSME z-)@)nNN+8yHoFU~&29qs3c|*KfJ0NPU^(FA5L}E2bo`6f@}PeyM`rD7nrP!@s(D`E zzdp-5q-qdrEf)i-qk;`+Fmi>pni(C=LJXB*9nP8?TKh+9?L!^;bVfs!=!f%) zy#K7se-$IL&Cbs~rD7x710>u+3Rpth`gM* zX=U_%&&4G?d)`cJxMcgx%P!5@e|Ot2BdMFyzVv^a$oTEtv68>ve2Ve~R+kzaHQ7*g zBj6ub#9arQ;~}|olS@2wHu|~i9u#3I+@JDOWnXdbsXorVn;FABLsD)QU$2*oc46#` zlAM^$JAaABopebJm0cFzv+{l`hGb{C3c9cSxP9G;7oBnbb1z-+Dcc$z!nrY%(WS0t zVPa14Kd#+x9Mfzw|DHbR=vh}cdEe}0q>4yJu|O8RC@J6-N{yU%CnM^=&XzL?B3Qb zFf^{+bNXfVev1N^ixWPcSZ&p1#gSBhvmiOxxA$FB?ow8PgN1Ba0$XH5jUzTMEJ*vb zu!-q3u(M+V9?tr)aQOAK7b$ZA$F_h=Vzv30nWUJ3r$6snXH?3RmH+I`o6?~F$IQ+q z&w9V0vDu)pVM*NpSJOySxWFUZfa|TEU3Oh#pd`T$9~v<>F*7xaf(rl-@Bj-Klo(hc z#$kXfVSxL{47i{MfaVt1fdmti_*y7KNF5@3Wx5VX)(lB6lOgDI7uWzFXjF%30dOxW zNIx_CLWPAeHe=fY0_ssXS~~1YaHu2 zyLEGUh|m4lgortTIpz(SPiFg1OK%I%uX3LCBkyc>@bbq3a|GICmQOf-k$Ji4DW?4Y zHyzG&NOeBm=d!8eoX+My6XvCJm)a+8Xg<{~&(EXkbK6IsIcM|x>=Uacg zHnH#m$5c4M$$9d?MkjbE7Tb^x@=$COqnCjcd?XII>Xa8{tPNvq_02o`45>pdYDs?c z^ZM`D8*(=1Reb9gKCZa=!aGC#C~1SnUq}Wp=^Hk_GH85m(D(#+dK-%Zqo2cL3qg_E znNPczlJw{P)t6tsY5!T#kmnkqU+#PKCKxo%7zFtMd%ywL+&*`)EZ0rFVz}b&jeYlT z{yueYGxvqMj@jW&b+e}(*|`2Wa)Jc*DnV;*iwl=>POWe&-?{eN;*14zI(Wm%WEVNv z>ABb6{j}%BF}cRQhrfsD&7S>8He|Vq7ijmJM+4$Ho>2>qW?+2cq(Rng#^_HGm z_th+kzy;R}UF_MW>~Ne`;*|L}Ji(jsxw*yqrY9n;+UJD?>~@5{>c9U+!7f$IiX&X} z#!I=hlQFi()^1$kFH?G1;zQ<_N0Xg21gG+^p1jn9rT){}AIHx6&A1fyPyYOUy%sKR zK7mQoH5HBcJ{evuPCRMnW#ZMaM^|)$_|AK}qm1%^pDs?aqoGRZ5w-i zm64I?;&p`;pNr;S(rwZUkb7Si(WiD%&uGK)i!VjnCNw?_sLQjNkiB}}nIpT3*K!p# zZQrf!21no7`}EH|J@M|EsgHl^bM3$L4b*W2 E0GFf)F8}}l literal 0 HcmV?d00001 diff --git a/easytier/third_party/Packet.dll b/easytier/third_party/x86_64/Packet.dll similarity index 100% rename from easytier/third_party/Packet.dll rename to easytier/third_party/x86_64/Packet.dll diff --git a/easytier/third_party/Packet.lib b/easytier/third_party/x86_64/Packet.lib similarity index 100% rename from easytier/third_party/Packet.lib rename to easytier/third_party/x86_64/Packet.lib diff --git a/easytier/third_party/x86_64/WinDivert64.sys b/easytier/third_party/x86_64/WinDivert64.sys new file mode 100644 index 0000000000000000000000000000000000000000..218ccaf423ef0a67696226f9ef3a09149e4441d0 GIT binary patch literal 94144 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 Mok_m={2+h)7u^}>0RR91 literal 0 HcmV?d00001 diff --git a/easytier/third_party/wintun.dll b/easytier/third_party/x86_64/wintun.dll similarity index 100% rename from easytier/third_party/wintun.dll rename to easytier/third_party/x86_64/wintun.dll