From c19cd1bff36d7c53ba3e95ff53a9588634889d99 Mon Sep 17 00:00:00 2001 From: KKRainbow <443152178@qq.com> Date: Sun, 28 Dec 2025 21:35:30 +0800 Subject: [PATCH] add tcp hole punching (#1713) add tcp hole punching and tcp stun test --- .../easytier-magisk/config/config.toml | 1 + .../easytier-uptime/src/health_checker.rs | 1 + easytier-gui/package.json | 2 +- .../frontend-lib/src/components/Config.vue | 1 + easytier-web/frontend-lib/src/locales/cn.yaml | 3 + easytier-web/frontend-lib/src/locales/en.yaml | 3 + .../frontend-lib/src/types/network.ts | 2 + easytier/locales/app.yml | 3 + easytier/src/common/config.rs | 1 + easytier/src/common/stun.rs | 613 ++++++++++++++- easytier/src/connector/direct.rs | 6 - easytier/src/connector/mod.rs | 1 + easytier/src/connector/tcp_hole_punch.rs | 730 ++++++++++++++++++ easytier/src/connector/udp_hole_punch/mod.rs | 4 +- easytier/src/core.rs | 12 + easytier/src/easytier-cli.rs | 4 +- easytier/src/instance/instance.rs | 5 + easytier/src/instance/listeners.rs | 1 + easytier/src/launcher.rs | 6 + easytier/src/peer_center/instance.rs | 9 +- easytier/src/peers/peer_ospf_route.rs | 21 +- easytier/src/proto/api_manage.proto | 1 + easytier/src/proto/common.proto | 2 + easytier/src/proto/peer_rpc.proto | 12 +- easytier/src/tunnel/fake_tcp/mod.rs | 22 +- easytier/src/tunnel/fake_tcp/netfilter/mod.rs | 32 +- .../src/tunnel/fake_tcp/netfilter/pnet.rs | 30 +- pnpm-lock.yaml | 273 ++----- script/install.sh | 1 + 29 files changed, 1502 insertions(+), 300 deletions(-) create mode 100644 easytier/src/connector/tcp_hole_punch.rs diff --git a/easytier-contrib/easytier-magisk/config/config.toml b/easytier-contrib/easytier-magisk/config/config.toml index eb0390a6..3fb8d1f7 100644 --- a/easytier-contrib/easytier-magisk/config/config.toml +++ b/easytier-contrib/easytier-magisk/config/config.toml @@ -33,5 +33,6 @@ foreign_network_whitelist = "*" disable_p2p = false relay_all_peer_rpc = false disable_udp_hole_punching = false +disable_tcp_hole_punching = false diff --git a/easytier-contrib/easytier-uptime/src/health_checker.rs b/easytier-contrib/easytier-uptime/src/health_checker.rs index 2d83e29f..44cbc7af 100644 --- a/easytier-contrib/easytier-uptime/src/health_checker.rs +++ b/easytier-contrib/easytier-uptime/src/health_checker.rs @@ -374,6 +374,7 @@ impl HealthChecker { flags.no_tun = true; flags.disable_p2p = true; flags.disable_udp_hole_punching = true; + flags.disable_tcp_hole_punching = true; cfg.set_flags(flags); Ok(cfg) diff --git a/easytier-gui/package.json b/easytier-gui/package.json index 9df6e447..e5e2cb01 100644 --- a/easytier-gui/package.json +++ b/easytier-gui/package.json @@ -54,7 +54,7 @@ "unplugin-vue-router": "^0.10.8", "uuid": "^10.0.0", "vite": "^5.4.8", - "vite-plugin-vue-devtools": "^7.4.6", + "vite-plugin-vue-devtools": "^8.0.5", "vite-plugin-vue-layouts": "^0.11.0", "vue-i18n": "^10.0.0", "vue-tsc": "^2.1.10" diff --git a/easytier-web/frontend-lib/src/components/Config.vue b/easytier-web/frontend-lib/src/components/Config.vue index b5fb7f9a..1aa54154 100644 --- a/easytier-web/frontend-lib/src/components/Config.vue +++ b/easytier-web/frontend-lib/src/components/Config.vue @@ -165,6 +165,7 @@ const bool_flags: BoolFlag[] = [ { field: 'multi_thread', help: 'multi_thread_help' }, { field: 'proxy_forward_by_system', help: 'proxy_forward_by_system_help' }, { field: 'disable_encryption', help: 'disable_encryption_help' }, + { field: 'disable_tcp_hole_punching', help: 'disable_tcp_hole_punching_help' }, { field: 'disable_udp_hole_punching', help: 'disable_udp_hole_punching_help' }, { field: 'disable_sym_hole_punching', help: 'disable_sym_hole_punching_help' }, { field: 'enable_magic_dns', help: 'enable_magic_dns_help' }, diff --git a/easytier-web/frontend-lib/src/locales/cn.yaml b/easytier-web/frontend-lib/src/locales/cn.yaml index 311ea916..ea8be33e 100644 --- a/easytier-web/frontend-lib/src/locales/cn.yaml +++ b/easytier-web/frontend-lib/src/locales/cn.yaml @@ -132,6 +132,9 @@ proxy_forward_by_system_help: 通过系统内核转发子网代理数据包, disable_encryption: 禁用加密 disable_encryption_help: 禁用对等节点通信的加密,默认为false,必须与对等节点相同 +disable_tcp_hole_punching: 禁用TCP打洞 +disable_tcp_hole_punching_help: 禁用TCP打洞功能 + disable_udp_hole_punching: 禁用UDP打洞 disable_udp_hole_punching_help: 禁用UDP打洞功能 diff --git a/easytier-web/frontend-lib/src/locales/en.yaml b/easytier-web/frontend-lib/src/locales/en.yaml index cc0dfe7e..f4c918fe 100644 --- a/easytier-web/frontend-lib/src/locales/en.yaml +++ b/easytier-web/frontend-lib/src/locales/en.yaml @@ -131,6 +131,9 @@ proxy_forward_by_system_help: Forward packet to proxy networks via system kernel disable_encryption: Disable Encryption disable_encryption_help: Disable encryption for peers communication, default is false, must be same with peers +disable_tcp_hole_punching: Disable TCP Hole Punching +disable_tcp_hole_punching_help: Disable tcp hole punching + disable_udp_hole_punching: Disable UDP Hole Punching disable_udp_hole_punching_help: Disable udp hole punching diff --git a/easytier-web/frontend-lib/src/types/network.ts b/easytier-web/frontend-lib/src/types/network.ts index 6f8eda34..10fe8177 100644 --- a/easytier-web/frontend-lib/src/types/network.ts +++ b/easytier-web/frontend-lib/src/types/network.ts @@ -50,6 +50,7 @@ export interface NetworkConfig { multi_thread?: boolean proxy_forward_by_system?: boolean disable_encryption?: boolean + disable_tcp_hole_punching?: boolean disable_udp_hole_punching?: boolean disable_sym_hole_punching?: boolean @@ -120,6 +121,7 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig { multi_thread: true, proxy_forward_by_system: false, disable_encryption: false, + disable_tcp_hole_punching: false, disable_udp_hole_punching: false, disable_sym_hole_punching: false, enable_relay_network_whitelist: false, diff --git a/easytier/locales/app.yml b/easytier/locales/app.yml index 10ef47e1..2f210723 100644 --- a/easytier/locales/app.yml +++ b/easytier/locales/app.yml @@ -157,6 +157,9 @@ core_clap: p2p_only: en: "only communicate with peers that already establish p2p connection" zh-CN: "仅与已经建立P2P连接的对等节点通信" + disable_tcp_hole_punching: + en: "disable tcp hole punching" + zh-CN: "禁用TCP打洞功能" disable_udp_hole_punching: en: "disable udp hole punching" zh-CN: "禁用UDP打洞功能" diff --git a/easytier/src/common/config.rs b/easytier/src/common/config.rs index 4277068b..772bb75f 100644 --- a/easytier/src/common/config.rs +++ b/easytier/src/common/config.rs @@ -39,6 +39,7 @@ pub fn gen_default_flags() -> Flags { disable_p2p: false, p2p_only: false, relay_all_peer_rpc: false, + disable_tcp_hole_punching: false, disable_udp_hole_punching: false, multi_thread: true, data_compress_algo: CompressionAlgoPb::None.into(), diff --git a/easytier/src/common/stun.rs b/easytier/src/common/stun.rs index f586ed4f..0f832048 100644 --- a/easytier/src/common/stun.rs +++ b/easytier/src/common/stun.rs @@ -1,5 +1,5 @@ use std::collections::BTreeSet; -use std::net::{IpAddr, Ipv6Addr, SocketAddr}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::sync::atomic::AtomicBool; use std::sync::{Arc, RwLock}; use std::time::{Duration, Instant}; @@ -9,6 +9,8 @@ use anyhow::Context; use chrono::Local; use crossbeam::atomic::AtomicCell; use rand::seq::IteratorRandom; +use socket2::{SockAddr, SockRef}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{lookup_host, UdpSocket}; use tokio::sync::{broadcast, Mutex}; use tokio::task::JoinSet; @@ -375,16 +377,28 @@ impl StunClientBuilder { } #[derive(Debug, Clone)] -pub struct UdpNatTypeDetectResult { +pub enum StunTransport { + Udp, + Tcp, +} + +#[derive(Debug, Clone)] +pub struct StunNatTypeDetectResult { + transport: StunTransport, source_addr: SocketAddr, stun_resps: Vec, // if we are easy symmetric nat, we need to test with another port to check inc or dec extra_bind_test: Option, } -impl UdpNatTypeDetectResult { - fn new(source_addr: SocketAddr, stun_resps: Vec) -> Self { +impl StunNatTypeDetectResult { + fn new( + transport: StunTransport, + source_addr: SocketAddr, + stun_resps: Vec, + ) -> Self { Self { + transport, source_addr, stun_resps, extra_bind_test: None, @@ -447,7 +461,7 @@ impl UdpNatTypeDetectResult { mapped_addr_count == 1 } - pub fn nat_type(&self) -> NatType { + fn nat_type_udp(&self) -> NatType { if self.stun_server_count() < 2 { return NatType::Unknown; } @@ -498,6 +512,33 @@ impl UdpNatTypeDetectResult { } } + fn nat_type_tcp(&self) -> NatType { + if self.is_open_internet() { + return NatType::OpenInternet; + } + + if self.stun_server_count() < 2 || self.stun_resps.is_empty() { + return NatType::Unknown; + } + + if self.is_cone() { + if self.is_pat() { + NatType::NoPat + } else { + NatType::FullCone + } + } else { + NatType::Symmetric + } + } + + pub fn nat_type(&self) -> NatType { + match self.transport { + StunTransport::Udp => self.nat_type_udp(), + StunTransport::Tcp => self.nat_type_tcp(), + } + } + pub fn public_ips(&self) -> Vec { self.stun_resps .iter() @@ -521,7 +562,7 @@ impl UdpNatTypeDetectResult { self.source_addr } - pub fn extend_result(&mut self, other: UdpNatTypeDetectResult) { + pub fn extend_result(&mut self, other: StunNatTypeDetectResult) { self.stun_resps.extend(other.stun_resps); } @@ -575,7 +616,10 @@ impl UdpNatTypeDetector { .await } - pub async fn detect_nat_type(&self, source_port: u16) -> Result { + pub async fn detect_nat_type( + &self, + source_port: u16, + ) -> Result { let udp = Arc::new(UdpSocket::bind(format!("0.0.0.0:{}", source_port)).await?); self.detect_nat_type_with_socket(udp).await } @@ -584,7 +628,7 @@ impl UdpNatTypeDetector { pub async fn detect_nat_type_with_socket( &self, udp: Arc, - ) -> Result { + ) -> Result { let mut stun_servers = vec![]; let mut host_resolver = HostResolverIter::new( self.stun_server_hosts.clone(), @@ -623,7 +667,241 @@ impl UdpNatTypeDetector { } } - Ok(UdpNatTypeDetectResult::new(udp.local_addr()?, bind_resps)) + Ok(StunNatTypeDetectResult::new( + StunTransport::Udp, + udp.local_addr()?, + bind_resps, + )) + } +} + +#[derive(Debug, Clone)] +struct TcpStunClient { + stun_server: SocketAddr, + conn_timeout: Duration, + io_timeout: Duration, + source_port: u16, +} + +impl TcpStunClient { + pub fn new(stun_server: SocketAddr, source_port: u16) -> Self { + Self { + stun_server, + conn_timeout: Duration::from_millis(1500), + io_timeout: Duration::from_millis(3000), + source_port, + } + } + + fn extract_mapped_addr(msg: &Message) -> Option { + let mut mapped_addr = None; + for x in msg.attributes() { + match x { + Attribute::MappedAddress(addr) => { + if mapped_addr.is_none() { + let _ = mapped_addr.insert(addr.address()); + } + } + Attribute::XorMappedAddress(addr) => { + if mapped_addr.is_none() { + let _ = mapped_addr.insert(addr.address()); + } + } + _ => {} + } + } + mapped_addr + } + + fn message_size_from_header(header: &[u8; 20]) -> Result { + if (header[0] & 0b1100_0000) != 0 { + return Err(Error::MessageDecodeError( + "invalid stun message type".to_string(), + )); + } + let msg_len = u16::from_be_bytes([header[2], header[3]]) as usize; + if !msg_len.is_multiple_of(4) { + return Err(Error::MessageDecodeError( + "invalid stun message length".to_string(), + )); + } + let total = 20usize + .checked_add(msg_len) + .ok_or_else(|| Error::MessageDecodeError("invalid stun message size".to_string()))?; + if total > 4096 { + return Err(Error::MessageDecodeError( + "stun message too large".to_string(), + )); + } + Ok(total) + } + + async fn tcp_read_stun_message( + stream: &mut tokio::net::TcpStream, + timeout: Duration, + ) -> Result, Error> { + let mut header = [0u8; 20]; + tokio::time::timeout(timeout, stream.read_exact(&mut header)).await??; + let total_size = Self::message_size_from_header(&header)?; + let mut buf = vec![0u8; total_size]; + buf[..20].copy_from_slice(&header); + if total_size > 20 { + tokio::time::timeout(timeout, stream.read_exact(&mut buf[20..])).await??; + } + + let mut decoder = MessageDecoder::::new(); + let Ok(msg) = decoder + .decode_from_bytes(&buf) + .with_context(|| "decode tcp stun message")? + else { + return Err(Error::MessageDecodeError( + "invalid stun message".to_string(), + )); + }; + Ok(msg) + } + + async fn connect(&self) -> Result { + let bind_addr = match self.stun_server { + SocketAddr::V4(_) => { + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), self.source_port) + } + SocketAddr::V6(_) => { + SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), self.source_port) + } + }; + + let socket2_socket = socket2::Socket::new( + socket2::Domain::for_address(self.stun_server), + socket2::Type::STREAM, + Some(socket2::Protocol::TCP), + )?; + + if bind_addr.is_ipv6() { + socket2_socket.set_only_v6(true)?; + } + + socket2_socket.set_nonblocking(true)?; + socket2_socket.set_reuse_address(true)?; + + #[cfg(all(unix, not(target_os = "solaris"), not(target_os = "illumos")))] + { + let _ = socket2_socket.set_reuse_port(true); + } + + socket2_socket.bind(&SockAddr::from(bind_addr))?; + + let socket = tokio::net::TcpSocket::from_std_stream(socket2_socket.into()); + let stream = + tokio::time::timeout(self.conn_timeout, socket.connect(self.stun_server)).await??; + + let _ = SockRef::from(&stream).set_linger(Some(Duration::ZERO)); + + Ok(stream) + } + + #[tracing::instrument(ret, level = Level::TRACE)] + pub async fn bind_request(self) -> Result { + let mut tids = vec![]; + + let mut stream = self.connect().await?; + let local_addr = stream.local_addr()?; + let stun_host = self.stun_server; + + let tid = rand::random::(); + let message = Message::::new(MessageClass::Request, BINDING, u32_to_tid(tid)); + let mut encoder = MessageEncoder::new(); + let msg = encoder + .encode_into_bytes(message.clone()) + .with_context(|| "encode tcp stun message")?; + tids.push(tid); + tokio::time::timeout(self.io_timeout, stream.write_all(msg.as_slice())).await??; + + let now = Instant::now(); + let msg = Self::tcp_read_stun_message(&mut stream, self.io_timeout).await?; + if msg.class() != MessageClass::SuccessResponse + || msg.method() != BINDING + || !tids.contains(&tid_to_u32(&msg.transaction_id())) + { + return Err(Error::MessageDecodeError( + "unexpected stun response".to_string(), + )); + } + + Ok(BindRequestResponse { + local_addr, + stun_server_addr: stun_host, + recv_from_addr: stun_host, + mapped_socket_addr: Self::extract_mapped_addr(&msg), + changed_socket_addr: None, + change_ip: false, + change_port: false, + real_ip_changed: false, + real_port_changed: false, + latency_us: now.elapsed().as_micros() as u32, + }) + } +} + +pub struct TcpNatTypeDetector { + stun_server_hosts: Vec, + max_ip_per_domain: u32, +} + +impl TcpNatTypeDetector { + pub fn new(stun_server_hosts: Vec, max_ip_per_domain: u32) -> Self { + Self { + stun_server_hosts, + max_ip_per_domain, + } + } + + #[tracing::instrument(skip(self))] + pub async fn detect_nat_type( + &self, + source_port: u16, + ) -> Result { + let mut stun_servers = vec![]; + let mut host_resolver = HostResolverIter::new( + self.stun_server_hosts.clone(), + self.max_ip_per_domain, + false, + ); + while let Some(addr) = host_resolver.next().await { + stun_servers.push(addr); + } + + let mut bind_resps = vec![]; + let mut source_addr = None; + let mut selected_source_port = if source_port == 0 { + None + } else { + Some(source_port) + }; + for server in stun_servers.iter() { + let resp = TcpStunClient::new(*server, selected_source_port.unwrap_or(0)) + .bind_request() + .await; + if let Ok(resp) = resp { + if selected_source_port.is_none() { + selected_source_port = Some(resp.local_addr.port()); + } + source_addr.get_or_insert(resp.local_addr); + bind_resps.push(resp); + if bind_resps.len() >= 3 { + break; + } + } + } + + let Some(source_addr) = source_addr else { + return Err(Error::NotFound); + }; + Ok(StunNatTypeDetectResult::new( + StunTransport::Tcp, + source_addr, + bind_resps, + )) } } @@ -632,12 +910,15 @@ impl UdpNatTypeDetector { pub trait StunInfoCollectorTrait: Send + Sync { fn get_stun_info(&self) -> StunInfo; async fn get_udp_port_mapping(&self, local_port: u16) -> Result; + async fn get_tcp_port_mapping(&self, local_port: u16) -> Result; } pub struct StunInfoCollector { stun_servers: Arc>>, + tcp_stun_servers: Arc>>, stun_servers_v6: Arc>>, - udp_nat_test_result: Arc>>, + udp_nat_test_result: Arc>>, + tcp_nat_test_result: Arc>>, public_ipv6: Arc>>, nat_test_result_time: Arc>>, redetect_notify: Arc, @@ -650,21 +931,44 @@ impl StunInfoCollectorTrait for StunInfoCollector { fn get_stun_info(&self) -> StunInfo { self.start_stun_routine(); - let Some(result) = self.udp_nat_test_result.read().unwrap().clone() else { + let udp_result = self.udp_nat_test_result.read().unwrap().clone(); + let tcp_result = self.tcp_nat_test_result.read().unwrap().clone(); + if udp_result.is_none() && tcp_result.is_none() { return Default::default(); - }; + } + + let mut public_ip = BTreeSet::::new(); + if let Some(result) = &udp_result { + public_ip.extend(result.public_ips().into_iter().map(|x| x.to_string())); + } + if let Some(result) = &tcp_result { + public_ip.extend(result.public_ips().into_iter().map(|x| x.to_string())); + } + if let Some(v6) = self.public_ipv6.load() { + public_ip.insert(v6.to_string()); + } + StunInfo { - udp_nat_type: result.nat_type() as i32, - tcp_nat_type: 0, + udp_nat_type: udp_result + .as_ref() + .map(|x| x.nat_type() as i32) + .unwrap_or(NatType::Unknown as i32), + tcp_nat_type: tcp_result + .as_ref() + .map(|x| x.nat_type() as i32) + .unwrap_or(NatType::Unknown as i32), last_update_time: self.nat_test_result_time.load().timestamp(), - public_ip: result - .public_ips() - .iter() - .map(|x| x.to_string()) - .chain(self.public_ipv6.load().map(|x| x.to_string())) - .collect(), - min_port: result.min_port() as u32, - max_port: result.max_port() as u32, + public_ip: public_ip.into_iter().collect(), + min_port: udp_result + .as_ref() + .map(|x| x.min_port() as u32) + .or_else(|| tcp_result.as_ref().map(|x| x.min_port() as u32)) + .unwrap_or(0), + max_port: udp_result + .as_ref() + .map(|x| x.max_port() as u32) + .or_else(|| tcp_result.as_ref().map(|x| x.max_port() as u32)) + .unwrap_or(0), } } @@ -715,14 +1019,60 @@ impl StunInfoCollectorTrait for StunInfoCollector { Err(Error::NotFound) } + + async fn get_tcp_port_mapping(&self, local_port: u16) -> Result { + self.start_stun_routine(); + + let mut stun_servers = self + .tcp_nat_test_result + .read() + .unwrap() + .clone() + .map(|x| x.collect_available_stun_server()) + .unwrap_or_default(); + + if stun_servers.is_empty() { + let mut host_resolver = + HostResolverIter::new(self.tcp_stun_servers.read().unwrap().clone(), 2, false); + while let Some(addr) = host_resolver.next().await { + stun_servers.push(addr); + if stun_servers.len() >= 2 { + break; + } + } + } + + if stun_servers.is_empty() { + return Err(Error::NotFound); + } + + for server in stun_servers.iter() { + let Ok(ret) = TcpStunClient::new(*server, local_port).bind_request().await else { + tracing::warn!(?server, "tcp stun bind request failed"); + continue; + }; + + if let Some(mapped_addr) = ret.mapped_socket_addr { + return Ok(mapped_addr); + } + } + + Err(Error::NotFound) + } } impl StunInfoCollector { - pub fn new(stun_servers: Vec, stun_servers_v6: Vec) -> Self { + pub fn new( + udp_stun_servers: Vec, + tcp_stun_servers: Vec, + stun_servers_v6: Vec, + ) -> Self { Self { - stun_servers: Arc::new(RwLock::new(stun_servers)), + stun_servers: Arc::new(RwLock::new(udp_stun_servers)), + tcp_stun_servers: Arc::new(RwLock::new(tcp_stun_servers)), stun_servers_v6: Arc::new(RwLock::new(stun_servers_v6)), udp_nat_test_result: Arc::new(RwLock::new(None)), + tcp_nat_test_result: Arc::new(RwLock::new(None)), public_ipv6: Arc::new(AtomicCell::new(None)), nat_test_result_time: Arc::new(AtomicCell::new(Local::now())), redetect_notify: Arc::new(tokio::sync::Notify::new()), @@ -732,7 +1082,11 @@ impl StunInfoCollector { } pub fn new_with_default_servers() -> Self { - Self::new(Self::get_default_servers(), Self::get_default_servers_v6()) + Self::new( + Self::get_default_servers(), + Self::get_default_tcp_servers(), + Self::get_default_servers_v6(), + ) } pub fn set_stun_servers(&self, stun_servers: Vec) { @@ -745,6 +1099,11 @@ impl StunInfoCollector { *g = stun_servers_v6; } + pub fn set_tcp_stun_servers(&self, stun_servers: Vec) { + let mut g = self.tcp_stun_servers.write().unwrap(); + *g = stun_servers; + } + pub fn get_default_servers() -> Vec { // NOTICE: we may need to choose stun server based on geolocation // stun server cross nation may return an external ip address with high latency and loss rate @@ -759,6 +1118,21 @@ impl StunInfoCollector { .collect() } + pub fn get_default_tcp_servers() -> Vec { + [ + "stun.hot-chilli.net", + "stun.fitauto.ru", + "fwa.lifesizecloud.com", + "global.turn.twilio.com", + "turn.cloudflare.com", + "stun.voip.blackberry.com", + "stun.radiojar.com", + ] + .iter() + .map(|x| x.to_string()) + .collect() + } + pub fn get_default_servers_v6() -> Vec { ["txt:stun-v6.easytier.cn"] .iter() @@ -794,35 +1168,35 @@ impl StunInfoCollector { let stun_servers = self.stun_servers.clone(); let udp_nat_test_result = self.udp_nat_test_result.clone(); - let udp_test_time = self.nat_test_result_time.clone(); + let nat_test_time = self.nat_test_result_time.clone(); let redetect_notify = self.redetect_notify.clone(); self.tasks.lock().unwrap().spawn(async move { loop { - let servers = stun_servers.read().unwrap().clone(); - // use first three and random choose one from the rest - let servers = servers + let udp_servers = stun_servers.read().unwrap().clone(); + let udp_servers: Vec = udp_servers .iter() .take(2) - .chain(servers.iter().skip(2).choose(&mut rand::thread_rng())) + .chain(udp_servers.iter().skip(2).choose(&mut rand::thread_rng())) .map(|x| x.to_string()) .collect(); - let detector = UdpNatTypeDetector::new(servers, 1); - let mut ret = detector.detect_nat_type(0).await; - tracing::debug!(?ret, "finish udp nat type detect"); + + let udp_detector = UdpNatTypeDetector::new(udp_servers, 1); + let mut udp_ret = udp_detector.detect_nat_type(0).await; + tracing::debug!(?udp_ret, "finish udp nat type detect"); let mut nat_type = NatType::Unknown; - if let Ok(resp) = &ret { + if let Ok(resp) = &udp_ret { tracing::debug!(?resp, "got udp nat type detect result"); nat_type = resp.nat_type(); } // if nat type is symmtric, detect with another port to gather more info if nat_type == NatType::Symmetric { - let old_resp = ret.as_mut().unwrap(); + let old_resp = udp_ret.as_mut().unwrap(); tracing::debug!(?old_resp, "start get extra bind result"); let available_stun_servers = old_resp.collect_available_stun_server(); for server in available_stun_servers.iter() { - let ret = detector + let ret = udp_detector .get_extra_bind_result(0, *server) .await .with_context(|| "get extra bind result failed"); @@ -835,8 +1209,8 @@ impl StunInfoCollector { } let mut sleep_sec = 10; - if let Ok(resp) = &ret { - udp_test_time.store(Local::now()); + if let Ok(resp) = &udp_ret { + nat_test_time.store(Local::now()); *udp_nat_test_result.write().unwrap() = Some(resp.clone()); if nat_type != NatType::Unknown && (nat_type != NatType::Symmetric || resp.extra_bind_test.is_some()) @@ -852,6 +1226,40 @@ impl StunInfoCollector { } }); + let tcp_stun_servers = self.tcp_stun_servers.clone(); + let tcp_nat_test_result = self.tcp_nat_test_result.clone(); + let nat_test_time = self.nat_test_result_time.clone(); + let redetect_notify = self.redetect_notify.clone(); + self.tasks.lock().unwrap().spawn(async move { + loop { + let tcp_servers = tcp_stun_servers.read().unwrap().clone(); + let tcp_servers: Vec = tcp_servers + .iter() + .take(2) + .chain(tcp_servers.iter().skip(2).choose(&mut rand::thread_rng())) + .map(|x| x.to_string()) + .collect(); + + let tcp_detector = TcpNatTypeDetector::new(tcp_servers, 1); + let tcp_ret = tcp_detector.detect_nat_type(0).await; + tracing::debug!(?tcp_ret, "finish tcp nat type detect"); + + let mut sleep_sec = 10; + if let Ok(resp) = &tcp_ret { + nat_test_time.store(Local::now()); + *tcp_nat_test_result.write().unwrap() = Some(resp.clone()); + if resp.nat_type() != NatType::Unknown { + sleep_sec = 600; + } + } + + tokio::select! { + _ = redetect_notify.notified() => {} + _ = tokio::time::sleep(Duration::from_secs(sleep_sec)) => {} + } + } + }); + // for ipv6 let stun_servers = self.stun_servers_v6.clone(); let stored_ipv6 = self.public_ipv6.clone(); @@ -878,7 +1286,7 @@ impl StunInfoCollector { } pub fn update_stun_info(&self) { - self.redetect_notify.notify_one(); + self.redetect_notify.notify_waiters(); } } @@ -905,6 +1313,13 @@ impl StunInfoCollectorTrait for MockStunInfoCollector { } Ok(format!("127.0.0.1:{}", port).parse().unwrap()) } + + async fn get_tcp_port_mapping(&self, mut port: u16) -> Result { + if port == 0 { + port = 40144; + } + Ok(format!("127.0.0.1:{}", port).parse().unwrap()) + } } #[cfg(test)] @@ -962,9 +1377,9 @@ mod tests { let detector = UdpNatTypeDetector::new(stun_servers, 1); for _ in 0..5 { let ret = detector.detect_nat_type(0).await; - println!("{:#?}, {:?}", ret, ret.as_ref().unwrap().nat_type()); - if ret.is_ok() { - assert!(!ret.unwrap().stun_resps.is_empty()); + println!("{:#?}, {:?}", ret, ret.as_ref().map(|x| x.nat_type())); + if let Ok(resp) = ret { + assert!(!resp.stun_resps.is_empty()); return; } } @@ -974,6 +1389,120 @@ mod tests { ); } + #[tokio::test] + #[ignore] + async fn test_public_tcp_stun_server_fitauto_ru() { + let stun_servers = vec![ + "stun.fitauto.ru".to_string(), + "stun.hot-chilli.net".to_string(), + ]; + let detector = TcpNatTypeDetector::new(stun_servers, 3); + let ret = detector.detect_nat_type(0).await; + println!("{:#?}, {:?}", ret, ret.as_ref().map(|x| x.nat_type())); + if let Ok(resp) = ret { + assert!(!resp.stun_resps.is_empty()); + } + } + + #[tokio::test] + async fn test_internal_tcp_stun_server_reuse_same_local_port() { + use stun_codec::rfc5389::attributes::XorMappedAddress; + use tokio::net::TcpListener; + + async fn spawn_tcp_stun_server() -> SocketAddr { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let server_addr = listener.local_addr().unwrap(); + + tokio::spawn(async move { + let (mut stream, peer_addr) = listener.accept().await.unwrap(); + + let req = TcpStunClient::tcp_read_stun_message(&mut stream, Duration::from_secs(2)) + .await + .unwrap(); + let mut resp_msg = Message::::new( + MessageClass::SuccessResponse, + BINDING, + req.transaction_id(), + ); + resp_msg.add_attribute(Attribute::XorMappedAddress(XorMappedAddress::new( + peer_addr, + ))); + + let mut encoder = MessageEncoder::new(); + let rsp_buf = encoder.encode_into_bytes(resp_msg).unwrap(); + stream.write_all(rsp_buf.as_slice()).await.unwrap(); + }); + + server_addr + } + + let server1 = spawn_tcp_stun_server().await; + let server2 = spawn_tcp_stun_server().await; + + let stun_servers = vec![server1.to_string(), server2.to_string()]; + let detector = TcpNatTypeDetector::new(stun_servers, 1); + + let ret = detector.detect_nat_type(0).await.unwrap(); + assert!(ret.stun_resps.len() >= 2); + + let local_ports = ret + .stun_resps + .iter() + .map(|x| x.local_addr.port()) + .collect::>(); + assert_eq!(local_ports.len(), 1); + + let mapped_ports = ret + .stun_resps + .iter() + .map(|x| x.mapped_socket_addr.unwrap().port()) + .collect::>(); + assert_eq!(mapped_ports.len(), 1); + assert_eq!( + local_ports.into_iter().next(), + mapped_ports.into_iter().next() + ); + } + + #[tokio::test] + async fn test_stun_info_collector_tcp_port_mapping() { + use stun_codec::rfc5389::attributes::XorMappedAddress; + use tokio::net::TcpListener; + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let server_addr = listener.local_addr().unwrap(); + + tokio::spawn(async move { + for _ in 0..8 { + let Ok((mut stream, peer_addr)) = listener.accept().await else { + break; + }; + + let req = TcpStunClient::tcp_read_stun_message(&mut stream, Duration::from_secs(2)) + .await + .unwrap(); + let mut resp_msg = Message::::new( + MessageClass::SuccessResponse, + BINDING, + req.transaction_id(), + ); + resp_msg.add_attribute(Attribute::XorMappedAddress(XorMappedAddress::new( + peer_addr, + ))); + + let mut encoder = MessageEncoder::new(); + let rsp_buf = encoder.encode_into_bytes(resp_msg).unwrap(); + stream.write_all(rsp_buf.as_slice()).await.unwrap(); + } + }); + + let collector = StunInfoCollector::new(vec![], vec![server_addr.to_string()], vec![]); + collector.set_tcp_stun_servers(vec![server_addr.to_string()]); + let mapped = collector.get_tcp_port_mapping(0).await.unwrap(); + assert_eq!(mapped.ip(), IpAddr::V4(Ipv4Addr::LOCALHOST)); + assert!(mapped.port() > 0); + } + #[tokio::test] async fn test_v4_stun() { let mut udp_server = UdpTunnelListener::new("udp://0.0.0.0:55355".parse().unwrap()); diff --git a/easytier/src/connector/direct.rs b/easytier/src/connector/direct.rs index d91e4d57..3bcdce36 100644 --- a/easytier/src/connector/direct.rs +++ b/easytier/src/connector/direct.rs @@ -35,7 +35,6 @@ use crate::{ use_global_var, }; -use crate::proto::api::instance::PeerConnInfo; use anyhow::Context; use rand::Rng; use tokio::{net::UdpSocket, task::JoinSet, time::timeout}; @@ -51,7 +50,6 @@ static TESTING: AtomicBool = AtomicBool::new(false); #[async_trait::async_trait] pub trait PeerManagerForDirectConnector { async fn list_peers(&self) -> Vec; - async fn list_peer_conns(&self, peer_id: PeerId) -> Option>; fn get_peer_rpc_mgr(&self) -> Arc; } @@ -73,10 +71,6 @@ impl PeerManagerForDirectConnector for PeerManager { ret } - async fn list_peer_conns(&self, peer_id: PeerId) -> Option> { - self.get_peer_map().list_peer_conns(peer_id).await - } - fn get_peer_rpc_mgr(&self) -> Arc { self.get_peer_rpc_mgr() } diff --git a/easytier/src/connector/mod.rs b/easytier/src/connector/mod.rs index a3ea8644..d0653262 100644 --- a/easytier/src/connector/mod.rs +++ b/easytier/src/connector/mod.rs @@ -20,6 +20,7 @@ use crate::{ pub mod direct; pub mod manual; +pub mod tcp_hole_punch; pub mod udp_hole_punch; pub mod dns_connector; diff --git a/easytier/src/connector/tcp_hole_punch.rs b/easytier/src/connector/tcp_hole_punch.rs new file mode 100644 index 00000000..13d140ad --- /dev/null +++ b/easytier/src/connector/tcp_hole_punch.rs @@ -0,0 +1,730 @@ +use std::{ + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + sync::Arc, + time::Duration, +}; + +use anyhow::{Context, Error}; +use rand::Rng as _; +use tokio::task::JoinSet; + +use crate::{ + common::{join_joinset_background, stun::StunInfoCollectorTrait, PeerId}, + connector::udp_hole_punch::BackOff, + peers::{ + peer_manager::PeerManager, + peer_task::{PeerTaskLauncher, PeerTaskManager}, + }, + proto::{ + common::NatType, + peer_rpc::{ + TcpHolePunchRequest, TcpHolePunchResponse, TcpHolePunchRpc, + TcpHolePunchRpcClientFactory, TcpHolePunchRpcServer, + }, + rpc_types::{self, controller::BaseController}, + }, + tunnel::{ + common::setup_sokcet2, + tcp::{TcpTunnelConnector, TcpTunnelListener}, + TunnelConnector as _, TunnelListener as _, + }, +}; + +pub const BLACKLIST_TIMEOUT_SEC: u64 = 3600; + +fn handle_rpc_result( + ret: Result, + dst_peer_id: PeerId, + blacklist: &timedmap::TimedMap, +) -> Result { + match ret { + Ok(ret) => Ok(ret), + Err(e) => { + if matches!(e, rpc_types::error::Error::InvalidServiceKey(_, _)) { + blacklist.insert(dst_peer_id, (), Duration::from_secs(BLACKLIST_TIMEOUT_SEC)); + } + Err(e) + } + } +} + +fn is_symmetric_tcp_nat(nat_type: NatType) -> bool { + matches!( + nat_type, + NatType::Symmetric | NatType::SymmetricEasyInc | NatType::SymmetricEasyDec + ) +} + +fn bind_addr_for_port(port: u16, is_v6: bool) -> SocketAddr { + if is_v6 { + SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port) + } else { + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port) + } +} + +async fn select_local_port(peer_mgr: &Arc, is_v6: bool) -> Result { + let bind_addr = bind_addr_for_port(0, is_v6); + tracing::trace!(?bind_addr, is_v6, "tcp hole punch select local port"); + let _g = peer_mgr.get_global_ctx().net_ns.guard(); + let listener = tokio::net::TcpListener::bind(bind_addr).await?; + let port = listener.local_addr()?.port(); + tracing::debug!(?bind_addr, port, "tcp hole punch selected local port"); + Ok(port) +} + +async fn send_syn_from_port( + peer_mgr: &Arc, + local_port: u16, + dst: SocketAddr, +) -> Result<(), Error> { + let bind_addr = bind_addr_for_port(local_port, dst.is_ipv6()); + tracing::debug!(?bind_addr, ?dst, "tcp hole punch send syn"); + let _g = peer_mgr.get_global_ctx().net_ns.guard(); + + let socket2_socket = socket2::Socket::new( + socket2::Domain::for_address(dst), + socket2::Type::STREAM, + Some(socket2::Protocol::TCP), + )?; + setup_sokcet2(&socket2_socket, &bind_addr)?; + let socket = tokio::net::TcpSocket::from_std_stream(socket2_socket.into()); + match tokio::time::timeout(Duration::from_millis(600), socket.connect(dst)).await { + Ok(Ok(_stream)) => { + tracing::trace!(?bind_addr, ?dst, "tcp hole punch syn connect succeeded"); + } + Ok(Err(e)) => { + tracing::trace!(?bind_addr, ?dst, ?e, "tcp hole punch syn connect failed"); + } + Err(e) => { + tracing::trace!(?bind_addr, ?dst, ?e, "tcp hole punch syn connect timeout"); + } + } + Ok(()) +} + +// tcp support simultaneous connect, so initiator and server can both use connect. +async fn try_connect_to_remote( + peer_mgr: Arc, + a_mapped_addr: SocketAddr, + local_port: u16, + is_client: bool, + max_attempts: u32, +) -> Result<(), Error> { + tracing::info!( + ?a_mapped_addr, + local_port, + "tcp hole punch server start connect loop" + ); + + let mut connector = + TcpTunnelConnector::new(format!("tcp://{}", a_mapped_addr).parse().unwrap()); + connector.set_bind_addrs(vec![bind_addr_for_port( + local_port, + a_mapped_addr.is_ipv6(), + )]); + + let start = tokio::time::Instant::now(); + let mut attempts: u32 = 0; + while start.elapsed() < Duration::from_secs(10) && attempts < max_attempts { + attempts = attempts.wrapping_add(1); + let _g = peer_mgr.get_global_ctx().net_ns.guard(); + if let Ok(Ok(tunnel)) = + tokio::time::timeout(Duration::from_secs(3), connector.connect()).await + { + let add_tunnel_ret = if is_client { + peer_mgr.add_client_tunnel(tunnel, false).await.map(|_| ()) + } else { + peer_mgr.add_tunnel_as_server(tunnel, false).await + }; + if let Err(e) = add_tunnel_ret { + tracing::error!( + ?a_mapped_addr, + local_port, + attempts, + ?e, + "tcp hole punch server connected and added client tunnel failed" + ); + continue; + } else { + tracing::info!( + ?a_mapped_addr, + local_port, + attempts, + is_client, + "tcp hole punch server connected and added tunnel" + ); + return Ok(()); + } + } + tracing::trace!( + ?a_mapped_addr, + local_port, + attempts, + "tcp hole punch server connect attempt failed" + ); + let sleep_ms = rand::thread_rng().gen_range(10..100); + tokio::time::sleep(Duration::from_millis(sleep_ms)).await; + } + + tracing::warn!( + ?a_mapped_addr, + local_port, + attempts, + "tcp hole punch server connect loop timeout" + ); + + Err(anyhow::anyhow!( + "tcp hole punch server connect loop timeout" + )) +} + +struct TcpHolePunchServer { + peer_mgr: Arc, + tasks: Arc>>, +} + +impl TcpHolePunchServer { + fn new(peer_mgr: Arc) -> Arc { + let tasks = Arc::new(std::sync::Mutex::new(JoinSet::new())); + join_joinset_background(tasks.clone(), "tcp hole punch server".to_string()); + Arc::new(Self { peer_mgr, tasks }) + } +} + +#[async_trait::async_trait] +impl TcpHolePunchRpc for TcpHolePunchServer { + type Controller = BaseController; + + #[tracing::instrument(skip(self), fields(a_mapped_addr = ?input.connector_mapped_addr), err)] + async fn exchange_mapped_addr( + &self, + _ctrl: Self::Controller, + input: TcpHolePunchRequest, + ) -> rpc_types::error::Result { + let my_tcp_nat_type = NatType::try_from( + self.peer_mgr + .get_global_ctx() + .get_stun_info_collector() + .get_stun_info() + .tcp_nat_type, + ) + .unwrap_or(NatType::Unknown); + tracing::debug!(?my_tcp_nat_type, "tcp hole punch rpc received"); + if matches!(my_tcp_nat_type, NatType::Unknown) { + tracing::warn!(?my_tcp_nat_type, "tcp hole punch rpc rejected (unknown)"); + return Err(anyhow::anyhow!("tcp nat type unknown not supported").into()); + } + + let a_mapped_addr = input + .connector_mapped_addr + .ok_or(anyhow::anyhow!("connector_mapped_addr is required"))?; + let a_mapped_addr: SocketAddr = a_mapped_addr.into(); + let a_ip = a_mapped_addr.ip(); + if a_ip.is_unspecified() || a_ip.is_multicast() { + tracing::warn!(?a_mapped_addr, "tcp hole punch rpc invalid connector addr"); + return Err(anyhow::anyhow!("connector_mapped_addr is malformed").into()); + } + + let is_v6 = a_mapped_addr.is_ipv6(); + let local_port = select_local_port(&self.peer_mgr, is_v6).await?; + let mapped_addr = self + .peer_mgr + .get_global_ctx() + .get_stun_info_collector() + .get_tcp_port_mapping(local_port) + .await + .with_context(|| "failed to get tcp port mapping")?; + + tracing::info!( + ?a_mapped_addr, + local_port, + ?mapped_addr, + "tcp hole punch rpc responding with listener mapped addr and start connecting" + ); + + let peer_mgr = self.peer_mgr.clone(); + self.tasks.lock().unwrap().spawn(async move { + let _ = try_connect_to_remote(peer_mgr, a_mapped_addr, local_port, true, 5).await; + }); + + Ok(TcpHolePunchResponse { + listener_mapped_addr: Some(mapped_addr.into()), + }) + } +} + +struct TcpHolePunchConnectorData { + peer_mgr: Arc, + blacklist: Arc>, +} + +impl TcpHolePunchConnectorData { + fn new(peer_mgr: Arc) -> Arc { + Arc::new(Self { + peer_mgr, + blacklist: Arc::new(timedmap::TimedMap::new()), + }) + } + + async fn punch_as_initiator(self: Arc, dst_peer_id: PeerId) -> Result<(), Error> { + let mut backoff = BackOff::new(vec![1000, 1000, 4000, 8000]); + + loop { + backoff.sleep_for_next_backoff().await; + if self.do_punch_as_initiator(dst_peer_id).await.is_ok() { + break; + } + + if self.blacklist.contains(&dst_peer_id) { + tracing::warn!( + dst_peer_id, + "tcp hole punch initiator skipped (blacklisted)" + ); + break; + } + } + + Ok(()) + } + + #[tracing::instrument(skip(self), fields(dst_peer_id), err)] + async fn do_punch_as_initiator(&self, dst_peer_id: PeerId) -> Result<(), Error> { + let global_ctx = self.peer_mgr.get_global_ctx(); + let my_tcp_nat_type = NatType::try_from( + global_ctx + .get_stun_info_collector() + .get_stun_info() + .tcp_nat_type, + ) + .unwrap_or(NatType::Unknown); + tracing::debug!(?my_tcp_nat_type, "tcp hole punch initiator start"); + if is_symmetric_tcp_nat(my_tcp_nat_type) || my_tcp_nat_type == NatType::Unknown { + tracing::debug!("tcp hole punch initiator skipped (symmetric)"); + return Ok(()); + } + + let local_port = select_local_port(&self.peer_mgr, false).await?; + let mapped_addr = global_ctx + .get_stun_info_collector() + .get_tcp_port_mapping(local_port) + .await + .with_context(|| "failed to get tcp port mapping")?; + + tracing::info!( + dst_peer_id, + local_port, + ?mapped_addr, + "tcp hole punch initiator got mapped addr, start rpc exchange" + ); + + let rpc_stub = self + .peer_mgr + .get_peer_rpc_mgr() + .rpc_client() + .scoped_client::>( + self.peer_mgr.my_peer_id(), + dst_peer_id, + global_ctx.get_network_name(), + ); + + let resp = rpc_stub + .exchange_mapped_addr( + BaseController { + timeout_ms: 6000, + ..Default::default() + }, + TcpHolePunchRequest { + connector_mapped_addr: Some(mapped_addr.into()), + }, + ) + .await; + let resp = handle_rpc_result(resp, dst_peer_id, &self.blacklist)?; + let remote_mapped_addr = resp + .listener_mapped_addr + .ok_or(anyhow::anyhow!("listener_mapped_addr is required"))?; + let remote_mapped_addr: SocketAddr = remote_mapped_addr.into(); + tracing::info!( + dst_peer_id, + ?remote_mapped_addr, + "tcp hole punch initiator rpc returned" + ); + + if let Ok(()) = try_connect_to_remote( + self.peer_mgr.clone(), + remote_mapped_addr, + local_port, + false, + 1, + ) + .await + { + tracing::info!( + dst_peer_id, + local_port, + ?remote_mapped_addr, + "tcp hole punch initiator connected to remote mapped addr with simultaneous connection" + ); + return Ok(()); + } + + tracing::debug!( + dst_peer_id, + local_port, + ?remote_mapped_addr, + "tcp hole punch initiator sent syn to remote mapped addr" + ); + + let mut listener = + TcpTunnelListener::new(format!("tcp://0.0.0.0:{}", local_port).parse().unwrap()); + { + let _g = self.peer_mgr.get_global_ctx().net_ns.guard(); + listener.listen().await?; + } + tracing::info!( + dst_peer_id, + local_port, + url = %listener.local_url(), + "tcp hole punch initiator listening" + ); + + tokio::time::timeout( + Duration::from_secs(10), + self.accept_loop(&mut listener, dst_peer_id), + ) + .await??; + + tracing::info!( + dst_peer_id, + "tcp hole punch initiator accepted and added server tunnel" + ); + + Ok(()) + } + + async fn accept_loop( + &self, + listener: &mut TcpTunnelListener, + dst_peer_id: PeerId, + ) -> Result<(), Error> { + loop { + match listener.accept().await { + Ok(tunnel) => { + if let Err(e) = self.peer_mgr.add_tunnel_as_server(tunnel, false).await { + tracing::error!("tcp hole punch add tunnel error: {}", e); + continue; + } + + tracing::info!( + dst_peer_id, + "tcp hole punch initiator accepted and added server tunnel" + ); + } + Err(e) => { + tracing::error!("tcp hole punch accept error: {}", e); + } + } + } + } +} + +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +struct TcpPunchTaskInfo { + dst_peer_id: PeerId, +} + +#[derive(Clone)] +struct TcpHolePunchPeerTaskLauncher {} + +#[async_trait::async_trait] +impl PeerTaskLauncher for TcpHolePunchPeerTaskLauncher { + type Data = Arc; + type CollectPeerItem = TcpPunchTaskInfo; + type TaskRet = (); + + fn new_data(&self, peer_mgr: Arc) -> Self::Data { + TcpHolePunchConnectorData::new(peer_mgr) + } + + #[tracing::instrument(skip(self, data))] + async fn collect_peers_need_task(&self, data: &Self::Data) -> Vec { + let global_ctx = data.peer_mgr.get_global_ctx(); + let my_tcp_nat_type = NatType::try_from( + global_ctx + .get_stun_info_collector() + .get_stun_info() + .tcp_nat_type, + ) + .unwrap_or(NatType::Unknown); + if is_symmetric_tcp_nat(my_tcp_nat_type) || my_tcp_nat_type == NatType::Unknown { + tracing::trace!( + ?my_tcp_nat_type, + "tcp hole punch task collect skipped (symmetric)" + ); + return vec![]; + } + + let my_peer_id = data.peer_mgr.my_peer_id(); + + data.blacklist.cleanup(); + + let mut peers_to_connect = Vec::new(); + for route in data.peer_mgr.list_routes().await.iter() { + if route + .feature_flag + .map(|x| x.is_public_server) + .unwrap_or(false) + { + continue; + } + + let peer_id: PeerId = route.peer_id; + if peer_id == my_peer_id { + tracing::trace!(peer_id, "tcp hole punch task collect skip self"); + continue; + } + + if data.blacklist.contains(&peer_id) { + tracing::debug!(peer_id, "tcp hole punch task collect skip blacklisted"); + continue; + } + + if data.peer_mgr.get_peer_map().has_peer(peer_id) { + tracing::trace!(peer_id, "tcp hole punch task collect skip already has peer"); + continue; + } + + let peer_tcp_nat_type = route + .stun_info + .as_ref() + .map(|x| x.tcp_nat_type) + .unwrap_or(0); + let peer_tcp_nat_type = + NatType::try_from(peer_tcp_nat_type).unwrap_or(NatType::Unknown); + if matches!(peer_tcp_nat_type, NatType::Unknown) { + tracing::debug!( + peer_id, + ?peer_tcp_nat_type, + "tcp hole punch task collect skip peer unknown" + ); + continue; + } + + tracing::info!( + peer_id, + my_peer_id, + ?my_tcp_nat_type, + ?peer_tcp_nat_type, + "tcp hole punch task collect add peer" + ); + peers_to_connect.push(TcpPunchTaskInfo { + dst_peer_id: peer_id, + }); + } + + peers_to_connect + } + + async fn launch_task( + &self, + data: &Self::Data, + item: Self::CollectPeerItem, + ) -> tokio::task::JoinHandle> { + let data = data.clone(); + tokio::spawn(async move { data.punch_as_initiator(item.dst_peer_id).await.map(|_| ()) }) + } + + async fn all_task_done(&self, _data: &Self::Data) {} + + fn loop_interval_ms(&self) -> u64 { + 5000 + } +} + +pub struct TcpHolePunchConnector { + server: Arc, + client: PeerTaskManager, + peer_mgr: Arc, +} + +impl TcpHolePunchConnector { + pub fn new(peer_mgr: Arc) -> Self { + Self { + server: TcpHolePunchServer::new(peer_mgr.clone()), + client: PeerTaskManager::new(TcpHolePunchPeerTaskLauncher {}, peer_mgr.clone()), + peer_mgr, + } + } + + pub async fn run_as_client(&mut self) -> Result<(), Error> { + tracing::info!("tcp hole punch client start"); + self.client.start(); + Ok(()) + } + + pub async fn run_as_server(&mut self) -> Result<(), Error> { + tracing::info!("tcp hole punch server register rpc"); + self.peer_mgr + .get_peer_rpc_mgr() + .rpc_server() + .registry() + .register( + TcpHolePunchRpcServer::new(self.server.clone()), + &self.peer_mgr.get_global_ctx().get_network_name(), + ); + Ok(()) + } + + pub async fn run(&mut self) -> Result<(), Error> { + if self.peer_mgr.get_global_ctx().get_flags().disable_p2p { + tracing::debug!("tcp hole punch disabled by disable_p2p"); + return Ok(()); + } + + self.run_as_client().await?; + self.run_as_server().await?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::{net::SocketAddr, sync::Arc, time::Duration}; + + use crate::{ + common::{error::Error, stun::StunInfoCollectorTrait}, + connector::tcp_hole_punch::TcpHolePunchConnector, + peers::{ + peer_manager::PeerManager, + tests::{connect_peer_manager, create_mock_peer_manager, wait_route_appear}, + }, + proto::common::{NatType, StunInfo}, + tunnel::common::tests::wait_for_condition, + }; + + struct MockStunInfoCollector { + udp_nat_type: NatType, + tcp_nat_type: NatType, + } + + #[async_trait::async_trait] + impl StunInfoCollectorTrait for MockStunInfoCollector { + fn get_stun_info(&self) -> StunInfo { + StunInfo { + udp_nat_type: self.udp_nat_type as i32, + tcp_nat_type: self.tcp_nat_type as i32, + last_update_time: 0, + public_ip: vec!["127.0.0.1".to_string(), "::1".to_string()], + min_port: 100, + max_port: 200, + } + } + + async fn get_udp_port_mapping(&self, mut port: u16) -> Result { + if port == 0 { + port = 40144; + } + Ok(format!("127.0.0.1:{}", port).parse().unwrap()) + } + + async fn get_tcp_port_mapping(&self, mut port: u16) -> Result { + if port == 0 { + port = 40144; + } + Ok(format!("127.0.0.1:{}", port).parse().unwrap()) + } + } + + fn replace_stun_info_collector(peer_mgr: Arc, tcp_nat_type: NatType) { + let collector = Box::new(MockStunInfoCollector { + udp_nat_type: NatType::Unknown, + tcp_nat_type, + }); + peer_mgr + .get_global_ctx() + .replace_stun_info_collector(collector); + } + + #[tokio::test] + async fn tcp_hole_punch_connects() { + let p_a = create_mock_peer_manager().await; + let p_b = create_mock_peer_manager().await; + let p_c = create_mock_peer_manager().await; + + replace_stun_info_collector(p_a.clone(), NatType::PortRestricted); + replace_stun_info_collector(p_b.clone(), NatType::PortRestricted); + replace_stun_info_collector(p_c.clone(), NatType::PortRestricted); + + connect_peer_manager(p_a.clone(), p_b.clone()).await; + connect_peer_manager(p_b.clone(), p_c.clone()).await; + wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap(); + + let mut hole_punching_a = TcpHolePunchConnector::new(p_a.clone()); + let mut hole_punching_c = TcpHolePunchConnector::new(p_c.clone()); + hole_punching_a.run().await.unwrap(); + hole_punching_c.run().await.unwrap(); + + hole_punching_a.client.run_immediately().await; + hole_punching_c.client.run_immediately().await; + + wait_for_condition( + || { + let p_a = p_a.clone(); + let p_c = p_c.clone(); + async move { + let a_has = p_a + .get_peer_map() + .list_peer_conns(p_c.my_peer_id()) + .await + .is_some_and(|c| !c.is_empty()); + let c_has = p_c + .get_peer_map() + .list_peer_conns(p_a.my_peer_id()) + .await + .is_some_and(|c| !c.is_empty()); + a_has || c_has + } + }, + Duration::from_secs(15), + ) + .await; + } + + #[tokio::test] + async fn tcp_hole_punch_skip_symmetric_peer() { + let p_a = create_mock_peer_manager().await; + let p_b = create_mock_peer_manager().await; + let p_c = create_mock_peer_manager().await; + + replace_stun_info_collector(p_a.clone(), NatType::Symmetric); + replace_stun_info_collector(p_b.clone(), NatType::PortRestricted); + replace_stun_info_collector(p_c.clone(), NatType::Symmetric); + + connect_peer_manager(p_a.clone(), p_b.clone()).await; + connect_peer_manager(p_b.clone(), p_c.clone()).await; + wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap(); + + let mut hole_punching_a = TcpHolePunchConnector::new(p_a.clone()); + let mut hole_punching_c = TcpHolePunchConnector::new(p_c.clone()); + hole_punching_a.run().await.unwrap(); + hole_punching_c.run().await.unwrap(); + + hole_punching_a.client.run_immediately().await; + hole_punching_c.client.run_immediately().await; + + tokio::time::sleep(Duration::from_secs(2)).await; + + assert!(p_a + .get_peer_map() + .list_peer_conns(p_c.my_peer_id()) + .await + .map(|c| c.is_empty()) + .unwrap_or(true)); + assert!(p_c + .get_peer_map() + .list_peer_conns(p_a.my_peer_id()) + .await + .map(|c| c.is_empty()) + .unwrap_or(true)); + } +} diff --git a/easytier/src/connector/udp_hole_punch/mod.rs b/easytier/src/connector/udp_hole_punch/mod.rs index 5fcf96d5..2238b73b 100644 --- a/easytier/src/connector/udp_hole_punch/mod.rs +++ b/easytier/src/connector/udp_hole_punch/mod.rs @@ -14,7 +14,6 @@ use tokio::{sync::Mutex, task::JoinHandle}; use crate::{ common::{stun::StunInfoCollectorTrait, PeerId}, - connector::direct::PeerManagerForDirectConnector, peers::{ peer_manager::PeerManager, peer_task::{PeerTaskLauncher, PeerTaskManager}, @@ -461,8 +460,7 @@ impl PeerTaskLauncher for UdpHolePunchPeerTaskLauncher { continue; } - let conns = data.peer_mgr.list_peer_conns(peer_id).await; - if conns.is_some() && !conns.unwrap().is_empty() { + if data.peer_mgr.get_peer_map().has_peer(peer_id) { continue; } diff --git a/easytier/src/core.rs b/easytier/src/core.rs index 762538d9..573c8b42 100644 --- a/easytier/src/core.rs +++ b/easytier/src/core.rs @@ -421,6 +421,15 @@ struct NetworkOptions { )] disable_udp_hole_punching: Option, + #[arg( + long, + env = "ET_DISABLE_TCP_HOLE_PUNCHING", + help = t!("core_clap.disable_tcp_hole_punching").to_string(), + num_args = 0..=1, + default_missing_value = "true" + )] + disable_tcp_hole_punching: Option, + #[arg( long, env = "ET_DISABLE_SYM_HOLE_PUNCHING", @@ -925,6 +934,9 @@ impl NetworkOptions { } f.disable_p2p = self.disable_p2p.unwrap_or(f.disable_p2p); f.p2p_only = self.p2p_only.unwrap_or(f.p2p_only); + f.disable_tcp_hole_punching = self + .disable_tcp_hole_punching + .unwrap_or(f.disable_tcp_hole_punching); f.disable_udp_hole_punching = self .disable_udp_hole_punching .unwrap_or(f.disable_udp_hole_punching); diff --git a/easytier/src/easytier-cli.rs b/easytier/src/easytier-cli.rs index d6024dbf..1d8db9bf 100644 --- a/easytier/src/easytier-cli.rs +++ b/easytier/src/easytier-cli.rs @@ -1470,7 +1470,9 @@ async fn main() -> Result<(), Error> { let collector = StunInfoCollector::new_with_default_servers(); loop { let ret = collector.get_stun_info(); - if ret.udp_nat_type != NatType::Unknown as i32 { + if ret.udp_nat_type != NatType::Unknown as i32 + && ret.tcp_nat_type != NatType::Unknown as i32 + { if cli.output_format == OutputFormat::Json { match serde_json::to_string_pretty(&ret) { Ok(json) => println!("{}", json), diff --git a/easytier/src/instance/instance.rs b/easytier/src/instance/instance.rs index b475c7a4..d4c404bc 100644 --- a/easytier/src/instance/instance.rs +++ b/easytier/src/instance/instance.rs @@ -21,6 +21,7 @@ use crate::common::scoped_task::ScopedTask; use crate::common::PeerId; use crate::connector::direct::DirectConnectorManager; use crate::connector::manual::{ConnectorManagerRpcService, ManualConnectorManager}; +use crate::connector::tcp_hole_punch::TcpHolePunchConnector; use crate::connector::udp_hole_punch::UdpHolePunchConnector; use crate::gateway::icmp_proxy::IcmpProxy; use crate::gateway::kcp_proxy::{KcpProxyDst, KcpProxyDstRpcService, KcpProxySrc}; @@ -516,6 +517,7 @@ pub struct Instance { conn_manager: Arc, direct_conn_manager: Arc, udp_hole_puncher: Arc>, + tcp_hole_puncher: Arc>, ip_proxy: Option, @@ -571,6 +573,7 @@ impl Instance { direct_conn_manager.run(); let udp_hole_puncher = UdpHolePunchConnector::new(peer_manager.clone()); + let tcp_hole_puncher = TcpHolePunchConnector::new(peer_manager.clone()); let peer_center = Arc::new(PeerCenterInstance::new(peer_manager.clone())); @@ -594,6 +597,7 @@ impl Instance { conn_manager, direct_conn_manager: Arc::new(direct_conn_manager), udp_hole_puncher: Arc::new(Mutex::new(udp_hole_puncher)), + tcp_hole_puncher: Arc::new(Mutex::new(tcp_hole_puncher)), ip_proxy: None, kcp_proxy_src: None, @@ -949,6 +953,7 @@ impl Instance { self.run_ip_proxy().await?; self.udp_hole_puncher.lock().await.run().await?; + self.tcp_hole_puncher.lock().await.run().await?; self.peer_center.init().await; let route_calc = self.peer_center.get_cost_calculator(); diff --git a/easytier/src/instance/listeners.rs b/easytier/src/instance/listeners.rs index 27e7fc17..93d090ed 100644 --- a/easytier/src/instance/listeners.rs +++ b/easytier/src/instance/listeners.rs @@ -201,6 +201,7 @@ impl ListenerManage return; } tokio::time::sleep(std::time::Duration::from_secs(1)).await; + continue; } } loop { diff --git a/easytier/src/launcher.rs b/easytier/src/launcher.rs index cc42d1fb..199c7c2c 100644 --- a/easytier/src/launcher.rs +++ b/easytier/src/launcher.rs @@ -740,6 +740,10 @@ impl NetworkConfig { } } + if let Some(disable_tcp_hole_punching) = self.disable_tcp_hole_punching { + flags.disable_tcp_hole_punching = disable_tcp_hole_punching; + } + if let Some(disable_udp_hole_punching) = self.disable_udp_hole_punching { flags.disable_udp_hole_punching = disable_udp_hole_punching; } @@ -898,6 +902,7 @@ impl NetworkConfig { result.multi_thread = Some(flags.multi_thread); result.proxy_forward_by_system = Some(flags.proxy_forward_by_system); result.disable_encryption = Some(!flags.enable_encryption); + result.disable_tcp_hole_punching = Some(flags.disable_tcp_hole_punching); result.disable_udp_hole_punching = Some(flags.disable_udp_hole_punching); result.disable_sym_hole_punching = Some(flags.disable_sym_hole_punching); result.enable_magic_dns = Some(flags.accept_dns); @@ -1140,6 +1145,7 @@ mod tests { flags.multi_thread = rng.gen_bool(0.7); flags.proxy_forward_by_system = rng.gen_bool(0.3); flags.enable_encryption = rng.gen_bool(0.8); + flags.disable_tcp_hole_punching = rng.gen_bool(0.2); flags.disable_udp_hole_punching = rng.gen_bool(0.2); flags.accept_dns = rng.gen_bool(0.6); flags.mtu = rng.gen_range(1200..1500); diff --git a/easytier/src/peer_center/instance.rs b/easytier/src/peer_center/instance.rs index 90f88d0e..057b3d75 100644 --- a/easytier/src/peer_center/instance.rs +++ b/easytier/src/peer_center/instance.rs @@ -545,11 +545,12 @@ mod tests { println!("rpc service ready, {:#?}", rpc_service.global_peer_map); - if digest.is_none() { - digest = Some(rpc_service.global_peer_map_digest.load()); - } else { + if let Some(prev) = digest { let v = rpc_service.global_peer_map_digest.load(); - assert_eq!(digest.unwrap(), v); + assert_eq!(prev, v); + digest = Some(prev); + } else { + digest = Some(rpc_service.global_peer_map_digest.load()); } let mut route_cost = pc.get_cost_calculator(); diff --git a/easytier/src/peers/peer_ospf_route.rs b/easytier/src/peers/peer_ospf_route.rs index e1496a8c..335b1729 100644 --- a/easytier/src/peers/peer_ospf_route.rs +++ b/easytier/src/peers/peer_ospf_route.rs @@ -131,7 +131,8 @@ impl RoutePeerInfo { ipv4_addr: None, proxy_cidrs: Vec::new(), hostname: None, - udp_stun_info: 0, + udp_nat_type: 0, + tcp_nat_type: 0, // ensure this is updated when the peer_infos/conn_info/foreign_network lock is acquired. // else we may assign a older timestamp than iterate time. last_update: None, @@ -160,6 +161,7 @@ impl RoutePeerInfo { peer_route_id: u64, global_ctx: &ArcGlobalCtx, ) -> Self { + let stun_info = global_ctx.get_stun_info_collector().get_stun_info(); Self { peer_id: my_peer_id, inst_id: Some(global_ctx.get_id().into()), @@ -174,10 +176,8 @@ impl RoutePeerInfo { .map(|x| x.to_string()) .collect(), hostname: Some(global_ctx.get_hostname()), - udp_stun_info: global_ctx - .get_stun_info_collector() - .get_stun_info() - .udp_nat_type, + udp_nat_type: stun_info.udp_nat_type, + tcp_nat_type: stun_info.tcp_nat_type, // these two fields should not participate in comparison. last_update: None, @@ -251,9 +251,12 @@ impl From for crate::proto::api::instance::Route { hostname: val.hostname.unwrap_or_default(), stun_info: { let mut stun_info = StunInfo::default(); - if let Ok(udp_nat_type) = NatType::try_from(val.udp_stun_info) { + if let Ok(udp_nat_type) = NatType::try_from(val.udp_nat_type) { stun_info.set_udp_nat_type(udp_nat_type); } + if let Ok(tcp_nat_type) = NatType::try_from(val.tcp_nat_type) { + stun_info.set_tcp_nat_type(tcp_nat_type); + } Some(stun_info) }, inst_id: val.inst_id.map(|x| x.to_string()).unwrap_or_default(), @@ -869,10 +872,10 @@ impl RouteTable { self.get_next_hop(peer_id).is_some() } - fn get_nat_type(&self, peer_id: PeerId) -> Option { + fn get_udp_nat_type(&self, peer_id: PeerId) -> Option { self.peer_infos .get(&peer_id) - .map(|x| NatType::try_from(x.udp_stun_info).unwrap_or_default()) + .map(|x| NatType::try_from(x.udp_nat_type).unwrap_or_default()) } // return graph and start node index (node of my peer id). @@ -2516,7 +2519,7 @@ impl RouteSessionManager { let mut new_initiator_dst = None; // if any peer has NoPAT or OpenInternet stun type, we should use it. for peer_id in initiator_candidates.iter() { - let Some(nat_type) = service_impl.route_table.get_nat_type(*peer_id) else { + let Some(nat_type) = service_impl.route_table.get_udp_nat_type(*peer_id) else { continue; }; if nat_type == NatType::NoPat || nat_type == NatType::OpenInternet { diff --git a/easytier/src/proto/api_manage.proto b/easytier/src/proto/api_manage.proto index 8e1d41a9..11034309 100644 --- a/easytier/src/proto/api_manage.proto +++ b/easytier/src/proto/api_manage.proto @@ -80,6 +80,7 @@ message NetworkConfig { optional bool p2p_only = 51; optional common.CompressionAlgoPb data_compress_algo = 52; optional string encryption_algorithm = 53; + optional bool disable_tcp_hole_punching = 54; } message PortForwardConfig { diff --git a/easytier/src/proto/common.proto b/easytier/src/proto/common.proto index 1710f75f..96e89d0b 100644 --- a/easytier/src/proto/common.proto +++ b/easytier/src/proto/common.proto @@ -62,6 +62,8 @@ message FlagsInConfig { string tld_dns_zone = 31; bool p2p_only = 32; + + bool disable_tcp_hole_punching = 34; } message RpcDescriptor { diff --git a/easytier/src/proto/peer_rpc.proto b/easytier/src/proto/peer_rpc.proto index 2aef0d7e..3376a63f 100644 --- a/easytier/src/proto/peer_rpc.proto +++ b/easytier/src/proto/peer_rpc.proto @@ -13,7 +13,7 @@ message RoutePeerInfo { optional common.Ipv4Addr ipv4_addr = 4; repeated string proxy_cidrs = 5; optional string hostname = 6; - common.NatType udp_stun_info = 7; + common.NatType udp_nat_type = 7; google.protobuf.Timestamp last_update = 8; uint32 version = 9; @@ -27,6 +27,8 @@ message RoutePeerInfo { optional common.Ipv6Inet ipv6_addr = 15; repeated PeerGroupInfo groups = 16; + + common.NatType tcp_nat_type = 17; } message PeerIdVersion { @@ -207,6 +209,14 @@ service UdpHolePunchRpc { returns (SendPunchPacketBothEasySymResponse); } +message TcpHolePunchRequest { common.SocketAddr connector_mapped_addr = 1; } + +message TcpHolePunchResponse { common.SocketAddr listener_mapped_addr = 1; } + +service TcpHolePunchRpc { + rpc ExchangeMappedAddr(TcpHolePunchRequest) returns (TcpHolePunchResponse); +} + message DirectConnectedPeerInfo { int32 latency_ms = 1; } message PeerInfoForGlobalMap { diff --git a/easytier/src/tunnel/fake_tcp/mod.rs b/easytier/src/tunnel/fake_tcp/mod.rs index dededaf9..7dc28fd9 100644 --- a/easytier/src/tunnel/fake_tcp/mod.rs +++ b/easytier/src/tunnel/fake_tcp/mod.rs @@ -134,25 +134,25 @@ impl FakeTcpTunnelListener { 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); + let ret = match self.stack_map.entry(interface_name.to_string()) { + dashmap::Entry::Occupied(entry) => entry.get().clone(), + dashmap::Entry::Vacant(entry) => { + 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( + let stack = Arc::new(Mutex::new(stack::Stack::new( tun, local_ip.unwrap_or(Ipv4Addr::UNSPECIFIED), local_ip6, accept_result.mac, - ))) - }) - .clone(); + ))); + entry.insert(stack.clone()); + stack + } + }; Ok(ret) } @@ -314,7 +314,7 @@ impl crate::tunnel::TunnelConnector for FakeTcpTunnelConnector { IpAddr::V6(ip) => (None, Some(ip)), }; - let tun = create_tun(&interface_name, Some(remote_addr), local_addr); + 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(); diff --git a/easytier/src/tunnel/fake_tcp/netfilter/mod.rs b/easytier/src/tunnel/fake_tcp/netfilter/mod.rs index 5ed33d7a..960920d8 100644 --- a/easytier/src/tunnel/fake_tcp/netfilter/mod.rs +++ b/easytier/src/tunnel/fake_tcp/netfilter/mod.rs @@ -1,6 +1,6 @@ pub mod pnet; -use std::{net::SocketAddr, sync::Arc}; +use std::{io, net::SocketAddr, sync::Arc}; cfg_if::cfg_if! { if #[cfg(target_os = "linux")] { @@ -10,19 +10,19 @@ cfg_if::cfg_if! { interface_name: &str, src_addr: Option, dst_addr: SocketAddr, - ) -> Arc { + ) -> io::Result> { match linux_bpf::LinuxBpfTun::new(interface_name, src_addr, dst_addr) { - Ok(tun) => Arc::new(tun), + Ok(tun) => Ok(Arc::new(tun)), Err(e) => { tracing::warn!( ?e, interface_name, "LinuxBpfTun init failed, falling back to PnetTun" ); - Arc::new(pnet::PnetTun::new( + Ok(Arc::new(pnet::PnetTun::new( interface_name, pnet::create_packet_filter(src_addr, dst_addr), - )) + )?)) } } } @@ -33,19 +33,19 @@ cfg_if::cfg_if! { interface_name: &str, src_addr: Option, dst_addr: SocketAddr, - ) -> Arc { + ) -> io::Result> { match macos_bpf::MacosBpfTun::new(interface_name, src_addr, dst_addr) { - Ok(tun) => Arc::new(tun), + Ok(tun) => Ok(Arc::new(tun)), Err(e) => { tracing::warn!( ?e, interface_name, "MacosBpfTun init failed, falling back to PnetTun" ); - Arc::new(pnet::PnetTun::new( + Ok(Arc::new(pnet::PnetTun::new( interface_name, pnet::create_packet_filter(src_addr, dst_addr), - )) + )?)) } } } @@ -56,19 +56,19 @@ cfg_if::cfg_if! { _interface_name: &str, _src_addr: Option, local_addr: SocketAddr, - ) -> Arc { + ) -> io::Result> { match windivert::WinDivertTun::new(local_addr) { - Ok(tun) => Arc::new(tun), + Ok(tun) => Ok(Arc::new(tun)), Err(e) => { tracing::warn!( ?e, ?local_addr, "WinDivertTun init failed, falling back to PnetTun" ); - Arc::new(pnet::PnetTun::new( + Ok(Arc::new(pnet::PnetTun::new( local_addr.to_string().as_str(), pnet::create_packet_filter(None, local_addr), - )) + )?)) } } } @@ -77,11 +77,11 @@ cfg_if::cfg_if! { interface_name: &str, src_addr: Option, dst_addr: SocketAddr, - ) -> Arc { - Arc::new(pnet::PnetTun::new( + ) -> io::Result> { + Ok(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 index d7246a0c..c5ef0759 100644 --- a/easytier/src/tunnel/fake_tcp/netfilter/pnet.rs +++ b/easytier/src/tunnel/fake_tcp/netfilter/pnet.rs @@ -1,4 +1,5 @@ use std::{ + io, net::{IpAddr, SocketAddr}, sync::{ atomic::{AtomicU32, Ordering}, @@ -145,14 +146,11 @@ struct InterfaceWorker { } impl InterfaceWorker { - fn new(interface: NetworkInterface) -> Arc { + fn new(interface: NetworkInterface) -> io::Result> { 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 - ), + Ok(_) => return Err(io::Error::other("Unhandled channel type")), + Err(e) => return Err(io::Error::other(e)), }; let subscribers = Arc::new(DashMap::::new()); @@ -187,10 +185,10 @@ impl InterfaceWorker { } }); - Arc::new(Self { + Ok(Arc::new(Self { tx: Mutex::new(tx), subscribers, - }) + })) } fn subscribe(&self, filter: PacketFilter, sender: tokio::sync::mpsc::Sender>) -> u32 { @@ -207,13 +205,13 @@ impl InterfaceWorker { static INTERFACE_MANAGERS: Lazy>> = Lazy::new(DashMap::new); -fn get_or_create_worker(interface_name: &str) -> Arc { +fn get_or_create_worker(interface_name: &str) -> io::Result> { // Check if we have an active worker if let Some(worker) = INTERFACE_MANAGERS .get(interface_name) .and_then(|w| w.upgrade()) { - return worker; + return Ok(worker); } // Need to create new worker. @@ -229,9 +227,9 @@ fn get_or_create_worker(interface_name: &str) -> Arc { .find(|iface| iface.name == interface_name) .expect("Network interface not found"); - let worker = InterfaceWorker::new(interface); + let worker = InterfaceWorker::new(interface)?; INTERFACE_MANAGERS.insert(interface_name.to_string(), Arc::downgrade(&worker)); - worker + Ok(worker) } pub struct PnetTun { @@ -241,17 +239,17 @@ pub struct PnetTun { } impl PnetTun { - pub fn new(interface_name: &str, filter: PacketFilter) -> Self { + pub fn new(interface_name: &str, filter: PacketFilter) -> io::Result { tracing::debug!(interface_name, "Creating new PnetTun"); - let worker = get_or_create_worker(interface_name); + let worker = get_or_create_worker(interface_name)?; let (tx, rx) = tokio::sync::mpsc::channel(1024); let id = worker.subscribe(filter, tx); - Self { + Ok(Self { worker, subscription_id: id, recv_queue: Mutex::new(rx), - } + }) } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b493165..68459558 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -127,8 +127,8 @@ importers: specifier: ^5.4.8 version: 5.4.19(@types/node@22.18.1) vite-plugin-vue-devtools: - specifier: ^7.4.6 - version: 7.7.7(rollup@4.50.1)(vite@5.4.19(@types/node@22.18.1))(vue@3.5.21(typescript@5.6.3)) + specifier: ^8.0.5 + version: 8.0.5(vite@5.4.19(@types/node@22.18.1))(vue@3.5.21(typescript@5.6.3)) vite-plugin-vue-layouts: specifier: ^0.11.0 version: 0.11.0(vite@5.4.19(@types/node@22.18.1))(vue-router@4.5.1(vue@3.5.21(typescript@5.6.3)))(vue@3.5.21(typescript@5.6.3)) @@ -949,8 +949,8 @@ packages: resolution: {integrity: sha512-BcmHpb5bQyeVNrptC3UhzpBZB/YHHDoEREOUERrmF2BRxsyOEuRrq+Z96C/D4+2KJb8kuHiouzAei7BXlG0YYw==} engines: {node: '>= 16'} - '@intlify/shared@11.1.12': - resolution: {integrity: sha512-Om86EjuQtA69hdNj3GQec9ZC0L0vPSAnXzB3gP/gyJ7+mA7t06d9aOAiqMZ+xEOsumGP4eEBlfl8zF2LOTzf2A==} + '@intlify/shared@11.2.7': + resolution: {integrity: sha512-uvlkvc/0uQ4FDlHQZccpUnmcOwNcaI3i+69ck2YJ+GqM35AoVbuS63b+YfirV4G0SZh64Ij2UMcFRMmB4nr95w==} engines: {node: '>= 16'} '@intlify/shared@12.0.0-alpha.3': @@ -1089,25 +1089,21 @@ packages: resolution: {integrity: sha512-mMB1AvqzTH25rbUo1eRfvFzNqBopX6aRlDmO1fIVVzIWi6YJNKckxbkGaatez4hH/n86IR6aEdZFM3qBUjn3Tg==} cpu: [arm64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@4.2.0': resolution: {integrity: sha512-9oPBU8Yb35z15/14LzALn/8rRwwrtfe19l25N1MRZVSONGiOwfzWNqDNjWiDdyW+EUt/hlylmFOItZmreL6iIw==} cpu: [arm64] os: [linux] - libc: [musl] '@oxc-resolver/binding-linux-x64-gnu@4.2.0': resolution: {integrity: sha512-8wU4fwHb0b45i0qMBJ24UYBEtaLyvYWUOqVVCn0SpQZ1mhWWC8dvD6+zIVAKRVex/cKdgzi3imXoKGIDqVEu9w==} cpu: [x64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@4.2.0': resolution: {integrity: sha512-5CS2wlGxzESPJCj4NlNGr73QCku75VpGtkwNp8qJF4hLELKAzkoqIB0eBbcvNPg8m2rB7YeXb1u+puGUKXDhNQ==} cpu: [x64] os: [linux] - libc: [musl] '@oxc-resolver/binding-wasm32-wasi@4.2.0': resolution: {integrity: sha512-VOLpvmVAQZjvj/7Et/gYzW6yBqL9VKjLWOGaFiQ7cvTpY9R9d/1mrNKEuP3beDHF2si2fM5f2pl9bL+N4tvwiA==} @@ -1239,67 +1235,56 @@ packages: resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.50.1': resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.50.1': resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.50.1': resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.50.1': resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.50.1': resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.50.1': resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.50.1': resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.50.1': resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.50.1': resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.50.1': resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openharmony-arm64@4.50.1': resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==} @@ -1343,13 +1328,6 @@ packages: '@rushstack/ts-command-line@5.0.2': resolution: {integrity: sha512-+AkJDbu1GFMPIU8Sb7TLVXDv/Q7Mkvx+wAjEl8XiXVVq+p1FmWW6M3LYpJMmoHNckSofeMecgWg5lfMwNAAsEQ==} - '@sec-ant/readable-stream@0.4.1': - resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - - '@sindresorhus/merge-streams@4.0.0': - resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} - engines: {node: '>=18'} - '@stylistic/eslint-plugin@2.13.0': resolution: {integrity: sha512-RnO1SaiCFHn666wNz2QfZEFxvmiNRqhzaMXHXxXXKt+MEP7aajlPxUSMIQpKAaJfverpovEYqjBOXDq6dDcaOQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1386,35 +1364,30 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tauri-apps/cli-linux-arm64-musl@2.7.1': resolution: {integrity: sha512-/HXY0t4FHkpFzjeYS5c16mlA6z0kzn5uKLWptTLTdFSnYpr8FCnOP4Sdkvm2TDQPF2ERxXtNCd+WR/jQugbGnA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tauri-apps/cli-linux-riscv64-gnu@2.7.1': resolution: {integrity: sha512-GeW5lVI2GhhnaYckiDzstG2j2Jwlud5d2XefRGwlOK+C/bVGLT1le8MNPYK8wgRlpeK8fG1WnJJYD6Ke7YQ8bg==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - libc: [glibc] '@tauri-apps/cli-linux-x64-gnu@2.7.1': resolution: {integrity: sha512-DprxKQkPxIPYwUgg+cscpv2lcIUhn2nxEPlk0UeaiV9vATxCXyytxr1gLcj3xgjGyNPlM0MlJyYaPy1JmRg1cA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tauri-apps/cli-linux-x64-musl@2.7.1': resolution: {integrity: sha512-KLlq3kOK7OUyDR757c0zQjPULpGZpLhNB0lZmZpHXvoOUcqZoCXJHh4dT/mryWZJp5ilrem5l8o9ngrDo0X1AA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tauri-apps/cli-win32-arm64-msvc@2.7.1': resolution: {integrity: sha512-dH7KUjKkSypCeWPiainHyXoES3obS+JIZVoSwSZfKq2gWgs48FY3oT0hQNYrWveE+VR4VoR3b/F3CPGbgFvksA==} @@ -1600,49 +1573,41 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -1933,16 +1898,16 @@ packages: '@vue/devtools-api@6.6.4': resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} - '@vue/devtools-core@7.7.7': - resolution: {integrity: sha512-9z9TLbfC+AjAi1PQyWX+OErjIaJmdFlbDHcD+cAMYKY6Bh5VlsAtCeGyRMrXwIlMEQPukvnWt3gZBLwTAIMKzQ==} + '@vue/devtools-core@8.0.5': + resolution: {integrity: sha512-dpCw8nl0GDBuiL9SaY0mtDxoGIEmU38w+TQiYEPOLhW03VDC0lfNMYXS/qhl4I0YlysGp04NLY4UNn6xgD0VIQ==} peerDependencies: vue: ^3.0.0 - '@vue/devtools-kit@7.7.7': - resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==} + '@vue/devtools-kit@8.0.5': + resolution: {integrity: sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==} - '@vue/devtools-shared@7.7.7': - resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==} + '@vue/devtools-shared@8.0.5': + resolution: {integrity: sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==} '@vue/language-core@2.1.10': resolution: {integrity: sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==} @@ -2062,6 +2027,10 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -2113,6 +2082,9 @@ packages: birpc@2.5.0: resolution: {integrity: sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==} + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -2331,8 +2303,8 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - error-stack-parser-es@0.1.5: - resolution: {integrity: sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==} + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} @@ -2606,10 +2578,6 @@ packages: resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} - execa@9.6.0: - resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} - engines: {node: ^18.19.0 || >=20.5.0} - exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} @@ -2639,10 +2607,6 @@ packages: fault@2.0.1: resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} - figures@6.1.0: - resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} - engines: {node: '>=18'} - file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2731,10 +2695,6 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} - get-stream@9.0.1: - resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} - engines: {node: '>=18'} - get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} @@ -2812,10 +2772,6 @@ packages: resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} engines: {node: '>=14.18.0'} - human-signals@8.0.1: - resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} - engines: {node: '>=18.18.0'} - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2895,22 +2851,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - is-stream@4.0.1: - resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} - engines: {node: '>=18'} - - is-unicode-supported@2.1.0: - resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} - engines: {node: '>=18'} - is-what@4.1.16: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} engines: {node: '>=12.13'} @@ -3309,10 +3253,6 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - npm-run-path@6.0.0: - resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} - engines: {node: '>=18'} - nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -3324,6 +3264,9 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + onetime@6.0.0: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} @@ -3380,10 +3323,6 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} - parse-ms@4.0.0: - resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} - engines: {node: '>=18'} - parse-statements@1.0.11: resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} @@ -3415,8 +3354,8 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - perfect-debounce@1.0.0: - resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + perfect-debounce@2.0.0: + resolution: {integrity: sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3526,10 +3465,6 @@ packages: engines: {node: '>=14'} hasBin: true - pretty-ms@9.2.0: - resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==} - engines: {node: '>=18'} - primeicons@7.0.0: resolution: {integrity: sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==} @@ -3737,10 +3672,6 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} - strip-final-newline@4.0.0: - resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} - engines: {node: '>=18'} - strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -3891,10 +3822,6 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - unicorn-magic@0.3.0: - resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} - engines: {node: '>=18'} - unimport@3.14.6: resolution: {integrity: sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==} @@ -3953,6 +3880,10 @@ packages: webpack: optional: true + unplugin-utils@0.3.1: + resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} + engines: {node: '>=20.19.0'} + unplugin-vue-components@0.27.5: resolution: {integrity: sha512-m9j4goBeNwXyNN8oZHHxvIIYiG8FQ9UfmKWeNllpDvhU7btKNNELGPt+o3mckQKuPwrE7e0PvCsx+IWuDSD9Vg==} engines: {node: '>=14'} @@ -4023,6 +3954,11 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + vite-dev-rpc@1.1.0: + resolution: {integrity: sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 + vite-hot-client@2.1.0: resolution: {integrity: sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==} peerDependencies: @@ -4037,12 +3973,12 @@ packages: vite: optional: true - vite-plugin-inspect@0.8.9: - resolution: {integrity: sha512-22/8qn+LYonzibb1VeFZmISdVao5kC22jmEKm24vfFE8siEn47EpVcCLYMv6iKOYMJfjSvSJfueOwcFCkUnV3A==} + vite-plugin-inspect@11.3.3: + resolution: {integrity: sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==} engines: {node: '>=14'} peerDependencies: '@nuxt/kit': '*' - vite: ^3.1.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.1 + vite: ^6.0.0 || ^7.0.0-0 peerDependenciesMeta: '@nuxt/kit': optional: true @@ -4054,11 +3990,11 @@ packages: rollup: ^4.44.1 vite: ^5.4.11 || ^6.0.0 || ^7.0.0 - vite-plugin-vue-devtools@7.7.7: - resolution: {integrity: sha512-d0fIh3wRcgSlr4Vz7bAk4va1MkdqhQgj9ANE/rBhsAjOnRfTLs2ocjFMvSUOsv6SRRXU9G+VM7yMgqDb6yI4iQ==} + vite-plugin-vue-devtools@8.0.5: + resolution: {integrity: sha512-p619BlKFOqQXJ6uDWS1vUPQzuJOD6xJTfftj57JXBGoBD/yeQCowR7pnWcr/FEX4/HVkFbreI6w2uuGBmQOh6A==} engines: {node: '>=v14.21.3'} peerDependencies: - vite: ^3.1.0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0-0 vite-plugin-vue-inspector@5.3.2: resolution: {integrity: sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q==} @@ -4224,10 +4160,6 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yoctocolors@2.1.2: - resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} - engines: {node: '>=18'} - zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -4805,7 +4737,7 @@ snapshots: '@intlify/shared@10.0.8': {} - '@intlify/shared@11.1.12': {} + '@intlify/shared@11.2.7': {} '@intlify/shared@12.0.0-alpha.3': {} @@ -4815,8 +4747,8 @@ snapshots: dependencies: '@eslint-community/eslint-utils': 4.8.0(eslint@9.35.0(jiti@2.5.1)) '@intlify/bundle-utils': 9.0.0(vue-i18n@10.0.8(vue@3.5.21(typescript@5.6.3))) - '@intlify/shared': 11.1.12 - '@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@11.1.12)(@vue/compiler-dom@3.5.21)(vue-i18n@10.0.8(vue@3.5.21(typescript@5.6.3)))(vue@3.5.21(typescript@5.6.3)) + '@intlify/shared': 11.2.7 + '@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@11.2.7)(@vue/compiler-dom@3.5.21)(vue-i18n@10.0.8(vue@3.5.21(typescript@5.6.3)))(vue@3.5.21(typescript@5.6.3)) '@rollup/pluginutils': 5.3.0(rollup@4.50.1) '@typescript-eslint/scope-manager': 8.42.0 '@typescript-eslint/typescript-estree': 8.42.0(typescript@5.6.3) @@ -4838,11 +4770,11 @@ snapshots: - supports-color - typescript - '@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@11.1.12)(@vue/compiler-dom@3.5.21)(vue-i18n@10.0.8(vue@3.5.21(typescript@5.6.3)))(vue@3.5.21(typescript@5.6.3))': + '@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@11.2.7)(@vue/compiler-dom@3.5.21)(vue-i18n@10.0.8(vue@3.5.21(typescript@5.6.3)))(vue@3.5.21(typescript@5.6.3))': dependencies: '@babel/parser': 7.28.4 optionalDependencies: - '@intlify/shared': 11.1.12 + '@intlify/shared': 11.2.7 '@vue/compiler-dom': 3.5.21 vue: 3.5.21(typescript@5.6.3) vue-i18n: 10.0.8(vue@3.5.21(typescript@5.6.3)) @@ -5163,10 +5095,6 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@sec-ant/readable-stream@0.4.1': {} - - '@sindresorhus/merge-streams@4.0.0': {} - '@stylistic/eslint-plugin@2.13.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)': dependencies: '@typescript-eslint/utils': 8.42.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3) @@ -5846,10 +5774,10 @@ snapshots: '@vue/devtools-api@6.6.4': {} - '@vue/devtools-core@7.7.7(vite@5.4.19(@types/node@22.18.1))(vue@3.5.21(typescript@5.6.3))': + '@vue/devtools-core@8.0.5(vite@5.4.19(@types/node@22.18.1))(vue@3.5.21(typescript@5.6.3))': dependencies: - '@vue/devtools-kit': 7.7.7 - '@vue/devtools-shared': 7.7.7 + '@vue/devtools-kit': 8.0.5 + '@vue/devtools-shared': 8.0.5 mitt: 3.0.1 nanoid: 5.1.5 pathe: 2.0.3 @@ -5858,17 +5786,17 @@ snapshots: transitivePeerDependencies: - vite - '@vue/devtools-kit@7.7.7': + '@vue/devtools-kit@8.0.5': dependencies: - '@vue/devtools-shared': 7.7.7 - birpc: 2.5.0 + '@vue/devtools-shared': 8.0.5 + birpc: 2.9.0 hookable: 5.5.3 mitt: 3.0.1 - perfect-debounce: 1.0.0 + perfect-debounce: 2.0.0 speakingurl: 14.0.1 superjson: 2.2.2 - '@vue/devtools-shared@7.7.7': + '@vue/devtools-shared@8.0.5': dependencies: rfdc: 1.4.1 @@ -6018,6 +5946,8 @@ snapshots: ansi-styles@6.2.1: {} + ansis@4.2.0: {} + any-promise@1.3.0: {} anymatch@3.1.3: @@ -6071,6 +6001,8 @@ snapshots: birpc@2.5.0: {} + birpc@2.9.0: {} + boolbase@1.0.0: {} brace-expansion@1.1.12: @@ -6264,7 +6196,7 @@ snapshots: dependencies: is-arrayish: 0.2.1 - error-stack-parser-es@0.1.5: {} + error-stack-parser-es@1.0.5: {} es-define-property@1.0.1: {} @@ -6674,21 +6606,6 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 3.0.0 - execa@9.6.0: - dependencies: - '@sindresorhus/merge-streams': 4.0.0 - cross-spawn: 7.0.6 - figures: 6.1.0 - get-stream: 9.0.1 - human-signals: 8.0.1 - is-plain-obj: 4.1.0 - is-stream: 4.0.1 - npm-run-path: 6.0.0 - pretty-ms: 9.2.0 - signal-exit: 4.1.0 - strip-final-newline: 4.0.0 - yoctocolors: 2.1.2 - exsolve@1.0.7: {} extend-shallow@2.0.1: @@ -6719,10 +6636,6 @@ snapshots: dependencies: format: 0.2.2 - figures@6.1.0: - dependencies: - is-unicode-supported: 2.1.0 - file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -6808,11 +6721,6 @@ snapshots: get-stream@6.0.1: {} - get-stream@9.0.1: - dependencies: - '@sec-ant/readable-stream': 0.4.1 - is-stream: 4.0.1 - get-tsconfig@4.10.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -6879,8 +6787,6 @@ snapshots: human-signals@4.3.1: {} - human-signals@8.0.1: {} - ignore@5.3.2: {} ignore@7.0.5: {} @@ -6943,14 +6849,8 @@ snapshots: is-number@7.0.0: {} - is-plain-obj@4.1.0: {} - is-stream@3.0.0: {} - is-stream@4.0.1: {} - - is-unicode-supported@2.1.0: {} - is-what@4.1.16: {} is-wsl@3.1.0: @@ -7496,11 +7396,6 @@ snapshots: dependencies: path-key: 4.0.0 - npm-run-path@6.0.0: - dependencies: - path-key: 4.0.0 - unicorn-magic: 0.3.0 - nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -7509,6 +7404,8 @@ snapshots: object-hash@3.0.0: {} + ohash@2.0.11: {} + onetime@6.0.0: dependencies: mimic-fn: 4.0.0 @@ -7582,8 +7479,6 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - parse-ms@4.0.0: {} - parse-statements@1.0.11: {} path-browserify@1.0.1: {} @@ -7605,7 +7500,7 @@ snapshots: pathe@2.0.3: {} - perfect-debounce@1.0.0: {} + perfect-debounce@2.0.0: {} picocolors@1.1.1: {} @@ -7703,10 +7598,6 @@ snapshots: prettier@3.6.2: {} - pretty-ms@9.2.0: - dependencies: - parse-ms: 4.0.0 - primeicons@7.0.0: {} primevue@4.3.9(vue@3.5.21(typescript@5.6.3)): @@ -7912,8 +7803,6 @@ snapshots: strip-final-newline@3.0.0: {} - strip-final-newline@4.0.0: {} - strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -8073,8 +7962,6 @@ snapshots: undici-types@6.21.0: {} - unicorn-magic@0.3.0: {} - unimport@3.14.6(rollup@4.50.1): dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.50.1) @@ -8137,6 +8024,11 @@ snapshots: unplugin: 1.16.1 vite: 5.4.19(@types/node@22.18.1) + unplugin-utils@0.3.1: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.3 + unplugin-vue-components@0.27.5(@babel/parser@7.28.4)(rollup@4.50.1)(vue@3.5.21(typescript@5.6.3)): dependencies: '@antfu/utils': 0.7.10 @@ -8300,6 +8192,12 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + vite-dev-rpc@1.1.0(vite@5.4.19(@types/node@22.18.1)): + dependencies: + birpc: 2.5.0 + vite: 5.4.19(@types/node@22.18.1) + vite-hot-client: 2.1.0(vite@5.4.19(@types/node@22.18.1)) + vite-hot-client@2.1.0(vite@5.4.19(@types/node@22.18.1)): dependencies: vite: 5.4.19(@types/node@22.18.1) @@ -8323,20 +8221,19 @@ snapshots: - rollup - supports-color - vite-plugin-inspect@0.8.9(rollup@4.50.1)(vite@5.4.19(@types/node@22.18.1)): + vite-plugin-inspect@11.3.3(vite@5.4.19(@types/node@22.18.1)): dependencies: - '@antfu/utils': 0.7.10 - '@rollup/pluginutils': 5.3.0(rollup@4.50.1) + ansis: 4.2.0 debug: 4.4.1 - error-stack-parser-es: 0.1.5 - fs-extra: 11.3.1 + error-stack-parser-es: 1.0.5 + ohash: 2.0.11 open: 10.2.0 - perfect-debounce: 1.0.0 - picocolors: 1.1.1 + perfect-debounce: 2.0.0 sirv: 3.0.2 + unplugin-utils: 0.3.1 vite: 5.4.19(@types/node@22.18.1) + vite-dev-rpc: 1.1.0(vite@5.4.19(@types/node@22.18.1)) transitivePeerDependencies: - - rollup - supports-color vite-plugin-singlefile@2.3.0(rollup@4.50.1)(vite@5.4.19(@types/node@22.18.1)): @@ -8345,19 +8242,17 @@ snapshots: rollup: 4.50.1 vite: 5.4.19(@types/node@22.18.1) - vite-plugin-vue-devtools@7.7.7(rollup@4.50.1)(vite@5.4.19(@types/node@22.18.1))(vue@3.5.21(typescript@5.6.3)): + vite-plugin-vue-devtools@8.0.5(vite@5.4.19(@types/node@22.18.1))(vue@3.5.21(typescript@5.6.3)): dependencies: - '@vue/devtools-core': 7.7.7(vite@5.4.19(@types/node@22.18.1))(vue@3.5.21(typescript@5.6.3)) - '@vue/devtools-kit': 7.7.7 - '@vue/devtools-shared': 7.7.7 - execa: 9.6.0 + '@vue/devtools-core': 8.0.5(vite@5.4.19(@types/node@22.18.1))(vue@3.5.21(typescript@5.6.3)) + '@vue/devtools-kit': 8.0.5 + '@vue/devtools-shared': 8.0.5 sirv: 3.0.2 vite: 5.4.19(@types/node@22.18.1) - vite-plugin-inspect: 0.8.9(rollup@4.50.1)(vite@5.4.19(@types/node@22.18.1)) + vite-plugin-inspect: 11.3.3(vite@5.4.19(@types/node@22.18.1)) vite-plugin-vue-inspector: 5.3.2(vite@5.4.19(@types/node@22.18.1)) transitivePeerDependencies: - '@nuxt/kit' - - rollup - supports-color - vue @@ -8511,6 +8406,4 @@ snapshots: yocto-queue@0.1.0: {} - yoctocolors@2.1.2: {} - zwitch@2.0.4: {} diff --git a/script/install.sh b/script/install.sh index 6f454946..270d4ba3 100644 --- a/script/install.sh +++ b/script/install.sh @@ -271,6 +271,7 @@ foreign_network_whitelist = "*" disable_p2p = false p2p_only = false relay_all_peer_rpc = false +disable_tcp_hole_punching = false disable_udp_hole_punching = false EOF