diff --git a/easytier-gui/src-tauri/src/lib.rs b/easytier-gui/src-tauri/src/lib.rs index 26828a9b..4e91c7f8 100644 --- a/easytier-gui/src-tauri/src/lib.rs +++ b/easytier-gui/src-tauri/src/lib.rs @@ -73,7 +73,7 @@ static RPC_SERVER: once_cell::sync::Lazy>> = static WEB_CLIENT: once_cell::sync::Lazy>> = once_cell::sync::Lazy::new(|| RwLock::new(None)); -#[cfg(target_os = "android")] +#[cfg(any(target_os = "android", test))] const ANDROID_SHARED_TUN_DEV_NAME: &str = "easytier-shared"; macro_rules! get_client_manager { @@ -88,15 +88,18 @@ macro_rules! get_client_manager { fn normalize_network_config_for_runtime(cfg: &mut NetworkConfig) { #[cfg(target_os = "android")] - { - if !cfg.no_tun() && cfg.dev_name.as_deref().map(str::is_empty).unwrap_or(true) { - cfg.dev_name = Some(ANDROID_SHARED_TUN_DEV_NAME.to_owned()); - } - } + normalize_android_network_config(cfg); #[cfg(not(target_os = "android"))] let _ = cfg; } +#[cfg(any(target_os = "android", test))] +fn normalize_android_network_config(cfg: &mut NetworkConfig) { + if !cfg.no_tun() && cfg.dev_name.as_deref().map(str::is_empty).unwrap_or(true) { + cfg.dev_name = Some(ANDROID_SHARED_TUN_DEV_NAME.to_owned()); + } +} + #[tauri::command] fn easytier_version() -> Result { Ok(easytier::VERSION.to_string()) @@ -252,6 +255,31 @@ async fn set_tun_fd( return Err("duplicate instance id in TUN fd attachment group".to_string()); } + #[cfg(any(target_os = "android", target_os = "ios"))] + if fd <= 0 { + let mut errors = Vec::new(); + for instance_id in target_ids { + if let Err(error) = attach_mobile_tun_fd( + instance_manager.as_ref(), + instance_id, + fd, + easytier::instance::virtual_nic::MobileTunSources::default(), + ) + .await + { + errors.push(format!("{instance_id}: {error}")); + } + } + return if errors.is_empty() { + Ok(()) + } else { + Err(format!( + "failed to detach tun fd from the instance group: {}", + errors.join("; ") + )) + }; + } + #[cfg(any(target_os = "android", target_os = "ios"))] let mut source_map = parse_tun_fd_instance_sources(instance_sources.unwrap_or_default())?; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -955,9 +983,10 @@ mod manager { &self, app: &AppHandle, inst_id: Uuid, - cfg: NetworkConfig, + mut cfg: NetworkConfig, source: PersistedConfigSource, ) -> anyhow::Result<()> { + normalize_network_config_for_runtime(&mut cfg); let source = self .network_configs .get(&inst_id) @@ -1436,8 +1465,33 @@ mod manager { #[cfg(test)] mod tests { use super::{PersistedConfigSource, StoredGuiConfig}; + use crate::{ANDROID_SHARED_TUN_DEV_NAME, normalize_android_network_config}; use easytier::proto::api::manage::NetworkConfig; + #[test] + fn android_default_tun_config_uses_shared_device_name() { + let mut config = NetworkConfig::default(); + + normalize_android_network_config(&mut config); + + assert_eq!( + config.dev_name.as_deref(), + Some(ANDROID_SHARED_TUN_DEV_NAME) + ); + } + + #[test] + fn android_no_tun_config_keeps_empty_device_name() { + let mut config = NetworkConfig { + no_tun: Some(true), + ..Default::default() + }; + + normalize_android_network_config(&mut config); + + assert!(config.dev_name.is_none()); + } + #[test] fn stored_gui_config_defaults_missing_source_to_legacy() { let stored: StoredGuiConfig = serde_json::from_value(serde_json::json!({ diff --git a/easytier-gui/src/composables/mobile_vpn.ts b/easytier-gui/src/composables/mobile_vpn.ts index d2b8535d..87b64bc4 100644 --- a/easytier-gui/src/composables/mobile_vpn.ts +++ b/easytier-gui/src/composables/mobile_vpn.ts @@ -100,11 +100,23 @@ async function waitVpnStatus(target_status: boolean, timeout_sec: number) { } } +async function detachTunFd(instanceIds: string[], instanceSources: TunFdInstanceSources[]) { + try { + await setTunFd(0, instanceIds, instanceSources) + } + catch (e) { + console.error('detach tun fd failed', e) + } +} + async function doStopVpn(force = false) { const wasRunning = curVpnStatus.running if (!force && !wasRunning) { return } + const instanceIds = [...curVpnStatus.instanceIds] + const instanceSources = [...curVpnStatus.instanceSources] + await detachTunFd(instanceIds, instanceSources) console.log('stop vpn') const stop_ret = await stop_vpn() console.log('stop vpn', JSON.stringify((stop_ret))) @@ -194,6 +206,9 @@ async function onVpnServiceStart(payload: any) { async function onVpnServiceStop(payload: any) { console.log('vpn service stop', JSON.stringify(payload)) + const instanceIds = [...curVpnStatus.instanceIds] + const instanceSources = [...curVpnStatus.instanceSources] + await detachTunFd(instanceIds, instanceSources) curVpnStatus.running = false resetVpnConfigStatus() } diff --git a/easytier/src/instance/shared_virtual_nic/dispatcher.rs b/easytier/src/instance/shared_virtual_nic/dispatcher.rs index 297e81f1..39b223e1 100644 --- a/easytier/src/instance/shared_virtual_nic/dispatcher.rs +++ b/easytier/src/instance/shared_virtual_nic/dispatcher.rs @@ -15,6 +15,7 @@ use pnet_packet::{ icmp::{self, MutableIcmpPacket}, ip::IpNextHeaderProtocols, ipv4::{self, MutableIpv4Packet}, + ipv6::MutableIpv6Packet, tcp::{self, MutableTcpPacket}, udp::{self, MutableUdpPacket}, }; @@ -48,6 +49,7 @@ const TCP_HEADER_MIN_LEN: usize = 20; const UDP_HEADER_LEN: usize = 8; const ICMP_ECHO_HEADER_LEN: usize = 8; const ICMP_PROTOCOL: u8 = 1; +const ICMPV6_PROTOCOL: u8 = 58; const TCP_PROTOCOL: u8 = 6; const UDP_PROTOCOL: u8 = 17; const DISPATCHER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(1); @@ -372,6 +374,13 @@ impl SharedVirtualNicFlowAddr { Self::V6(_) => None, } } + + fn as_ipv6(self) -> Option { + match self { + Self::V4(_) => None, + Self::V6(addr) => Some(std::net::Ipv6Addr::from(addr)), + } + } } impl SharedVirtualNicFlowKey { @@ -416,12 +425,16 @@ impl SharedVirtualNicFlowKey { return None; } - let protocol = payload[6]; + let transport = ipv6_transport_info(payload)?; Some(Self { src: SharedVirtualNicFlowAddr::V6(read_ipv6_addr(payload, 8)), dst: SharedVirtualNicFlowAddr::V6(read_ipv6_addr(payload, 24)), - protocol, - ports: transport_ports(protocol, &payload[IPV6_HEADER_LEN..]), + protocol: transport.protocol, + ports: if transport.fragment_offset == 0 { + transport_ports(transport.protocol, &payload[transport.offset..]) + } else { + None + }, }) } @@ -431,7 +444,7 @@ impl SharedVirtualNicFlowKey { dst: self.src, protocol: self.protocol, ports: self.ports.map(|ports| { - if self.protocol == ICMP_PROTOCOL { + if matches!(self.protocol, ICMP_PROTOCOL | ICMPV6_PROTOCOL) { ports } else { ports.reversed() @@ -1231,21 +1244,27 @@ impl SharedVirtualNicDispatcherState { member_id: SharedVirtualNicMemberId, mut packet: ZCPacket, ) -> Result<(), ZCPacket> { - let mut nat_entry = None; - if let Some(key) = SharedVirtualNicFlowKey::from_packet(&packet) { - if let Some(translated_src) = self - .source_table - .source_for_member_destination(member_id, key.dst) - { - if key.src != translated_src - && rewrite_packet_source(&mut packet, key.src, translated_src) - { - nat_entry = Some((packet.clone(), key.src, translated_src)); - } - } - } + let Some(key) = SharedVirtualNicFlowKey::from_packet(&packet) else { + return Err(packet); + }; + let Some(translated_src) = self + .source_table + .source_for_member_destination(member_id, key.dst) + else { + return Err(packet); + }; + let original_packet = packet.clone(); + let nat_entry = if key.src == translated_src { + None + } else if rewrite_packet_source(&mut packet, key.src, translated_src) { + Some((packet.clone(), key.src, translated_src)) + } else { + return Err(packet); + }; - self.send_packet_to_member(member_id, packet).await?; + if self.send_packet_to_member(member_id, packet).await.is_err() { + return Err(original_packet); + } if let Some((translated_packet, original_src, translated_src)) = nat_entry { self.nat_table .remember(&translated_packet, original_src, translated_src); @@ -1304,7 +1323,7 @@ fn rewrite_packet_source( expected_source: SharedVirtualNicFlowAddr, new_source: SharedVirtualNicFlowAddr, ) -> bool { - rewrite_ipv4_addr(packet, expected_source, new_source, RewriteIpv4Addr::Source) + rewrite_ip_addr(packet, expected_source, new_source, RewriteIpAddr::Source) } fn rewrite_packet_destination( @@ -1312,25 +1331,42 @@ fn rewrite_packet_destination( expected_destination: SharedVirtualNicFlowAddr, new_destination: SharedVirtualNicFlowAddr, ) -> bool { - rewrite_ipv4_addr( + rewrite_ip_addr( packet, expected_destination, new_destination, - RewriteIpv4Addr::Destination, + RewriteIpAddr::Destination, ) } #[derive(Clone, Copy)] -enum RewriteIpv4Addr { +enum RewriteIpAddr { Source, Destination, } +fn rewrite_ip_addr( + packet: &mut ZCPacket, + expected_addr: SharedVirtualNicFlowAddr, + new_addr: SharedVirtualNicFlowAddr, + rewrite: RewriteIpAddr, +) -> bool { + match (expected_addr, new_addr) { + (SharedVirtualNicFlowAddr::V4(_), SharedVirtualNicFlowAddr::V4(_)) => { + rewrite_ipv4_addr(packet, expected_addr, new_addr, rewrite) + } + (SharedVirtualNicFlowAddr::V6(_), SharedVirtualNicFlowAddr::V6(_)) => { + rewrite_ipv6_addr(packet, expected_addr, new_addr, rewrite) + } + _ => false, + } +} + fn rewrite_ipv4_addr( packet: &mut ZCPacket, expected_addr: SharedVirtualNicFlowAddr, new_addr: SharedVirtualNicFlowAddr, - rewrite: RewriteIpv4Addr, + rewrite: RewriteIpAddr, ) -> bool { let Some(expected_addr) = expected_addr.as_ipv4() else { return false; @@ -1355,10 +1391,10 @@ fn rewrite_ipv4_addr( || (ipv4_packet.get_flags() & ipv4::Ipv4Flags::MoreFragments) != 0; match rewrite { - RewriteIpv4Addr::Source if ipv4_packet.get_source() == expected_addr => { + RewriteIpAddr::Source if ipv4_packet.get_source() == expected_addr => { ipv4_packet.set_source(new_addr); } - RewriteIpv4Addr::Destination if ipv4_packet.get_destination() == expected_addr => { + RewriteIpAddr::Destination if ipv4_packet.get_destination() == expected_addr => { ipv4_packet.set_destination(new_addr); } _ => return false, @@ -1380,6 +1416,154 @@ fn rewrite_ipv4_addr( true } +fn rewrite_ipv6_addr( + packet: &mut ZCPacket, + expected_addr: SharedVirtualNicFlowAddr, + new_addr: SharedVirtualNicFlowAddr, + rewrite: RewriteIpAddr, +) -> bool { + let Some(expected_addr) = expected_addr.as_ipv6() else { + return false; + }; + let Some(new_addr) = new_addr.as_ipv6() else { + return false; + }; + + let payload = packet.mut_payload(); + let Some(transport) = ipv6_transport_info(payload) else { + return false; + }; + if matches!(transport.protocol, 50 | 51) + || (matches!(rewrite, RewriteIpAddr::Destination) && !transport.destination_rewrite_safe) + { + return false; + } + let Some(mut ipv6_packet) = MutableIpv6Packet::new(payload) else { + return false; + }; + + let old_source = ipv6_packet.get_source(); + let old_destination = ipv6_packet.get_destination(); + match rewrite { + RewriteIpAddr::Source if old_source == expected_addr => { + ipv6_packet.set_source(new_addr); + } + RewriteIpAddr::Destination if old_destination == expected_addr => { + ipv6_packet.set_destination(new_addr); + } + _ => return false, + } + + if transport.fragment_offset == 0 { + let source = ipv6_packet.get_source(); + let destination = ipv6_packet.get_destination(); + adjust_ipv6_transport_checksum( + ipv6_packet.packet_mut(), + transport, + old_source, + source, + old_destination, + destination, + ); + } + true +} + +#[derive(Clone, Copy)] +struct Ipv6TransportInfo { + protocol: u8, + offset: usize, + fragment_offset: u16, + destination_rewrite_safe: bool, +} + +fn ipv6_transport_info(payload: &[u8]) -> Option { + if payload.len() < IPV6_HEADER_LEN || payload[0] >> 4 != 6 { + return None; + } + + let mut protocol = payload[6]; + let mut offset = IPV6_HEADER_LEN; + let mut fragment_offset = 0; + let mut destination_rewrite_safe = true; + loop { + match protocol { + // Hop-by-hop, routing, and destination options headers. + 0 | 43 | 60 => { + if protocol == 43 { + destination_rewrite_safe = false; + } + let header = payload.get(offset..offset + 2)?; + protocol = header[0]; + let header_len = (usize::from(header[1]) + 1) * 8; + offset = offset.checked_add(header_len)?; + if offset > payload.len() { + return None; + } + } + // Fragment header. + 44 => { + let header = payload.get(offset..offset + 8)?; + protocol = header[0]; + fragment_offset = u16::from_be_bytes([header[2], header[3]]) >> 3; + offset += 8; + if fragment_offset != 0 { + return Some(Ipv6TransportInfo { + protocol, + offset, + fragment_offset, + destination_rewrite_safe: destination_rewrite_safe + && matches!(protocol, TCP_PROTOCOL | UDP_PROTOCOL | ICMPV6_PROTOCOL), + }); + } + } + _ => { + return Some(Ipv6TransportInfo { + protocol, + offset, + fragment_offset, + destination_rewrite_safe, + }); + } + } + } +} + +fn adjust_ipv6_transport_checksum( + payload: &mut [u8], + transport: Ipv6TransportInfo, + old_source: std::net::Ipv6Addr, + source: std::net::Ipv6Addr, + old_destination: std::net::Ipv6Addr, + destination: std::net::Ipv6Addr, +) { + let checksum_offset = match transport.protocol { + TCP_PROTOCOL => 16, + UDP_PROTOCOL => 6, + ICMPV6_PROTOCOL => 2, + _ => return, + }; + let Some(checksum_bytes) = + payload.get_mut(transport.offset + checksum_offset..transport.offset + checksum_offset + 2) + else { + return; + }; + let checksum = u16::from_be_bytes([checksum_bytes[0], checksum_bytes[1]]); + let checksum = adjust_ipv6_pseudo_header_checksum( + checksum, + old_source, + source, + old_destination, + destination, + ); + let checksum = if transport.protocol == UDP_PROTOCOL && checksum == 0 { + 0xffff + } else { + checksum + }; + checksum_bytes.copy_from_slice(&checksum.to_be_bytes()); +} + fn update_ipv4_transport_checksum(ipv4_packet: &mut MutableIpv4Packet<'_>, header_len: usize) { let source = ipv4_packet.get_source(); let destination = ipv4_packet.get_destination(); @@ -1483,6 +1667,29 @@ fn adjust_ipv4_pseudo_header_checksum( checksum } +fn adjust_ipv6_pseudo_header_checksum( + mut checksum: u16, + old_source: std::net::Ipv6Addr, + source: std::net::Ipv6Addr, + old_destination: std::net::Ipv6Addr, + destination: std::net::Ipv6Addr, +) -> u16 { + for (old, new) in old_source + .segments() + .into_iter() + .zip(source.segments()) + .chain( + old_destination + .segments() + .into_iter() + .zip(destination.segments()), + ) + { + checksum = adjust_checksum_word(checksum, old, new); + } + checksum +} + fn ipv4_checksum_words(addr: std::net::Ipv4Addr) -> impl Iterator { let octets = addr.octets(); [ @@ -1753,7 +1960,7 @@ fn transport_ports(protocol: u8, payload: &[u8]) -> Option TCP_HEADER_MIN_LEN, UDP_PROTOCOL => UDP_HEADER_LEN, - ICMP_PROTOCOL => ICMP_ECHO_HEADER_LEN, + ICMP_PROTOCOL | ICMPV6_PROTOCOL => ICMP_ECHO_HEADER_LEN, _ => return None, }; @@ -1763,6 +1970,7 @@ fn transport_ports(protocol: u8, payload: &[u8]) -> Option icmp_echo_flow(payload), + ICMPV6_PROTOCOL => icmpv6_echo_flow(payload), _ => Some(SharedVirtualNicTransportPorts { src: u16::from_be_bytes([payload[0], payload[1]]), dst: u16::from_be_bytes([payload[2], payload[3]]), @@ -1770,6 +1978,16 @@ fn transport_ports(protocol: u8, payload: &[u8]) -> Option Option { + match payload[0] { + 128 | 129 => Some(SharedVirtualNicTransportPorts { + src: u16::from_be_bytes([payload[4], payload[5]]), + dst: u16::from_be_bytes([payload[6], payload[7]]), + }), + _ => None, + } +} + fn icmp_echo_flow(payload: &[u8]) -> Option { match payload[0] { ty if ty == icmp::IcmpTypes::EchoRequest.0 || ty == icmp::IcmpTypes::EchoReply.0 => { @@ -1834,6 +2052,84 @@ mod tests { ZCPacket::new_with_payload(&payload) } + fn ipv6_udp_packet_with_ports( + src: Ipv6Addr, + dst: Ipv6Addr, + src_port: u16, + dst_port: u16, + ) -> ZCPacket { + let mut payload = vec![0; IPV6_HEADER_LEN + UDP_HEADER_LEN]; + { + let mut ipv6_packet = pnet_packet::ipv6::MutableIpv6Packet::new(&mut payload).unwrap(); + ipv6_packet.set_version(6); + ipv6_packet.set_payload_length(UDP_HEADER_LEN as u16); + ipv6_packet.set_next_header(IpNextHeaderProtocols::Udp); + ipv6_packet.set_hop_limit(64); + ipv6_packet.set_source(src); + ipv6_packet.set_destination(dst); + } + { + let mut udp_packet = MutableUdpPacket::new(&mut payload[IPV6_HEADER_LEN..]).unwrap(); + udp_packet.set_source(src_port); + udp_packet.set_destination(dst_port); + udp_packet.set_length(UDP_HEADER_LEN as u16); + let checksum = udp::ipv6_checksum(&udp_packet.to_immutable(), &src, &dst); + udp_packet.set_checksum(checksum); + } + ZCPacket::new_with_payload(&payload) + } + + fn ipv6_tcp_packet_with_ports( + src: Ipv6Addr, + dst: Ipv6Addr, + src_port: u16, + dst_port: u16, + ) -> ZCPacket { + let mut payload = vec![0; IPV6_HEADER_LEN + TCP_HEADER_MIN_LEN]; + { + let mut ipv6_packet = pnet_packet::ipv6::MutableIpv6Packet::new(&mut payload).unwrap(); + ipv6_packet.set_version(6); + ipv6_packet.set_payload_length(TCP_HEADER_MIN_LEN as u16); + ipv6_packet.set_next_header(IpNextHeaderProtocols::Tcp); + ipv6_packet.set_hop_limit(64); + ipv6_packet.set_source(src); + ipv6_packet.set_destination(dst); + } + { + let mut tcp_packet = MutableTcpPacket::new(&mut payload[IPV6_HEADER_LEN..]).unwrap(); + tcp_packet.set_source(src_port); + tcp_packet.set_destination(dst_port); + tcp_packet.set_data_offset(5); + let checksum = tcp::ipv6_checksum(&tcp_packet.to_immutable(), &src, &dst); + tcp_packet.set_checksum(checksum); + } + ZCPacket::new_with_payload(&payload) + } + + fn ipv6_icmp_echo_packet(src: Ipv6Addr, dst: Ipv6Addr) -> ZCPacket { + let mut payload = vec![0; IPV6_HEADER_LEN + ICMP_ECHO_HEADER_LEN]; + { + let mut ipv6_packet = pnet_packet::ipv6::MutableIpv6Packet::new(&mut payload).unwrap(); + ipv6_packet.set_version(6); + ipv6_packet.set_payload_length(ICMP_ECHO_HEADER_LEN as u16); + ipv6_packet.set_next_header(IpNextHeaderProtocols::Icmpv6); + ipv6_packet.set_hop_limit(64); + ipv6_packet.set_source(src); + ipv6_packet.set_destination(dst); + } + { + let mut icmp_packet = + pnet_packet::icmpv6::MutableIcmpv6Packet::new(&mut payload[IPV6_HEADER_LEN..]) + .unwrap(); + icmp_packet.set_icmpv6_type(pnet_packet::icmpv6::Icmpv6Types::EchoRequest); + icmp_packet.packet_mut()[4..6].copy_from_slice(&1234_u16.to_be_bytes()); + icmp_packet.packet_mut()[6..8].copy_from_slice(&1_u16.to_be_bytes()); + let checksum = pnet_packet::icmpv6::checksum(&icmp_packet.to_immutable(), &src, &dst); + icmp_packet.set_checksum(checksum); + } + ZCPacket::new_with_payload(&payload) + } + fn ipv4_udp_packet(src: Ipv4Addr, dst: Ipv4Addr) -> ZCPacket { ipv4_udp_packet_with_ports(src, dst, 1234, 5678) } @@ -1987,6 +2283,13 @@ mod tests { SharedVirtualNicMemberSources::from_claims(&member_claims(&[], ipv6, &[], &[])) } + fn member_sources_with_ipv6_routes( + ipv6: &[&str], + ipv6_routes: &[&str], + ) -> SharedVirtualNicMemberSources { + SharedVirtualNicMemberSources::from_claims(&member_claims(&[], ipv6, &[], ipv6_routes)) + } + fn member_claims( ipv4: &[&str], ipv6: &[&str], @@ -2268,6 +2571,98 @@ mod tests { assert_eq!(reply_ipv4.get_destination(), source_owner_ip); } + #[tokio::test] + async fn dispatcher_translates_wrong_local_ipv6_source_to_destination_member() { + let source_owner = uuid::Uuid::from_u128(1); + let destination_owner = uuid::Uuid::from_u128(2); + let source_owner_ip = "2001:db8:1::1".parse::().unwrap(); + let destination_owner_ip = "2001:db8:2::1".parse::().unwrap(); + let remote_ip = "2001:db8:99::2".parse::().unwrap(); + let (source_sender, mut source_receiver) = mpsc::channel(1); + let (destination_sender, mut destination_receiver) = mpsc::channel(1); + let mut state = SharedVirtualNicDispatcherState::default(); + + state.register(source_owner, member_entry(source_sender)); + state.register(destination_owner, member_entry(destination_sender)); + state.source_table.update_member_sources( + source_owner, + member_sources_with_ipv6(&["2001:db8:1::1/64"]), + ); + state.source_table.update_member_sources( + destination_owner, + member_sources_with_ipv6_routes(&["2001:db8:2::1/64"], &["2001:db8:99::/64"]), + ); + + state + .forward_tun_packet_to_member(ipv6_udp_packet_with_ports( + source_owner_ip, + remote_ip, + 1234, + 5678, + )) + .await; + + assert!(source_receiver.try_recv().is_err()); + let translated = destination_receiver.try_recv().unwrap(); + let translated_ipv6 = pnet_packet::ipv6::Ipv6Packet::new(translated.payload()).unwrap(); + assert_eq!(translated_ipv6.get_source(), destination_owner_ip); + assert_eq!(translated_ipv6.get_destination(), remote_ip); + let translated_udp = + pnet_packet::udp::UdpPacket::new(&translated.payload()[IPV6_HEADER_LEN..]).unwrap(); + assert_eq!( + translated_udp.get_checksum(), + udp::ipv6_checksum(&translated_udp, &destination_owner_ip, &remote_ip,) + ); + + let reply = state.prepare_member_packet_to_tun( + destination_owner, + ipv6_udp_packet_with_ports(remote_ip, destination_owner_ip, 5678, 1234), + ); + let reply_ipv6 = pnet_packet::ipv6::Ipv6Packet::new(reply.payload()).unwrap(); + assert_eq!(reply_ipv6.get_source(), remote_ip); + assert_eq!(reply_ipv6.get_destination(), source_owner_ip); + let reply_udp = + pnet_packet::udp::UdpPacket::new(&reply.payload()[IPV6_HEADER_LEN..]).unwrap(); + assert_eq!( + reply_udp.get_checksum(), + udp::ipv6_checksum(&reply_udp, &remote_ip, &source_owner_ip) + ); + } + + #[test] + fn rewrite_ipv6_source_updates_tcp_and_icmpv6_checksums() { + let source = "2001:db8:1::1".parse::().unwrap(); + let translated_source = "2001:db8:2::1".parse::().unwrap(); + let destination = "2001:db8:99::2".parse::().unwrap(); + + let mut tcp_packet = ipv6_tcp_packet_with_ports(source, destination, 1234, 5678); + assert!(rewrite_packet_source( + &mut tcp_packet, + SharedVirtualNicFlowAddr::from(source), + SharedVirtualNicFlowAddr::from(translated_source), + )); + let tcp = + pnet_packet::tcp::TcpPacket::new(&tcp_packet.payload()[IPV6_HEADER_LEN..]).unwrap(); + assert_eq!( + tcp.get_checksum(), + tcp::ipv6_checksum(&tcp, &translated_source, &destination) + ); + + let mut icmp_packet = ipv6_icmp_echo_packet(source, destination); + assert!(rewrite_packet_source( + &mut icmp_packet, + SharedVirtualNicFlowAddr::from(source), + SharedVirtualNicFlowAddr::from(translated_source), + )); + let icmp = + pnet_packet::icmpv6::Icmpv6Packet::new(&icmp_packet.payload()[IPV6_HEADER_LEN..]) + .unwrap(); + assert_eq!( + icmp.get_checksum(), + pnet_packet::icmpv6::checksum(&icmp, &translated_source, &destination) + ); + } + #[tokio::test] async fn dispatcher_unregister_clears_nat_translation() { let source_owner = uuid::Uuid::from_u128(1); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 240e35ff..18672c14 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,7 @@ settings: excludeLinksFromLockfile: false overrides: + happy-dom: 16.8.1 minimatch: 10.2.4 importers: @@ -58,7 +59,7 @@ importers: devDependencies: '@antfu/eslint-config': specifier: ^3.7.3 - version: 3.16.0(@typescript-eslint/utils@8.42.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3))(@vue/compiler-sfc@3.5.21)(eslint-plugin-format@0.1.3(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)(vitest@2.1.9(@types/node@22.18.1)(happy-dom@16.8.1)) + version: 3.16.0(@typescript-eslint/utils@8.42.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3))(@vue/compiler-sfc@3.5.21)(eslint-plugin-format@0.1.3(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)(vitest@4.1.9(@types/node@22.18.1)(happy-dom@16.8.1)(vite@5.4.21(@types/node@22.18.1))) '@intlify/unplugin-vue-i18n': specifier: ^5.2.0 version: 5.3.1(@vue/compiler-dom@3.5.21)(eslint@9.35.0(jiti@2.5.1))(rollup@4.50.1)(typescript@5.6.3)(vue-i18n@10.0.8(vue@3.5.21(typescript@5.6.3)))(vue@3.5.21(typescript@5.6.3)) @@ -1009,8 +1010,8 @@ packages: resolution: {integrity: sha512-BcmHpb5bQyeVNrptC3UhzpBZB/YHHDoEREOUERrmF2BRxsyOEuRrq+Z96C/D4+2KJb8kuHiouzAei7BXlG0YYw==} engines: {node: '>= 16'} - '@intlify/shared@11.4.8': - resolution: {integrity: sha512-XbRgrv+XEuvDr7UCY55oibVrh+o4u+A0VB6nSL0F5Z8LcZxE/8j573LYG6bCrOigIcHdGpSNI7Rh5UpC5/B/eg==} + '@intlify/shared@11.4.6': + resolution: {integrity: sha512-m1p1HHAMLhqSpTRH7VnXdrN0CQ4y+9vunFkpLkbD8soIuBsnQdawZXqMCgvwI2UVF9Ww7sVaw7g9tV2VO7shoA==} engines: {node: '>= 22'} '@intlify/shared@12.0.0-alpha.4': @@ -1412,6 +1413,9 @@ packages: '@rushstack/ts-command-line@5.0.2': resolution: {integrity: sha512-+AkJDbu1GFMPIU8Sb7TLVXDv/Q7Mkvx+wAjEl8XiXVVq+p1FmWW6M3LYpJMmoHNckSofeMecgWg5lfMwNAAsEQ==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@stylistic/eslint-plugin@2.13.0': resolution: {integrity: sha512-RnO1SaiCFHn666wNz2QfZEFxvmiNRqhzaMXHXxXXKt+MEP7aajlPxUSMIQpKAaJfverpovEYqjBOXDq6dDcaOQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1522,9 +1526,15 @@ packages: '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/default-gateway@7.2.2': resolution: {integrity: sha512-35C93fYQlnLKLASkMPoxRvok4fENwB3By9clRLd2I/08n/XRl0pCdf7EB17K5oMMwZu8NBYA8i66jH5r/LYBKA==} @@ -1753,6 +1763,9 @@ packages: '@vitest/expect@2.1.9': resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + '@vitest/expect@4.1.9': + resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} + '@vitest/mocker@2.1.9': resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} peerDependencies: @@ -1764,21 +1777,47 @@ packages: vite: optional: true + '@vitest/mocker@4.1.9': + resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/pretty-format@2.1.9': resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@4.1.9': + resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} + '@vitest/runner@2.1.9': resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + '@vitest/runner@4.1.9': + resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==} + '@vitest/snapshot@2.1.9': resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + '@vitest/snapshot@4.1.9': + resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} + '@vitest/spy@2.1.9': resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + '@vitest/spy@4.1.9': + resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} + '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@4.1.9': + resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} + '@volar/language-core@2.4.15': resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==} @@ -2284,6 +2323,10 @@ packages: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2492,6 +2535,9 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -2793,6 +2839,15 @@ packages: fault@2.0.1: resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2901,7 +2956,6 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true globals@13.24.0: @@ -3478,6 +3532,10 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} + onetime@6.0.0: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} @@ -3583,6 +3641,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -3875,6 +3937,9 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -3973,6 +4038,14 @@ packages: tinyexec@1.0.1: resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + tinypool@1.1.1: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3981,6 +4054,10 @@ packages: resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} engines: {node: '>=14.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + tinyspy@3.0.2: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} @@ -4169,7 +4246,6 @@ packages: unplugin-vue-router@0.10.9: resolution: {integrity: sha512-DXmC0GMcROOnCmN56GRvi1bkkG1BnVs4xJqNvucBUeZkmB245URvtxOfbo3H6q4SOUQQbLPYWd6InzvjRh363A==} - deprecated: 'Merged into vuejs/router. Migrate: https://router.vuejs.org/guide/migration/v4-to-v5.html' peerDependencies: vue-router: ^4.4.0 peerDependenciesMeta: @@ -4201,7 +4277,6 @@ packages: uuid@10.0.0: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@11.1.0: @@ -4305,7 +4380,7 @@ packages: '@types/node': ^18.0.0 || >=20.0.0 '@vitest/browser': 2.1.9 '@vitest/ui': 2.1.9 - happy-dom: '*' + happy-dom: 16.8.1 jsdom: '*' peerDependenciesMeta: '@edge-runtime/vm': @@ -4321,6 +4396,47 @@ packages: jsdom: optional: true + vitest@4.1.9: + resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.9 + '@vitest/browser-preview': 4.1.9 + '@vitest/browser-webdriverio': 4.1.9 + '@vitest/coverage-istanbul': 4.1.9 + '@vitest/coverage-v8': 4.1.9 + '@vitest/ui': 4.1.9 + happy-dom: 16.8.1 + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} @@ -4467,7 +4583,7 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@antfu/eslint-config@3.16.0(@typescript-eslint/utils@8.42.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3))(@vue/compiler-sfc@3.5.21)(eslint-plugin-format@0.1.3(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)(vitest@2.1.9(@types/node@22.18.1)(happy-dom@16.8.1))': + '@antfu/eslint-config@3.16.0(@typescript-eslint/utils@8.42.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3))(@vue/compiler-sfc@3.5.21)(eslint-plugin-format@0.1.3(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)(vitest@4.1.9(@types/node@22.18.1)(happy-dom@16.8.1)(vite@5.4.21(@types/node@22.18.1)))': dependencies: '@antfu/install-pkg': 1.1.0 '@clack/prompts': 0.9.1 @@ -4476,7 +4592,7 @@ snapshots: '@stylistic/eslint-plugin': 2.13.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3) '@typescript-eslint/eslint-plugin': 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3))(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3) '@typescript-eslint/parser': 8.42.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3) - '@vitest/eslint-plugin': 1.3.9(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)(vitest@2.1.9(@types/node@22.18.1)(happy-dom@16.8.1)) + '@vitest/eslint-plugin': 1.3.9(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)(vitest@4.1.9(@types/node@22.18.1)(happy-dom@16.8.1)(vite@5.4.21(@types/node@22.18.1))) eslint: 9.35.0(jiti@2.5.1) eslint-config-flat-gitignore: 1.0.1(eslint@9.35.0(jiti@2.5.1)) eslint-flat-config-utils: 1.1.0 @@ -5047,7 +5163,7 @@ snapshots: '@intlify/shared@10.0.8': {} - '@intlify/shared@11.4.8': {} + '@intlify/shared@11.4.6': {} '@intlify/shared@12.0.0-alpha.4': {} @@ -5057,8 +5173,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.4.8 - '@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@11.4.8)(@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.4.6 + '@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@11.4.6)(@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) @@ -5080,11 +5196,11 @@ snapshots: - supports-color - typescript - '@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@11.4.8)(@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.4.6)(@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.4.8 + '@intlify/shared': 11.4.6 '@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)) @@ -5420,6 +5536,9 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@standard-schema/spec@1.1.0': + optional: true + '@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) @@ -5510,10 +5629,19 @@ snapshots: '@types/argparse@1.0.38': {} + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + optional: true + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': + optional: true + '@types/default-gateway@7.2.2': {} '@types/estree@1.0.8': {} @@ -5711,14 +5839,14 @@ snapshots: vite: 5.4.21(@types/node@22.18.1) vue: 3.5.21(typescript@5.6.3) - '@vitest/eslint-plugin@1.3.9(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)(vitest@2.1.9(@types/node@22.18.1)(happy-dom@16.8.1))': + '@vitest/eslint-plugin@1.3.9(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)(vitest@4.1.9(@types/node@22.18.1)(happy-dom@16.8.1)(vite@5.4.21(@types/node@22.18.1)))': dependencies: '@typescript-eslint/scope-manager': 8.42.0 '@typescript-eslint/utils': 8.42.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3) eslint: 9.35.0(jiti@2.5.1) optionalDependencies: typescript: 5.6.3 - vitest: 2.1.9(@types/node@22.18.1)(happy-dom@16.8.1) + vitest: 4.1.9(@types/node@22.18.1)(happy-dom@16.8.1)(vite@5.4.21(@types/node@22.18.1)) transitivePeerDependencies: - supports-color @@ -5729,6 +5857,16 @@ snapshots: chai: 5.3.3 tinyrainbow: 1.2.0 + '@vitest/expect@4.1.9': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + chai: 6.2.2 + tinyrainbow: 3.1.0 + optional: true + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.18.1))': dependencies: '@vitest/spy': 2.1.9 @@ -5737,31 +5875,69 @@ snapshots: optionalDependencies: vite: 5.4.21(@types/node@22.18.1) + '@vitest/mocker@4.1.9(vite@5.4.21(@types/node@22.18.1))': + dependencies: + '@vitest/spy': 4.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@22.18.1) + optional: true + '@vitest/pretty-format@2.1.9': dependencies: tinyrainbow: 1.2.0 + '@vitest/pretty-format@4.1.9': + dependencies: + tinyrainbow: 3.1.0 + optional: true + '@vitest/runner@2.1.9': dependencies: '@vitest/utils': 2.1.9 pathe: 1.1.2 + '@vitest/runner@4.1.9': + dependencies: + '@vitest/utils': 4.1.9 + pathe: 2.0.3 + optional: true + '@vitest/snapshot@2.1.9': dependencies: '@vitest/pretty-format': 2.1.9 magic-string: 0.30.21 pathe: 1.1.2 + '@vitest/snapshot@4.1.9': + dependencies: + '@vitest/pretty-format': 4.1.9 + '@vitest/utils': 4.1.9 + magic-string: 0.30.21 + pathe: 2.0.3 + optional: true + '@vitest/spy@2.1.9': dependencies: tinyspy: 3.0.2 + '@vitest/spy@4.1.9': + optional: true + '@vitest/utils@2.1.9': dependencies: '@vitest/pretty-format': 2.1.9 loupe: 3.2.1 tinyrainbow: 1.2.0 + '@vitest/utils@4.1.9': + dependencies: + '@vitest/pretty-format': 4.1.9 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + optional: true + '@volar/language-core@2.4.15': dependencies: '@volar/source-map': 2.4.15 @@ -6436,6 +6612,9 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 + chai@6.2.2: + optional: true + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -6611,6 +6790,9 @@ snapshots: es-module-lexer@1.7.0: {} + es-module-lexer@2.1.0: + optional: true + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -7059,6 +7241,11 @@ snapshots: dependencies: format: 0.2.2 + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + optional: true + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -7850,6 +8037,9 @@ snapshots: object-hash@3.0.0: {} + obug@2.1.3: + optional: true + onetime@6.0.0: dependencies: mimic-fn: 4.0.0 @@ -7954,6 +8144,9 @@ snapshots: picomatch@4.0.3: {} + picomatch@4.0.4: + optional: true + pify@2.3.0: {} pinia@2.3.1(typescript@5.6.3)(vue@3.5.21(typescript@5.6.3)): @@ -8237,6 +8430,9 @@ snapshots: std-env@3.10.0: {} + std-env@4.1.0: + optional: true + string-argv@0.3.2: {} string-width@4.2.3: @@ -8353,10 +8549,22 @@ snapshots: tinyexec@1.0.1: {} + tinyexec@1.2.4: + optional: true + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + optional: true + tinypool@1.1.1: {} tinyrainbow@1.2.0: {} + tinyrainbow@3.1.0: + optional: true + tinyspy@3.0.2: {} to-regex-range@5.0.1: @@ -8810,6 +9018,35 @@ snapshots: - supports-color - terser + vitest@4.1.9(@types/node@22.18.1)(happy-dom@16.8.1)(vite@5.4.21(@types/node@22.18.1)): + dependencies: + '@vitest/expect': 4.1.9 + '@vitest/mocker': 4.1.9(vite@5.4.21(@types/node@22.18.1)) + '@vitest/pretty-format': 4.1.9 + '@vitest/runner': 4.1.9 + '@vitest/snapshot': 4.1.9 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 5.4.21(@types/node@22.18.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.18.1 + happy-dom: 16.8.1 + transitivePeerDependencies: + - msw + optional: true + vscode-uri@3.1.0: {} vue-chartjs@5.3.2(chart.js@4.5.0)(vue@3.5.21(typescript@5.6.3)):