From c41bdfc1cf2f41933852a0de10ec4b67cf1a958e Mon Sep 17 00:00:00 2001 From: Chi-Kai Date: Fri, 11 Jul 2025 17:03:22 +0800 Subject: [PATCH] =?UTF-8?q?issue=20469:=20TQUIC=20tools=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=8A=A5=E6=96=87=E4=B8=A2=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib.rs | 1 + src/packet.rs | 2 +- tools/src/bin/tquic_client.rs | 63 ++++++++- tools/src/bin/tquic_server.rs | 68 +++++++++- tools/src/common.rs | 49 ++++++- tools/src/packet_loss.rs | 243 ++++++++++++++++++++++++++++++++++ 6 files changed, 422 insertions(+), 4 deletions(-) create mode 100644 tools/src/packet_loss.rs diff --git a/src/lib.rs b/src/lib.rs index 450588f9f..f4e02679a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1229,6 +1229,7 @@ pub use crate::error::Error; pub use crate::multipath_scheduler::MultipathAlgorithm; pub use crate::packet::PacketHeader; pub use crate::tls::CertCompressionAlgorithm; +pub use crate::packet::PacketType; pub use crate::tls::TlsConfig; pub use crate::tls::TlsConfigSelector; diff --git a/src/packet.rs b/src/packet.rs index 436115cb6..0201d86ab 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -76,7 +76,7 @@ const RETRY_INTEGRITY_NONCE_V1: [u8; aead::NONCE_LEN] = [ ]; /// QUIC packet type. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum PacketType { /// The Version Negotiation packet is a response to a client packet that /// contains a version that is not supported by the server. diff --git a/tools/src/bin/tquic_client.rs b/tools/src/bin/tquic_client.rs index 5a72abfc1..3d614f9e1 100644 --- a/tools/src/bin/tquic_client.rs +++ b/tools/src/bin/tquic_client.rs @@ -68,6 +68,8 @@ use tquic::TransportHandler; use tquic::CertCompressionAlgorithm; use tquic_tools::ApplicationProto; use tquic_tools::CertCompressionAlgorithmArg; +use tquic_tools::LossPacketType; +use tquic_tools::PacketLossConfig; use tquic_tools::QuicSocket; use tquic_tools::Result; @@ -340,6 +342,40 @@ pub struct ClientOpt { /// The range of the request, like "0-1023". #[clap(long, value_name = "RANGE", help_heading = "Protocol")] pub range: Option, + /// Packet loss rate (0.0 to 1.0) for testing. + #[clap( + long, + default_value = "0.0", + value_name = "RATE", + help_heading = "Packet Loss" + )] + pub packet_loss_rate: f64, + + /// Specific packet numbers to drop (comma-separated). + #[clap( + long, + value_delimiter = ',', + value_name = "NUMBERS", + help_heading = "Packet Loss" + )] + pub packet_loss_numbers: Vec, + + /// Packet types to drop. + #[clap( + long, + value_delimiter = ',', + value_name = "TYPES", + help_heading = "Packet Loss" + )] + pub packet_loss_types: Vec, + + /// Apply packet loss to incoming packets. + #[clap(long, help_heading = "Packet Loss")] + pub packet_loss_incoming: bool, + + /// Apply packet loss to outgoing packets. + #[clap(long, help_heading = "Packet Loss")] + pub packet_loss_outgoing: bool, } const MAX_BUF_SIZE: usize = 65536; @@ -596,7 +632,32 @@ impl Worker { false => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0), } }; - let mut sock = QuicSocket::new(&local, registry)?; + + // Create packet loss configuration if needed + let packet_loss_config = if option.packet_loss_rate > 0.0 + || !option.packet_loss_numbers.is_empty() + || !option.packet_loss_types.is_empty() + { + let mut config = PacketLossConfig::new() + .with_loss_rate(option.packet_loss_rate) + .with_drop_packet_numbers(option.packet_loss_numbers.clone()) + .with_drop_incoming(option.packet_loss_incoming) + .with_drop_outgoing(option.packet_loss_outgoing); + + let packet_types: Vec<_> = option + .packet_loss_types + .iter() + .map(|t| t.to_packet_type()) + .collect(); + config = config.with_drop_packet_types(packet_types); + + Some(config) + } else { + None + }; + + let mut sock = + QuicSocket::with_packet_loss(&local, registry, packet_loss_config, option.cid_len)?; let mut assigned_addrs = Vec::new(); assigned_addrs.push(sock.local_addr()); diff --git a/tools/src/bin/tquic_server.rs b/tools/src/bin/tquic_server.rs index 1867b50a9..707091677 100644 --- a/tools/src/bin/tquic_server.rs +++ b/tools/src/bin/tquic_server.rs @@ -48,6 +48,8 @@ use tquic::TransportHandler; use tquic::CertCompressionAlgorithm; use tquic_tools::ApplicationProto; use tquic_tools::CertCompressionAlgorithmArg; +use tquic_tools::LossPacketType; +use tquic_tools::PacketLossConfig; use tquic_tools::QuicSocket; use tquic_tools::Result; @@ -257,6 +259,41 @@ pub struct ServerOpt { /// Disable encryption on 1-RTT packets. #[clap(long, help_heading = "Misc")] pub disable_encryption: bool, + + /// Packet loss rate (0.0 to 1.0) for testing. + #[clap( + long, + default_value = "0.0", + value_name = "RATE", + help_heading = "Packet Loss" + )] + pub packet_loss_rate: f64, + + /// Specific packet numbers to drop (comma-separated). + #[clap( + long, + value_delimiter = ',', + value_name = "NUMBERS", + help_heading = "Packet Loss" + )] + pub packet_loss_numbers: Vec, + + /// Packet types to drop. + #[clap( + long, + value_delimiter = ',', + value_name = "TYPES", + help_heading = "Packet Loss" + )] + pub packet_loss_types: Vec, + + /// Apply packet loss to incoming packets. + #[clap(long, help_heading = "Packet Loss")] + pub packet_loss_incoming: bool, + + /// Apply packet loss to outgoing packets. + #[clap(long, help_heading = "Packet Loss")] + pub packet_loss_outgoing: bool, } const MAX_BUF_SIZE: usize = 65536; @@ -340,7 +377,36 @@ impl Server { let registry = poll.registry(); let handlers = ServerHandler::new(option)?; - let sock = Rc::new(QuicSocket::new(&option.listen, registry)?); + + // Create packet loss configuration if needed + let packet_loss_config = if option.packet_loss_rate > 0.0 + || !option.packet_loss_numbers.is_empty() + || !option.packet_loss_types.is_empty() + { + let mut config = PacketLossConfig::new() + .with_loss_rate(option.packet_loss_rate) + .with_drop_packet_numbers(option.packet_loss_numbers.clone()) + .with_drop_incoming(option.packet_loss_incoming) + .with_drop_outgoing(option.packet_loss_outgoing); + + let packet_types: Vec<_> = option + .packet_loss_types + .iter() + .map(|t| t.to_packet_type()) + .collect(); + config = config.with_drop_packet_types(packet_types); + + Some(config) + } else { + None + }; + + let sock = Rc::new(QuicSocket::with_packet_loss( + &option.listen, + registry, + packet_loss_config, + option.cid_len, + )?); Ok(Server { endpoint: Endpoint::new(Box::new(config), true, Box::new(handlers), sock.clone()), diff --git a/tools/src/common.rs b/tools/src/common.rs index 01f6e605c..893898857 100644 --- a/tools/src/common.rs +++ b/tools/src/common.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::cell::RefCell; use std::io::ErrorKind; use std::net::SocketAddr; @@ -30,6 +31,9 @@ use tquic::PacketInfo; use tquic::PacketSendHandler; use tquic::CertCompressionAlgorithm; +pub mod packet_loss; +pub use packet_loss::{LossPacketType, PacketLossConfig, PacketLossSimulator}; + pub type Result = std::result::Result>; /// Certificate compression algorithm for clap parsing @@ -120,10 +124,25 @@ pub struct QuicSocket { /// Local address of the initial socket. local_addr: SocketAddr, + + /// Packet loss simulator for testing + packet_loss: RefCell>, + + /// Connection ID length for packet parsing + dcid_len: usize, } impl QuicSocket { pub fn new(local: &SocketAddr, registry: &Registry) -> Result { + Self::with_packet_loss(local, registry, None, 8) + } + + pub fn with_packet_loss( + local: &SocketAddr, + registry: &Registry, + packet_loss_config: Option, + dcid_len: usize, + ) -> Result { let mut socks = Slab::new(); let mut addrs = FxHashMap::default(); @@ -135,10 +154,14 @@ impl QuicSocket { let socket = socks.get_mut(sid).unwrap(); registry.register(socket, Token(sid), Interest::READABLE)?; + let packet_loss = RefCell::new(packet_loss_config.map(PacketLossSimulator::new)); + Ok(Self { socks, addrs, local_addr, + packet_loss, + dcid_len, }) } @@ -188,7 +211,21 @@ impl QuicSocket { }; match socket.recv_from(buf) { - Ok((len, remote)) => Ok((len, socket.local_addr()?, remote)), + Ok((len, remote)) => { + // Check if packet should be dropped due to loss simulation + if let Ok(mut packet_loss) = self.packet_loss.try_borrow_mut() { + if let Some(ref mut simulator) = *packet_loss { + if simulator.should_drop_incoming(&buf[..len], self.dcid_len) { + debug!("Simulating incoming packet loss - dropping packet"); + return Err(std::io::Error::new( + ErrorKind::WouldBlock, + "simulated packet loss", + )); + } + } + } + Ok((len, socket.local_addr()?, remote)) + } Err(e) => Err(e), } } @@ -196,6 +233,16 @@ impl QuicSocket { /// Send data on the socket to the given address. /// Note: packets with unknown src address are dropped. pub fn send_to(&self, buf: &[u8], src: SocketAddr, dst: SocketAddr) -> std::io::Result { + // Check if packet should be dropped due to loss simulation + if let Ok(mut packet_loss) = self.packet_loss.try_borrow_mut() { + if let Some(ref mut simulator) = *packet_loss { + if simulator.should_drop_outgoing(buf, self.dcid_len) { + debug!("Simulating outgoing packet loss - dropping packet"); + return Ok(buf.len()); // Pretend we sent it + } + } + } + let sid = match self.addrs.get(&src) { Some(sid) => sid, None => { diff --git a/tools/src/packet_loss.rs b/tools/src/packet_loss.rs new file mode 100644 index 000000000..19e811934 --- /dev/null +++ b/tools/src/packet_loss.rs @@ -0,0 +1,243 @@ +// Copyright (c) 2023 The TQUIC Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashSet; + +use clap::builder::PossibleValue; +use clap::ValueEnum; +use log::debug; +use rand::Rng; + +use tquic::PacketHeader; +use tquic::PacketType; + +/// Packet loss strategy configuration +#[derive(Clone, Debug, Default)] +pub struct PacketLossConfig { + /// Random packet loss rate (0.0 to 1.0) + pub loss_rate: f64, + /// Specific packet numbers to drop + pub drop_packet_numbers: HashSet, + /// Packet types to drop + pub drop_packet_types: HashSet, + /// Whether to apply loss to incoming packets + pub drop_incoming: bool, + /// Whether to apply loss to outgoing packets + pub drop_outgoing: bool, +} + +impl PacketLossConfig { + /// Create a new PacketLossConfig with default values + pub fn new() -> Self { + Self { + loss_rate: 0.0, + drop_packet_numbers: HashSet::new(), + drop_packet_types: HashSet::new(), + drop_incoming: true, + drop_outgoing: true, + } + } + + /// Set random loss rate + pub fn with_loss_rate(mut self, rate: f64) -> Self { + self.loss_rate = rate.clamp(0.0, 1.0); + self + } + + /// Add specific packet numbers to drop + pub fn with_drop_packet_numbers(mut self, numbers: Vec) -> Self { + self.drop_packet_numbers.extend(numbers); + self + } + + /// Add specific packet types to drop + pub fn with_drop_packet_types(mut self, types: Vec) -> Self { + self.drop_packet_types.extend(types); + self + } + + /// Set incoming packet loss + pub fn with_drop_incoming(mut self, drop: bool) -> Self { + self.drop_incoming = drop; + self + } + + /// Set outgoing packet loss + pub fn with_drop_outgoing(mut self, drop: bool) -> Self { + self.drop_outgoing = drop; + self + } +} + +/// Packet loss simulator +#[derive(Debug)] +pub struct PacketLossSimulator { + config: PacketLossConfig, + rng: rand::rngs::ThreadRng, +} + +impl PacketLossSimulator { + /// Create a new PacketLossSimulator + pub fn new(config: PacketLossConfig) -> Self { + Self { + config, + rng: rand::thread_rng(), + } + } + + /// Check if an incoming packet should be dropped + pub fn should_drop_incoming(&mut self, packet_data: &[u8], dcid_len: usize) -> bool { + if !self.config.drop_incoming { + return false; + } + self.should_drop_packet(packet_data, dcid_len) + } + + /// Check if an outgoing packet should be dropped + pub fn should_drop_outgoing(&mut self, packet_data: &[u8], dcid_len: usize) -> bool { + if !self.config.drop_outgoing { + return false; + } + self.should_drop_packet(packet_data, dcid_len) + } + + /// Internal method to determine if a packet should be dropped + fn should_drop_packet(&mut self, packet_data: &[u8], dcid_len: usize) -> bool { + // Try to parse packet header + let (header, _) = match PacketHeader::from_bytes(packet_data, dcid_len) { + Ok(result) => result, + Err(e) => { + debug!("Failed to parse packet header for loss simulation: {:?}", e); + return false; + } + }; + + // Check packet type filter + if self.config.drop_packet_types.contains(&header.pkt_type) { + debug!("Dropping packet due to type filter: {:?}", header.pkt_type); + return true; + } + + // Check packet number filter + if self.config.drop_packet_numbers.contains(&header.pkt_num) { + debug!( + "Dropping packet due to packet number filter: {}", + header.pkt_num + ); + return true; + } + + // Check random loss rate + if self.config.loss_rate > 0.0 { + let random_value: f64 = self.rng.gen(); + if random_value < self.config.loss_rate { + debug!( + "Dropping packet due to random loss (rate: {}, value: {})", + self.config.loss_rate, random_value + ); + return true; + } + } + + false + } +} + +/// Supported packet loss types for CLI +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LossPacketType { + Initial, + ZeroRTT, + Handshake, + OneRTT, + Retry, + VersionNegotiation, +} + +impl LossPacketType { + /// Convert to TQUIC PacketType + pub fn to_packet_type(&self) -> PacketType { + match self { + Self::Initial => PacketType::Initial, + Self::ZeroRTT => PacketType::ZeroRTT, + Self::Handshake => PacketType::Handshake, + Self::OneRTT => PacketType::OneRTT, + Self::Retry => PacketType::Retry, + Self::VersionNegotiation => PacketType::VersionNegotiation, + } + } +} + +impl ValueEnum for LossPacketType { + fn to_possible_value(&self) -> Option { + Some(match self { + Self::Initial => PossibleValue::new("initial"), + Self::ZeroRTT => PossibleValue::new("0rtt"), + Self::Handshake => PossibleValue::new("handshake"), + Self::OneRTT => PossibleValue::new("1rtt"), + Self::Retry => PossibleValue::new("retry"), + Self::VersionNegotiation => PossibleValue::new("version_negotiation"), + }) + } + + fn value_variants<'a>() -> &'a [Self] { + &[ + Self::Initial, + Self::ZeroRTT, + Self::Handshake, + Self::OneRTT, + Self::Retry, + Self::VersionNegotiation, + ] + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_packet_loss_config_creation() { + let config = PacketLossConfig::new() + .with_loss_rate(0.1) + .with_drop_packet_numbers(vec![1, 2, 3]) + .with_drop_packet_types(vec![PacketType::Initial]) + .with_drop_incoming(true) + .with_drop_outgoing(false); + + assert_eq!(config.loss_rate, 0.1); + assert!(config.drop_packet_numbers.contains(&1)); + assert!(config.drop_packet_types.contains(&PacketType::Initial)); + assert!(config.drop_incoming); + assert!(!config.drop_outgoing); + } + + #[test] + fn test_loss_rate_clamping() { + let config1 = PacketLossConfig::new().with_loss_rate(-0.5); + assert_eq!(config1.loss_rate, 0.0); + + let config2 = PacketLossConfig::new().with_loss_rate(1.5); + assert_eq!(config2.loss_rate, 1.0); + } + + #[test] + fn test_loss_packet_type_conversion() { + assert_eq!( + LossPacketType::Initial.to_packet_type(), + PacketType::Initial + ); + assert_eq!(LossPacketType::OneRTT.to_packet_type(), PacketType::OneRTT); + } +}