fix(shared-tun): close mobile routing gaps

Translate IPv6 traffic across shared mobile members, including
transport checksums and reverse-flow handling.

Normalize saved Android TUN configs and detach runtimes before the
VPN fd closes. Restore the mainline lockfile after resolving the merge.
This commit is contained in:
KKRainbow
2026-08-13 01:43:40 +08:00
parent 144e306562
commit b5bc4a853d
4 changed files with 750 additions and 49 deletions
+60 -6
View File
@@ -73,7 +73,7 @@ static RPC_SERVER: once_cell::sync::Lazy<Mutex<Option<RpcServer>>> =
static WEB_CLIENT: once_cell::sync::Lazy<RwLock<Option<WebClient>>> = static WEB_CLIENT: once_cell::sync::Lazy<RwLock<Option<WebClient>>> =
once_cell::sync::Lazy::new(|| RwLock::new(None)); 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"; const ANDROID_SHARED_TUN_DEV_NAME: &str = "easytier-shared";
macro_rules! get_client_manager { macro_rules! get_client_manager {
@@ -88,13 +88,16 @@ macro_rules! get_client_manager {
fn normalize_network_config_for_runtime(cfg: &mut NetworkConfig) { fn normalize_network_config_for_runtime(cfg: &mut NetworkConfig) {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
{ 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) { 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()); cfg.dev_name = Some(ANDROID_SHARED_TUN_DEV_NAME.to_owned());
} }
}
#[cfg(not(target_os = "android"))]
let _ = cfg;
} }
#[tauri::command] #[tauri::command]
@@ -252,6 +255,31 @@ async fn set_tun_fd(
return Err("duplicate instance id in TUN fd attachment group".to_string()); 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"))] #[cfg(any(target_os = "android", target_os = "ios"))]
let mut source_map = parse_tun_fd_instance_sources(instance_sources.unwrap_or_default())?; let mut source_map = parse_tun_fd_instance_sources(instance_sources.unwrap_or_default())?;
#[cfg(not(any(target_os = "android", target_os = "ios")))] #[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -955,9 +983,10 @@ mod manager {
&self, &self,
app: &AppHandle, app: &AppHandle,
inst_id: Uuid, inst_id: Uuid,
cfg: NetworkConfig, mut cfg: NetworkConfig,
source: PersistedConfigSource, source: PersistedConfigSource,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
normalize_network_config_for_runtime(&mut cfg);
let source = self let source = self
.network_configs .network_configs
.get(&inst_id) .get(&inst_id)
@@ -1436,8 +1465,33 @@ mod manager {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{PersistedConfigSource, StoredGuiConfig}; use super::{PersistedConfigSource, StoredGuiConfig};
use crate::{ANDROID_SHARED_TUN_DEV_NAME, normalize_android_network_config};
use easytier::proto::api::manage::NetworkConfig; 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] #[test]
fn stored_gui_config_defaults_missing_source_to_legacy() { fn stored_gui_config_defaults_missing_source_to_legacy() {
let stored: StoredGuiConfig = serde_json::from_value(serde_json::json!({ let stored: StoredGuiConfig = serde_json::from_value(serde_json::json!({
@@ -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) { async function doStopVpn(force = false) {
const wasRunning = curVpnStatus.running const wasRunning = curVpnStatus.running
if (!force && !wasRunning) { if (!force && !wasRunning) {
return return
} }
const instanceIds = [...curVpnStatus.instanceIds]
const instanceSources = [...curVpnStatus.instanceSources]
await detachTunFd(instanceIds, instanceSources)
console.log('stop vpn') console.log('stop vpn')
const stop_ret = await stop_vpn() const stop_ret = await stop_vpn()
console.log('stop vpn', JSON.stringify((stop_ret))) console.log('stop vpn', JSON.stringify((stop_ret)))
@@ -194,6 +206,9 @@ async function onVpnServiceStart(payload: any) {
async function onVpnServiceStop(payload: any) { async function onVpnServiceStop(payload: any) {
console.log('vpn service stop', JSON.stringify(payload)) console.log('vpn service stop', JSON.stringify(payload))
const instanceIds = [...curVpnStatus.instanceIds]
const instanceSources = [...curVpnStatus.instanceSources]
await detachTunFd(instanceIds, instanceSources)
curVpnStatus.running = false curVpnStatus.running = false
resetVpnConfigStatus() resetVpnConfigStatus()
} }
@@ -15,6 +15,7 @@ use pnet_packet::{
icmp::{self, MutableIcmpPacket}, icmp::{self, MutableIcmpPacket},
ip::IpNextHeaderProtocols, ip::IpNextHeaderProtocols,
ipv4::{self, MutableIpv4Packet}, ipv4::{self, MutableIpv4Packet},
ipv6::MutableIpv6Packet,
tcp::{self, MutableTcpPacket}, tcp::{self, MutableTcpPacket},
udp::{self, MutableUdpPacket}, udp::{self, MutableUdpPacket},
}; };
@@ -48,6 +49,7 @@ const TCP_HEADER_MIN_LEN: usize = 20;
const UDP_HEADER_LEN: usize = 8; const UDP_HEADER_LEN: usize = 8;
const ICMP_ECHO_HEADER_LEN: usize = 8; const ICMP_ECHO_HEADER_LEN: usize = 8;
const ICMP_PROTOCOL: u8 = 1; const ICMP_PROTOCOL: u8 = 1;
const ICMPV6_PROTOCOL: u8 = 58;
const TCP_PROTOCOL: u8 = 6; const TCP_PROTOCOL: u8 = 6;
const UDP_PROTOCOL: u8 = 17; const UDP_PROTOCOL: u8 = 17;
const DISPATCHER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(1); const DISPATCHER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(1);
@@ -372,6 +374,13 @@ impl SharedVirtualNicFlowAddr {
Self::V6(_) => None, Self::V6(_) => None,
} }
} }
fn as_ipv6(self) -> Option<std::net::Ipv6Addr> {
match self {
Self::V4(_) => None,
Self::V6(addr) => Some(std::net::Ipv6Addr::from(addr)),
}
}
} }
impl SharedVirtualNicFlowKey { impl SharedVirtualNicFlowKey {
@@ -416,12 +425,16 @@ impl SharedVirtualNicFlowKey {
return None; return None;
} }
let protocol = payload[6]; let transport = ipv6_transport_info(payload)?;
Some(Self { Some(Self {
src: SharedVirtualNicFlowAddr::V6(read_ipv6_addr(payload, 8)), src: SharedVirtualNicFlowAddr::V6(read_ipv6_addr(payload, 8)),
dst: SharedVirtualNicFlowAddr::V6(read_ipv6_addr(payload, 24)), dst: SharedVirtualNicFlowAddr::V6(read_ipv6_addr(payload, 24)),
protocol, protocol: transport.protocol,
ports: transport_ports(protocol, &payload[IPV6_HEADER_LEN..]), ports: if transport.fragment_offset == 0 {
transport_ports(transport.protocol, &payload[transport.offset..])
} else {
None
},
}) })
} }
@@ -431,7 +444,7 @@ impl SharedVirtualNicFlowKey {
dst: self.src, dst: self.src,
protocol: self.protocol, protocol: self.protocol,
ports: self.ports.map(|ports| { ports: self.ports.map(|ports| {
if self.protocol == ICMP_PROTOCOL { if matches!(self.protocol, ICMP_PROTOCOL | ICMPV6_PROTOCOL) {
ports ports
} else { } else {
ports.reversed() ports.reversed()
@@ -1231,21 +1244,27 @@ impl SharedVirtualNicDispatcherState {
member_id: SharedVirtualNicMemberId, member_id: SharedVirtualNicMemberId,
mut packet: ZCPacket, mut packet: ZCPacket,
) -> Result<(), ZCPacket> { ) -> Result<(), ZCPacket> {
let mut nat_entry = None; let Some(key) = SharedVirtualNicFlowKey::from_packet(&packet) else {
if let Some(key) = SharedVirtualNicFlowKey::from_packet(&packet) { return Err(packet);
if let Some(translated_src) = self };
let Some(translated_src) = self
.source_table .source_table
.source_for_member_destination(member_id, key.dst) .source_for_member_destination(member_id, key.dst)
{ else {
if key.src != translated_src return Err(packet);
&& rewrite_packet_source(&mut packet, key.src, translated_src) };
{ let original_packet = packet.clone();
nat_entry = Some((packet.clone(), key.src, translated_src)); 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 { if let Some((translated_packet, original_src, translated_src)) = nat_entry {
self.nat_table self.nat_table
.remember(&translated_packet, original_src, translated_src); .remember(&translated_packet, original_src, translated_src);
@@ -1304,7 +1323,7 @@ fn rewrite_packet_source(
expected_source: SharedVirtualNicFlowAddr, expected_source: SharedVirtualNicFlowAddr,
new_source: SharedVirtualNicFlowAddr, new_source: SharedVirtualNicFlowAddr,
) -> bool { ) -> 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( fn rewrite_packet_destination(
@@ -1312,25 +1331,42 @@ fn rewrite_packet_destination(
expected_destination: SharedVirtualNicFlowAddr, expected_destination: SharedVirtualNicFlowAddr,
new_destination: SharedVirtualNicFlowAddr, new_destination: SharedVirtualNicFlowAddr,
) -> bool { ) -> bool {
rewrite_ipv4_addr( rewrite_ip_addr(
packet, packet,
expected_destination, expected_destination,
new_destination, new_destination,
RewriteIpv4Addr::Destination, RewriteIpAddr::Destination,
) )
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
enum RewriteIpv4Addr { enum RewriteIpAddr {
Source, Source,
Destination, 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( fn rewrite_ipv4_addr(
packet: &mut ZCPacket, packet: &mut ZCPacket,
expected_addr: SharedVirtualNicFlowAddr, expected_addr: SharedVirtualNicFlowAddr,
new_addr: SharedVirtualNicFlowAddr, new_addr: SharedVirtualNicFlowAddr,
rewrite: RewriteIpv4Addr, rewrite: RewriteIpAddr,
) -> bool { ) -> bool {
let Some(expected_addr) = expected_addr.as_ipv4() else { let Some(expected_addr) = expected_addr.as_ipv4() else {
return false; return false;
@@ -1355,10 +1391,10 @@ fn rewrite_ipv4_addr(
|| (ipv4_packet.get_flags() & ipv4::Ipv4Flags::MoreFragments) != 0; || (ipv4_packet.get_flags() & ipv4::Ipv4Flags::MoreFragments) != 0;
match rewrite { 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); 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); ipv4_packet.set_destination(new_addr);
} }
_ => return false, _ => return false,
@@ -1380,6 +1416,154 @@ fn rewrite_ipv4_addr(
true 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<Ipv6TransportInfo> {
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) { fn update_ipv4_transport_checksum(ipv4_packet: &mut MutableIpv4Packet<'_>, header_len: usize) {
let source = ipv4_packet.get_source(); let source = ipv4_packet.get_source();
let destination = ipv4_packet.get_destination(); let destination = ipv4_packet.get_destination();
@@ -1483,6 +1667,29 @@ fn adjust_ipv4_pseudo_header_checksum(
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<Item = u16> { fn ipv4_checksum_words(addr: std::net::Ipv4Addr) -> impl Iterator<Item = u16> {
let octets = addr.octets(); let octets = addr.octets();
[ [
@@ -1753,7 +1960,7 @@ fn transport_ports(protocol: u8, payload: &[u8]) -> Option<SharedVirtualNicTrans
let min_len = match protocol { let min_len = match protocol {
TCP_PROTOCOL => TCP_HEADER_MIN_LEN, TCP_PROTOCOL => TCP_HEADER_MIN_LEN,
UDP_PROTOCOL => UDP_HEADER_LEN, UDP_PROTOCOL => UDP_HEADER_LEN,
ICMP_PROTOCOL => ICMP_ECHO_HEADER_LEN, ICMP_PROTOCOL | ICMPV6_PROTOCOL => ICMP_ECHO_HEADER_LEN,
_ => return None, _ => return None,
}; };
@@ -1763,6 +1970,7 @@ fn transport_ports(protocol: u8, payload: &[u8]) -> Option<SharedVirtualNicTrans
match protocol { match protocol {
ICMP_PROTOCOL => icmp_echo_flow(payload), ICMP_PROTOCOL => icmp_echo_flow(payload),
ICMPV6_PROTOCOL => icmpv6_echo_flow(payload),
_ => Some(SharedVirtualNicTransportPorts { _ => Some(SharedVirtualNicTransportPorts {
src: u16::from_be_bytes([payload[0], payload[1]]), src: u16::from_be_bytes([payload[0], payload[1]]),
dst: u16::from_be_bytes([payload[2], payload[3]]), dst: u16::from_be_bytes([payload[2], payload[3]]),
@@ -1770,6 +1978,16 @@ fn transport_ports(protocol: u8, payload: &[u8]) -> Option<SharedVirtualNicTrans
} }
} }
fn icmpv6_echo_flow(payload: &[u8]) -> Option<SharedVirtualNicTransportPorts> {
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<SharedVirtualNicTransportPorts> { fn icmp_echo_flow(payload: &[u8]) -> Option<SharedVirtualNicTransportPorts> {
match payload[0] { match payload[0] {
ty if ty == icmp::IcmpTypes::EchoRequest.0 || ty == icmp::IcmpTypes::EchoReply.0 => { ty if ty == icmp::IcmpTypes::EchoRequest.0 || ty == icmp::IcmpTypes::EchoReply.0 => {
@@ -1834,6 +2052,84 @@ mod tests {
ZCPacket::new_with_payload(&payload) 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 { fn ipv4_udp_packet(src: Ipv4Addr, dst: Ipv4Addr) -> ZCPacket {
ipv4_udp_packet_with_ports(src, dst, 1234, 5678) ipv4_udp_packet_with_ports(src, dst, 1234, 5678)
} }
@@ -1987,6 +2283,13 @@ mod tests {
SharedVirtualNicMemberSources::from_claims(&member_claims(&[], ipv6, &[], &[])) 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( fn member_claims(
ipv4: &[&str], ipv4: &[&str],
ipv6: &[&str], ipv6: &[&str],
@@ -2268,6 +2571,98 @@ mod tests {
assert_eq!(reply_ipv4.get_destination(), source_owner_ip); 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::<Ipv6Addr>().unwrap();
let destination_owner_ip = "2001:db8:2::1".parse::<Ipv6Addr>().unwrap();
let remote_ip = "2001:db8:99::2".parse::<Ipv6Addr>().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::<Ipv6Addr>().unwrap();
let translated_source = "2001:db8:2::1".parse::<Ipv6Addr>().unwrap();
let destination = "2001:db8:99::2".parse::<Ipv6Addr>().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] #[tokio::test]
async fn dispatcher_unregister_clears_nat_translation() { async fn dispatcher_unregister_clears_nat_translation() {
let source_owner = uuid::Uuid::from_u128(1); let source_owner = uuid::Uuid::from_u128(1);
+253 -16
View File
@@ -5,6 +5,7 @@ settings:
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
overrides: overrides:
happy-dom: 16.8.1
minimatch: 10.2.4 minimatch: 10.2.4
importers: importers:
@@ -58,7 +59,7 @@ importers:
devDependencies: devDependencies:
'@antfu/eslint-config': '@antfu/eslint-config':
specifier: ^3.7.3 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': '@intlify/unplugin-vue-i18n':
specifier: ^5.2.0 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)) 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==} resolution: {integrity: sha512-BcmHpb5bQyeVNrptC3UhzpBZB/YHHDoEREOUERrmF2BRxsyOEuRrq+Z96C/D4+2KJb8kuHiouzAei7BXlG0YYw==}
engines: {node: '>= 16'} engines: {node: '>= 16'}
'@intlify/shared@11.4.8': '@intlify/shared@11.4.6':
resolution: {integrity: sha512-XbRgrv+XEuvDr7UCY55oibVrh+o4u+A0VB6nSL0F5Z8LcZxE/8j573LYG6bCrOigIcHdGpSNI7Rh5UpC5/B/eg==} resolution: {integrity: sha512-m1p1HHAMLhqSpTRH7VnXdrN0CQ4y+9vunFkpLkbD8soIuBsnQdawZXqMCgvwI2UVF9Ww7sVaw7g9tV2VO7shoA==}
engines: {node: '>= 22'} engines: {node: '>= 22'}
'@intlify/shared@12.0.0-alpha.4': '@intlify/shared@12.0.0-alpha.4':
@@ -1412,6 +1413,9 @@ packages:
'@rushstack/ts-command-line@5.0.2': '@rushstack/ts-command-line@5.0.2':
resolution: {integrity: sha512-+AkJDbu1GFMPIU8Sb7TLVXDv/Q7Mkvx+wAjEl8XiXVVq+p1FmWW6M3LYpJMmoHNckSofeMecgWg5lfMwNAAsEQ==} 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': '@stylistic/eslint-plugin@2.13.0':
resolution: {integrity: sha512-RnO1SaiCFHn666wNz2QfZEFxvmiNRqhzaMXHXxXXKt+MEP7aajlPxUSMIQpKAaJfverpovEYqjBOXDq6dDcaOQ==} resolution: {integrity: sha512-RnO1SaiCFHn666wNz2QfZEFxvmiNRqhzaMXHXxXXKt+MEP7aajlPxUSMIQpKAaJfverpovEYqjBOXDq6dDcaOQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1522,9 +1526,15 @@ packages:
'@types/argparse@1.0.38': '@types/argparse@1.0.38':
resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} 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': '@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/default-gateway@7.2.2': '@types/default-gateway@7.2.2':
resolution: {integrity: sha512-35C93fYQlnLKLASkMPoxRvok4fENwB3By9clRLd2I/08n/XRl0pCdf7EB17K5oMMwZu8NBYA8i66jH5r/LYBKA==} resolution: {integrity: sha512-35C93fYQlnLKLASkMPoxRvok4fENwB3By9clRLd2I/08n/XRl0pCdf7EB17K5oMMwZu8NBYA8i66jH5r/LYBKA==}
@@ -1753,6 +1763,9 @@ packages:
'@vitest/expect@2.1.9': '@vitest/expect@2.1.9':
resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
'@vitest/expect@4.1.9':
resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==}
'@vitest/mocker@2.1.9': '@vitest/mocker@2.1.9':
resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==}
peerDependencies: peerDependencies:
@@ -1764,21 +1777,47 @@ packages:
vite: vite:
optional: true 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': '@vitest/pretty-format@2.1.9':
resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==}
'@vitest/pretty-format@4.1.9':
resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==}
'@vitest/runner@2.1.9': '@vitest/runner@2.1.9':
resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==}
'@vitest/runner@4.1.9':
resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==}
'@vitest/snapshot@2.1.9': '@vitest/snapshot@2.1.9':
resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==}
'@vitest/snapshot@4.1.9':
resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==}
'@vitest/spy@2.1.9': '@vitest/spy@2.1.9':
resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==}
'@vitest/spy@4.1.9':
resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==}
'@vitest/utils@2.1.9': '@vitest/utils@2.1.9':
resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==}
'@vitest/utils@4.1.9':
resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==}
'@volar/language-core@2.4.15': '@volar/language-core@2.4.15':
resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==} resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==}
@@ -2284,6 +2323,10 @@ packages:
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
engines: {node: '>=18'} engines: {node: '>=18'}
chai@6.2.2:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
chalk@4.1.2: chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -2492,6 +2535,9 @@ packages:
es-module-lexer@1.7.0: es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
es-module-lexer@2.1.0:
resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
es-object-atoms@1.1.1: es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2793,6 +2839,15 @@ packages:
fault@2.0.1: fault@2.0.1:
resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} 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: file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
@@ -2901,7 +2956,6 @@ packages:
glob@10.4.5: glob@10.4.5:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} 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 hasBin: true
globals@13.24.0: globals@13.24.0:
@@ -3478,6 +3532,10 @@ packages:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
obug@2.1.3:
resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==}
engines: {node: '>=12.20.0'}
onetime@6.0.0: onetime@6.0.0:
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -3583,6 +3641,10 @@ packages:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'} engines: {node: '>=12'}
picomatch@4.0.4:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
pify@2.3.0: pify@2.3.0:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -3875,6 +3937,9 @@ packages:
std-env@3.10.0: std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
std-env@4.1.0:
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
string-argv@0.3.2: string-argv@0.3.2:
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
engines: {node: '>=0.6.19'} engines: {node: '>=0.6.19'}
@@ -3973,6 +4038,14 @@ packages:
tinyexec@1.0.1: tinyexec@1.0.1:
resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} 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: tinypool@1.1.1:
resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
@@ -3981,6 +4054,10 @@ packages:
resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==}
engines: {node: '>=14.0.0'} 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: tinyspy@3.0.2:
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@@ -4169,7 +4246,6 @@ packages:
unplugin-vue-router@0.10.9: unplugin-vue-router@0.10.9:
resolution: {integrity: sha512-DXmC0GMcROOnCmN56GRvi1bkkG1BnVs4xJqNvucBUeZkmB245URvtxOfbo3H6q4SOUQQbLPYWd6InzvjRh363A==} resolution: {integrity: sha512-DXmC0GMcROOnCmN56GRvi1bkkG1BnVs4xJqNvucBUeZkmB245URvtxOfbo3H6q4SOUQQbLPYWd6InzvjRh363A==}
deprecated: 'Merged into vuejs/router. Migrate: https://router.vuejs.org/guide/migration/v4-to-v5.html'
peerDependencies: peerDependencies:
vue-router: ^4.4.0 vue-router: ^4.4.0
peerDependenciesMeta: peerDependenciesMeta:
@@ -4201,7 +4277,6 @@ packages:
uuid@10.0.0: uuid@10.0.0:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} 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 hasBin: true
uuid@11.1.0: uuid@11.1.0:
@@ -4305,7 +4380,7 @@ packages:
'@types/node': ^18.0.0 || >=20.0.0 '@types/node': ^18.0.0 || >=20.0.0
'@vitest/browser': 2.1.9 '@vitest/browser': 2.1.9
'@vitest/ui': 2.1.9 '@vitest/ui': 2.1.9
happy-dom: '*' happy-dom: 16.8.1
jsdom: '*' jsdom: '*'
peerDependenciesMeta: peerDependenciesMeta:
'@edge-runtime/vm': '@edge-runtime/vm':
@@ -4321,6 +4396,47 @@ packages:
jsdom: jsdom:
optional: true 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: vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
@@ -4467,7 +4583,7 @@ snapshots:
'@alloc/quick-lru@5.2.0': {} '@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: dependencies:
'@antfu/install-pkg': 1.1.0 '@antfu/install-pkg': 1.1.0
'@clack/prompts': 0.9.1 '@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) '@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/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) '@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: 9.35.0(jiti@2.5.1)
eslint-config-flat-gitignore: 1.0.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 eslint-flat-config-utils: 1.1.0
@@ -5047,7 +5163,7 @@ snapshots:
'@intlify/shared@10.0.8': {} '@intlify/shared@10.0.8': {}
'@intlify/shared@11.4.8': {} '@intlify/shared@11.4.6': {}
'@intlify/shared@12.0.0-alpha.4': {} '@intlify/shared@12.0.0-alpha.4': {}
@@ -5057,8 +5173,8 @@ snapshots:
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.8.0(eslint@9.35.0(jiti@2.5.1)) '@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/bundle-utils': 9.0.0(vue-i18n@10.0.8(vue@3.5.21(typescript@5.6.3)))
'@intlify/shared': 11.4.8 '@intlify/shared': 11.4.6
'@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))
'@rollup/pluginutils': 5.3.0(rollup@4.50.1) '@rollup/pluginutils': 5.3.0(rollup@4.50.1)
'@typescript-eslint/scope-manager': 8.42.0 '@typescript-eslint/scope-manager': 8.42.0
'@typescript-eslint/typescript-estree': 8.42.0(typescript@5.6.3) '@typescript-eslint/typescript-estree': 8.42.0(typescript@5.6.3)
@@ -5080,11 +5196,11 @@ snapshots:
- supports-color - supports-color
- typescript - 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: dependencies:
'@babel/parser': 7.28.4 '@babel/parser': 7.28.4
optionalDependencies: optionalDependencies:
'@intlify/shared': 11.4.8 '@intlify/shared': 11.4.6
'@vue/compiler-dom': 3.5.21 '@vue/compiler-dom': 3.5.21
vue: 3.5.21(typescript@5.6.3) vue: 3.5.21(typescript@5.6.3)
vue-i18n: 10.0.8(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: transitivePeerDependencies:
- '@types/node' - '@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)': '@stylistic/eslint-plugin@2.13.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3)':
dependencies: dependencies:
'@typescript-eslint/utils': 8.42.0(eslint@9.35.0(jiti@2.5.1))(typescript@5.6.3) '@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/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': '@types/debug@4.1.12':
dependencies: dependencies:
'@types/ms': 2.1.0 '@types/ms': 2.1.0
'@types/deep-eql@4.0.2':
optional: true
'@types/default-gateway@7.2.2': {} '@types/default-gateway@7.2.2': {}
'@types/estree@1.0.8': {} '@types/estree@1.0.8': {}
@@ -5711,14 +5839,14 @@ snapshots:
vite: 5.4.21(@types/node@22.18.1) vite: 5.4.21(@types/node@22.18.1)
vue: 3.5.21(typescript@5.6.3) 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: dependencies:
'@typescript-eslint/scope-manager': 8.42.0 '@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) '@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) eslint: 9.35.0(jiti@2.5.1)
optionalDependencies: optionalDependencies:
typescript: 5.6.3 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: transitivePeerDependencies:
- supports-color - supports-color
@@ -5729,6 +5857,16 @@ snapshots:
chai: 5.3.3 chai: 5.3.3
tinyrainbow: 1.2.0 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))': '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.18.1))':
dependencies: dependencies:
'@vitest/spy': 2.1.9 '@vitest/spy': 2.1.9
@@ -5737,31 +5875,69 @@ snapshots:
optionalDependencies: optionalDependencies:
vite: 5.4.21(@types/node@22.18.1) 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': '@vitest/pretty-format@2.1.9':
dependencies: dependencies:
tinyrainbow: 1.2.0 tinyrainbow: 1.2.0
'@vitest/pretty-format@4.1.9':
dependencies:
tinyrainbow: 3.1.0
optional: true
'@vitest/runner@2.1.9': '@vitest/runner@2.1.9':
dependencies: dependencies:
'@vitest/utils': 2.1.9 '@vitest/utils': 2.1.9
pathe: 1.1.2 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': '@vitest/snapshot@2.1.9':
dependencies: dependencies:
'@vitest/pretty-format': 2.1.9 '@vitest/pretty-format': 2.1.9
magic-string: 0.30.21 magic-string: 0.30.21
pathe: 1.1.2 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': '@vitest/spy@2.1.9':
dependencies: dependencies:
tinyspy: 3.0.2 tinyspy: 3.0.2
'@vitest/spy@4.1.9':
optional: true
'@vitest/utils@2.1.9': '@vitest/utils@2.1.9':
dependencies: dependencies:
'@vitest/pretty-format': 2.1.9 '@vitest/pretty-format': 2.1.9
loupe: 3.2.1 loupe: 3.2.1
tinyrainbow: 1.2.0 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': '@volar/language-core@2.4.15':
dependencies: dependencies:
'@volar/source-map': 2.4.15 '@volar/source-map': 2.4.15
@@ -6436,6 +6612,9 @@ snapshots:
loupe: 3.2.1 loupe: 3.2.1
pathval: 2.0.1 pathval: 2.0.1
chai@6.2.2:
optional: true
chalk@4.1.2: chalk@4.1.2:
dependencies: dependencies:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
@@ -6611,6 +6790,9 @@ snapshots:
es-module-lexer@1.7.0: {} es-module-lexer@1.7.0: {}
es-module-lexer@2.1.0:
optional: true
es-object-atoms@1.1.1: es-object-atoms@1.1.1:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0
@@ -7059,6 +7241,11 @@ snapshots:
dependencies: dependencies:
format: 0.2.2 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: file-entry-cache@8.0.0:
dependencies: dependencies:
flat-cache: 4.0.1 flat-cache: 4.0.1
@@ -7850,6 +8037,9 @@ snapshots:
object-hash@3.0.0: {} object-hash@3.0.0: {}
obug@2.1.3:
optional: true
onetime@6.0.0: onetime@6.0.0:
dependencies: dependencies:
mimic-fn: 4.0.0 mimic-fn: 4.0.0
@@ -7954,6 +8144,9 @@ snapshots:
picomatch@4.0.3: {} picomatch@4.0.3: {}
picomatch@4.0.4:
optional: true
pify@2.3.0: {} pify@2.3.0: {}
pinia@2.3.1(typescript@5.6.3)(vue@3.5.21(typescript@5.6.3)): 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@3.10.0: {}
std-env@4.1.0:
optional: true
string-argv@0.3.2: {} string-argv@0.3.2: {}
string-width@4.2.3: string-width@4.2.3:
@@ -8353,10 +8549,22 @@ snapshots:
tinyexec@1.0.1: {} 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: {} tinypool@1.1.1: {}
tinyrainbow@1.2.0: {} tinyrainbow@1.2.0: {}
tinyrainbow@3.1.0:
optional: true
tinyspy@3.0.2: {} tinyspy@3.0.2: {}
to-regex-range@5.0.1: to-regex-range@5.0.1:
@@ -8810,6 +9018,35 @@ snapshots:
- supports-color - supports-color
- terser - 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: {} vscode-uri@3.1.0: {}
vue-chartjs@5.3.2(chart.js@4.5.0)(vue@3.5.21(typescript@5.6.3)): vue-chartjs@5.3.2(chart.js@4.5.0)(vue@3.5.21(typescript@5.6.3)):