diff --git a/src/connection/connection.rs b/src/connection/connection.rs index 81559a68..7d0a878e 100644 --- a/src/connection/connection.rs +++ b/src/connection/connection.rs @@ -39,6 +39,8 @@ use self::ConnectionFlags::*; use crate::codec; use crate::codec::Decoder; use crate::codec::Encoder; +use crate::connection::space::BufferFlags; +use crate::connection::space::BufferType; use crate::connection::space::RateSamplePacketState; use crate::error::ConnectionError; use crate::error::Error; @@ -65,6 +67,7 @@ use crate::FourTuple; use crate::FourTupleIter; use crate::MultipathConfig; use crate::PacketInfo; +use crate::PathEvent; use crate::RecoveryConfig; use crate::Result; use crate::Shutdown; @@ -846,7 +849,12 @@ impl Connection { } Frame::PathResponse { data } => { - self.paths.on_path_resp_received(path_id, data); + if self.paths.on_path_resp_received(path_id, data) { + // Notify the path event to the multipath scheduler + if let Some(ref mut scheduler) = self.multipath_scheduler { + scheduler.on_path_updated(&mut self.paths, PathEvent::Validated(path_id)); + } + } } frame::Frame::PathAbandon { @@ -1581,7 +1589,7 @@ impl Connection { has_data: write_status.has_data, frames: write_status.frames, rate_sample_state: Default::default(), - reinjected: write_status.reinjected, + buffer_flags: write_status.buffer_flags, }; debug!( "{} sent packet {:?} {:?} {:?}", @@ -1731,8 +1739,8 @@ impl Connection { // Write a CRYPTO frame self.try_write_crypto_frame(out, st, pkt_type, path_id)?; - // Write reinjected frames - self.try_write_reinjected_frames(out, st, pkt_type, path_id)?; + // Write buffered frames + self.try_write_buffered_frames(out, st, pkt_type, path_id)?; // Write STREAM frames self.try_write_stream_frames(out, st, pkt_type, path_id)?; @@ -2228,9 +2236,9 @@ impl Connection { // Read stream data and write into the packet buffer directly. let (frame_data_len, fin) = stream.send.read(&mut out[len + frame_hdr_len..])?; - // Retain stream data for reinjection if needed. + // Retain stream data if needed. let data = if self.flags.contains(EnableMultipath) - && reinjection_required(self.multipath_conf.multipath_algorithm) + && buffer_required(self.multipath_conf.multipath_algorithm) { let start = len + frame_hdr_len; Bytes::copy_from_slice(&out[start..start + frame_data_len]) @@ -2306,8 +2314,8 @@ impl Connection { Ok(()) } - /// Populate reinjected frame to packet payload buffer. - fn try_write_reinjected_frames( + /// Populate buffered frame to packet payload buffer. + fn try_write_buffered_frames( &mut self, out: &mut [u8], st: &mut FrameWriteStatus, @@ -2318,33 +2326,31 @@ impl Connection { return Ok(()); } - let out = &mut out[st.written..]; let path = self.paths.get(path_id)?; if pkt_type != PacketType::OneRTT || self.is_closing() - || out.len() <= frame::MAX_STREAM_OVERHEAD + || out.len() - st.written <= frame::MAX_STREAM_OVERHEAD || !path.active() { return Ok(()); } - // Get reinjected frames on the path. + // Get buffered frames on the path. let space = self .spaces .get_mut(path.space_id) .ok_or(Error::InternalError)?; - let frames = &mut space.reinject.frames; + if space.buffered.is_empty() { + return Ok(()); + } debug!( - "{} try to write reinjected frames: path_id={} reinjected_frames={}", + "{} try to write buffered frames: path_id={} frames={}", self.trace_id, path_id, - frames.len() + space.buffered.len() ); - if frames.is_empty() { - return Ok(()); - } - while let Some(frame) = frames.pop_front() { + while let Some((frame, buffer_type)) = space.buffered.pop_front() { match frame { Frame::Stream { stream_id, @@ -2358,65 +2364,74 @@ impl Connection { _ => continue, }; - // Check acked range and injected the first non-acked subrange + // Check acked range and write the first non-acked subrange let range = offset..offset + length as u64; if let Some(r) = stream.send.filter_acked(range) { - let data_len = (r.end - r.start) as usize; - let frame_len = Self::reinjected_stream_frame_to_packet( + let data_len = Self::write_buffered_stream_frame_to_packet( stream_id, r.start, - data_len, - fin && data_len == length, + fin && r.end == offset + length as u64, data.slice((r.start - offset) as usize..(r.end - offset) as usize), out, + buffer_type, st, )?; // Processing the following subrange. - if r.end < offset + length as u64 { + if r.start + (data_len as u64) < offset + length as u64 { let frame = Frame::Stream { stream_id, - offset: r.end, - length: length - (r.end - offset) as usize, + offset: r.start + data_len as u64, + length: length - data_len, fin, - data: data.slice((r.end - offset) as usize..), + data: data.slice(data_len..), }; - frames.push_front(frame); + space.buffered.push_front(frame, buffer_type); + } + + if data_len == 0 { + break; } } } - // Ignore other reinjected frames. + // Ignore other buffered frames. _ => continue, } - - let out = &mut out[st.written..]; - if out.len() <= frame::MAX_STREAM_OVERHEAD { - break; - } } Ok(()) } - fn reinjected_stream_frame_to_packet( + fn write_buffered_stream_frame_to_packet( stream_id: u64, offset: u64, - length: usize, - fin: bool, - data: Bytes, + mut fin: bool, + mut data: Bytes, out: &mut [u8], + buffer_type: BufferType, st: &mut FrameWriteStatus, ) -> Result { - let len = frame::encode_stream_header(stream_id, offset, length as u64, fin, out)?; - out[len..len + data.len()].copy_from_slice(&data); + let out = &mut out[st.written..]; + if out.len() <= frame::MAX_STREAM_OVERHEAD { + return Ok(0); + } + + let hdr_len = frame::stream_header_wire_len(stream_id, offset); + let data_len = cmp::min(data.len(), out.len() - hdr_len); + if data_len < data.len() { + data.truncate(data_len); + fin = false; + } + + frame::encode_stream_header(stream_id, offset, data_len as u64, fin, out)?; + out[hdr_len..hdr_len + data.len()].copy_from_slice(&data); - let frame_len = len + data.len(); - st.written += frame_len; + st.written += hdr_len + data_len; st.ack_eliciting = true; st.in_flight = true; st.has_data = true; - st.reinjected = true; + st.buffer_flags.mark(buffer_type); st.frames.push(Frame::Stream { stream_id, offset, @@ -2424,7 +2439,7 @@ impl Connection { fin, data, }); - Ok(frame_len) + Ok(data_len) } /// Populate a QUIC frame to the give buffer. @@ -2623,7 +2638,7 @@ impl Connection { } } - // Select a validated path with ACK/PTO/Reinjected packets to send. + // Select a validated path with ACK/PTO/Buffered packets to send. for (pid, path) in self.paths.iter_mut() { if !path.active() { continue; @@ -2636,7 +2651,7 @@ impl Connection { if space.loss_probes > 0 { return Ok(pid); } - if space.need_send_reinjected_frames() && path.recovery.can_send() { + if space.need_send_buffered_frames() && path.recovery.can_send() { return Ok(pid); } continue; @@ -2732,7 +2747,7 @@ impl Connection { || path.need_send_validation_frames() || self.cids.need_send_cid_control_frames() || self.streams.need_send_stream_frames() - || self.spaces.need_send_reinjected_frames()) + || self.spaces.need_send_buffered_frames()) { if !self.is_server && self.tls_session.is_in_early_data() { return Ok(PacketType::ZeroRTT); @@ -3972,8 +3987,8 @@ struct FrameWriteStatus { /// Whether the congestion window should be ignored. is_probe: bool, - /// Whether it is a reinjected packet. - reinjected: bool, + /// Status about buffered frames written to the packet. + buffer_flags: BufferFlags, } /// Handshake status for loss recovery diff --git a/src/connection/path.rs b/src/connection/path.rs index 0363fa17..7e00a48a 100644 --- a/src/connection/path.rs +++ b/src/connection/path.rs @@ -35,25 +35,6 @@ pub(crate) const INITIAL_CHAL_TIMEOUT: u64 = 25; pub(crate) const MAX_PROBING_TIMEOUTS: usize = 8; -/// The states about the path validation. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum PathState { - /// The path validation failed - Failed, - - /// No path validation has been performed. - Unknown, - - /// The path is under validation. - Validating, - - /// The remote address has been validated, but not the path MTU. - ValidatingMTU, - - /// The path has been validated. - Validated, -} - /// A network path on which QUIC packets can be sent. pub struct Path { /// The local address. @@ -173,9 +154,10 @@ impl Path { } /// Handle incoming PATH_RESPONSE data. - pub(super) fn on_path_resp_received(&mut self, data: [u8; 8], multipath: bool) { + /// Return true if the path status changes to `Validated`. + pub(super) fn on_path_resp_received(&mut self, data: [u8; 8], multipath: bool) -> bool { if self.state == PathState::Validated { - return; + return false; } self.verified_peer_address = true; @@ -199,11 +181,12 @@ impl Path { self.promote_to(PathState::Validated); self.set_active(multipath); self.sent_chals.clear(); - return; + return true; } // If the MTU was not validated, probe again. self.need_send_challenge = true; + false } /// Fetch a received challenge data item. @@ -318,6 +301,25 @@ impl std::fmt::Debug for Path { } } +/// The states about the path validation. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum PathState { + /// The path validation failed + Failed, + + /// No path validation has been performed. + Unknown, + + /// The path is under validation. + Validating, + + /// The remote address has been validated, but not the path MTU. + ValidatingMTU, + + /// The path has been validated. + Validated, +} + /// Path manager for a QUIC connection pub(crate) struct PathMap { /// The paths of the connection. Each path has a path identifier @@ -451,10 +453,11 @@ impl PathMap { } /// Process a PATH_RESPONSE frame on the give path - pub fn on_path_resp_received(&mut self, path_id: usize, data: [u8; 8]) { + pub fn on_path_resp_received(&mut self, path_id: usize, data: [u8; 8]) -> bool { if let Some(path) = self.paths.get_mut(path_id) { - path.on_path_resp_received(data, self.is_multipath); + return path.on_path_resp_received(data, self.is_multipath); } + false } /// Handle the sent event of PATH_CHALLENGE. @@ -600,11 +603,11 @@ mod tests { assert_eq!(path_mgr.get_mut(pid)?.state, PathState::Validating); // Fake receiving of unmatched PATH_RESPONSE - path_mgr.on_path_resp_received(pid, [0xab; 8]); + assert_eq!(path_mgr.on_path_resp_received(pid, [0xab; 8]), false); assert_eq!(path_mgr.get_mut(pid)?.state, PathState::ValidatingMTU); // Fake receiving of PATH_RESPONSE - path_mgr.on_path_resp_received(pid, data); + assert_eq!(path_mgr.on_path_resp_received(pid, data), false); assert_eq!(path_mgr.get_mut(pid)?.path_chal_initiated(), true); assert_eq!(path_mgr.get_mut(pid)?.validated(), false); assert_eq!(path_mgr.get_mut(pid)?.state, PathState::ValidatingMTU); @@ -613,14 +616,14 @@ mod tests { path_mgr.on_path_chal_sent(pid, data, 1300, now)?; // Fake receiving of PATH_RESPONSE - path_mgr.on_path_resp_received(pid, data); + assert_eq!(path_mgr.on_path_resp_received(pid, data), true); assert_eq!(path_mgr.get_mut(pid)?.path_chal_initiated(), false); assert_eq!(path_mgr.get_mut(pid)?.validated(), true); assert_eq!(path_mgr.get_mut(pid)?.state, PathState::Validated); assert_eq!(path_mgr.get_mut(pid)?.sent_chals.len(), 0); // Fake receiving of depulicated PATH_RESPONSE - path_mgr.on_path_resp_received(pid, data); + assert_eq!(path_mgr.on_path_resp_received(pid, data), false); assert_eq!(path_mgr.get_mut(pid)?.validated(), true); // Timeout event diff --git a/src/connection/space.rs b/src/connection/space.rs index e52900e3..26a19fef 100644 --- a/src/connection/space.rs +++ b/src/connection/space.rs @@ -102,6 +102,9 @@ pub struct PacketNumSpace { /// Acknowledged frames. pub acked: Vec, + /// Buffered frames to be sent in multipath mode. + pub buffered: BufferQueue, + /// The time the most recent ack-eliciting packet was sent. pub time_of_last_sent_ack_eliciting_pkt: Option, @@ -124,9 +127,6 @@ pub struct PacketNumSpace { /// Packet number space for application data pub is_data: bool, - - /// Reinjected frames to be sent. - pub reinject: ReinjectQueue, } impl PacketNumSpace { @@ -145,6 +145,7 @@ impl PacketNumSpace { sent: VecDeque::new(), lost: Vec::new(), acked: Vec::new(), + buffered: BufferQueue::default(), time_of_last_sent_ack_eliciting_pkt: None, loss_time: None, largest_acked_pkt: std::u64::MAX, @@ -152,7 +153,6 @@ impl PacketNumSpace { bytes_in_flight: 0, ack_eliciting_in_flight: 0, is_data: id != SpaceId::Initial && id != SpaceId::Handshake, - reinject: ReinjectQueue::default(), } } @@ -187,9 +187,9 @@ impl PacketNumSpace { self.loss_probes > 0 } - /// Return whether the space should send a reinjection packet. - pub fn need_send_reinjected_frames(&self) -> bool { - !self.reinject.frames.is_empty() + /// Return whether the space should send a buffered packet. + pub fn need_send_buffered_frames(&self) -> bool { + !self.buffered.is_empty() } } @@ -259,10 +259,10 @@ impl PacketNumSpaceMap { }; } - /// Return whether the connection should send a reinjection packet. - pub fn need_send_reinjected_frames(&self) -> bool { + /// Return whether the connection should send a buffered packet. + pub fn need_send_buffered_frames(&self) -> bool { for space in self.spaces.values() { - if space.need_send_reinjected_frames() { + if space.need_send_buffered_frames() { return true; } } @@ -297,8 +297,6 @@ pub struct RateSamplePacketState { /// packet.lost: The volume of data that was declared lost on transmission. pub lost: u64, - // P.sent_time: The time when the packet was sent. (Use time_sent in SentPacket) - // sent_time: Instant, } /// Metadata of sent packet @@ -341,8 +339,8 @@ pub struct SentPacket { /// Snapshot of the current delivery information. pub rate_sample_state: RateSamplePacketState, - /// Whether it is a reinjected packet. - pub reinjected: bool, + /// Status about buffered frames written into the packet. + pub buffer_flags: BufferFlags, } impl Default for SentPacket { @@ -359,7 +357,7 @@ impl Default for SentPacket { has_data: false, sent_size: 0, rate_sample_state: RateSamplePacketState::default(), - reinjected: false, + buffer_flags: BufferFlags::default(), } } } @@ -386,16 +384,96 @@ pub struct AckedPacket { pub rtt: Duration, } -/// Metadata of packets to be reinjected +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum BufferType { + High = 0, + Mid = 1, + Low = 2, +} + +impl From for BufferType { + fn from(index: usize) -> BufferType { + match index { + 0 => BufferType::High, + 1 => BufferType::Mid, + _ => BufferType::Low, + } + } +} + +/// Metadata of buffered packets to be sent #[derive(Default)] -pub struct ReinjectQueue { - /// The reinjected frames to be sent. - pub frames: VecDeque, +pub struct BufferQueue { + queues: [VecDeque; 3], + count: usize, +} + +impl BufferQueue { + /// Remove the first frame and returns it + pub fn pop_front(&mut self) -> Option<(frame::Frame, BufferType)> { + for (i, queue) in self.queues.iter_mut().enumerate() { + if !queue.is_empty() { + self.count -= 1; + return Some((queue.pop_front().unwrap(), BufferType::from(i))); + } + } + None + } + + /// Prepend a frame to the specified queue. + pub fn push_front(&mut self, frame: frame::Frame, queue_type: BufferType) { + self.count += 1; + self.queues[queue_type as usize].push_front(frame) + } + + /// Append a frame to the back of the queue. + pub fn push_back(&mut self, frame: frame::Frame, queue_type: BufferType) { + self.count += 1; + self.queues[queue_type as usize].push_back(frame) + } + + /// Move all the frames into self. + pub fn append(&mut self, frames: &mut VecDeque, queue_type: BufferType) { + self.count += frames.len(); + self.queues[queue_type as usize].append(frames) + } + + /// Return the number of frames in the queue. + pub fn len(&self) -> usize { + self.count + } + + /// Return true if the queue is empty. + pub fn is_empty(&self) -> bool { + self.count == 0 + } +} + +#[derive(Clone, Default, Debug)] +pub struct BufferFlags { + pub from_high: bool, + pub from_mid: bool, + pub from_low: bool, +} + +impl BufferFlags { + pub fn has_buffered(&self) -> bool { + self.from_high || self.from_mid || self.from_low + } + + pub fn mark(&mut self, queue_type: BufferType) { + match queue_type { + BufferType::High => self.from_high = true, + BufferType::Mid => self.from_mid = true, + BufferType::Low => self.from_low = true, + } + } } #[cfg(test)] mod tests { use super::*; + use crate::frame::Frame; #[test] fn initial_spaces() { @@ -474,4 +552,70 @@ mod tests { "pn=9 frames=[PING, PADDINGS len=200] sent_size=240" ); } + + #[test] + fn buffer_queue() { + // initial queue + let mut queue = BufferQueue::default(); + assert_eq!(queue.len(), 0); + assert_eq!(queue.is_empty(), true); + + // push back/push front + let f1 = Frame::MaxStreamData { + stream_id: 4, + max: 10240, + }; + queue.push_back(f1.clone(), BufferType::High); + assert_eq!(queue.len(), 1); + assert_eq!(queue.is_empty(), false); + + let f2 = Frame::MaxStreamData { + stream_id: 8, + max: 24000, + }; + queue.push_front(f2.clone(), BufferType::High); + assert_eq!(queue.len(), 2); + assert_eq!(queue.is_empty(), false); + + let f3 = Frame::Ping; + queue.push_back(f3.clone(), BufferType::Low); + + assert_eq!(queue.pop_front(), Some((f2.clone(), BufferType::High))); + assert_eq!(queue.pop_front(), Some((f1.clone(), BufferType::High))); + assert_eq!(queue.pop_front(), Some((f3.clone(), BufferType::Low))); + assert_eq!(queue.pop_front(), None); + assert_eq!(queue.is_empty(), true); + + // append + let mut fs = VecDeque::new(); + fs.push_back(f1.clone()); + fs.push_back(f2.clone()); + queue.append(&mut fs, BufferType::Mid); + assert_eq!(queue.len(), 2); + assert_eq!(fs.len(), 0); + assert_eq!(queue.pop_front(), Some((f1.clone(), BufferType::Mid))); + assert_eq!(queue.pop_front(), Some((f2.clone(), BufferType::Mid))); + } + + #[test] + fn buffer_flags() { + use BufferType::*; + let cases = [ + (vec![], false), + (vec![High], true), + (vec![Mid], true), + (vec![Low], true), + (vec![Low, High], true), + (vec![Low, Mid], true), + (vec![High, Mid], true), + (vec![High, Mid, Low], true), + ]; + for case in cases { + let mut flags = BufferFlags::default(); + for flag in case.0 { + flags.mark(flag); + } + assert_eq!(flags.has_buffered(), case.1); + } + } } diff --git a/src/lib.rs b/src/lib.rs index 936b5360..f48ccfe0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -867,6 +867,15 @@ pub enum Shutdown { Write = 1, } +/// Important events about path +pub enum PathEvent { + /// The path has been validated. + Validated(usize), + + /// The path has been abandoned. + Abandoned(usize), +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/multipath_scheduler/multipath_scheduler.rs b/src/multipath_scheduler/multipath_scheduler.rs index 6f2faf8d..1e3e284d 100644 --- a/src/multipath_scheduler/multipath_scheduler.rs +++ b/src/multipath_scheduler/multipath_scheduler.rs @@ -26,6 +26,7 @@ use crate::connection::space::SentPacket; use crate::connection::stream::StreamMap; use crate::Error; use crate::MultipathConfig; +use crate::PathEvent; use crate::Result; /// MultipathScheduler is a packet scheduler that decides the path over which @@ -53,6 +54,9 @@ pub(crate) trait MultipathScheduler { streams: &mut StreamMap, ) { } + + /// Process a path event. + fn on_path_updated(&mut self, paths: &mut PathMap, event: PathEvent) {} } /// Available multipath scheduling algorithms. @@ -106,7 +110,7 @@ pub(crate) fn build_multipath_scheduler(conf: &MultipathConfig) -> Box bool { +pub(crate) fn buffer_required(algor: MultipathAlgorithm) -> bool { match algor { MultipathAlgorithm::MinRtt => false, MultipathAlgorithm::Redundant => true, diff --git a/src/multipath_scheduler/scheduler_redundant.rs b/src/multipath_scheduler/scheduler_redundant.rs index b88000d9..d089cc47 100644 --- a/src/multipath_scheduler/scheduler_redundant.rs +++ b/src/multipath_scheduler/scheduler_redundant.rs @@ -16,6 +16,7 @@ use log::*; use std::time::Instant; use crate::connection::path::PathMap; +use crate::connection::space::BufferType; use crate::connection::space::PacketNumSpaceMap; use crate::connection::space::SentPacket; use crate::connection::stream::StreamMap; @@ -68,7 +69,7 @@ impl MultipathScheduler for RedundantScheduler { spaces: &mut PacketNumSpaceMap, streams: &mut StreamMap, ) { - if packet.reinjected { + if packet.buffer_flags.has_buffered() { return; } @@ -84,7 +85,7 @@ impl MultipathScheduler for RedundantScheduler { for frame in &packet.frames { if let Frame::Stream { .. } = frame { debug!("RedundantScheduler: inject {:?} on path {:?}", frame, pid); - space.reinject.frames.push_back(frame.clone()); + space.buffered.push_back(frame.clone(), BufferType::High); } } } diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 00000000..5c997241 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,25 @@ +# TQUIC tools + +[TQUIC](https://github.com/Tencent/tquic) is a high-performance, lightweight, and cross-platform library for the [IETF QUIC](https://datatracker.ietf.org/wg/quic/bout/) protocol. + +The crate contains client and server tools based on TQUIC: +- tquic_client: A QUIC and HTTP/3 client. It's also an HTTP/3 benchmarking tool. +- tquic_server: A QUIC and HTTP/3 static file server. + + +## Installation + +``` +cargo install tquic_tools +``` + + +## Documentation + +- [English version](https://tquic.net/docs/getting_started/demo/) +- [Chinese version](https://tquic.net/zh/docs/getting_started/demo/) + + +## License + +The project is under the Apache 2.0 license. diff --git a/tools/tests/tquic_tools_test.sh b/tools/tests/tquic_tools_test.sh new file mode 100755 index 00000000..d0162708 --- /dev/null +++ b/tools/tests/tquic_tools_test.sh @@ -0,0 +1,139 @@ +#!/bin/bash + +# Copyright (c) 2024 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. + +# This simple scripts contains additional end-to-end test cases for tquic tools. +# When conditions permit, it's plan to implement all of the following test cases +# in the `github.com/tquic-group/quic-interop-runner` repo. + +set -e + +BIN_DIR="./" +TEST_DIR="./test-`date +%Y%m%d%H%M%S`" +TEST_CASES="multipath_minrtt,multipath_roundrobin,multipath_redundant" + +show_help() { + echo "Usage: $0 [options]" + echo " -b, Set the directory of tquic_client/tquic_server." + echo " -w, Set the workring directory for testing." + echo " -l, List all supported test cases." + echo " -t, Run the specified test cases." + echo " -h, Display this help and exit." +} + +while getopts ":b:w:t:lh" opt; do + case $opt in + b) + BIN_DIR="$OPTARG" + ;; + w) + TEST_DIR="$OPTARG" + ;; + t) + TEST_CASES="$OPTARG" + ;; + l) + echo $TEST_CASES + exit 0 + ;; + h) + show_help + exit 0 + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + show_help + exit 1 + ;; + :) + echo "Option -$OPTARG requires an argument." >&2 + exit 1 + ;; + esac +done + +generate_cert() { + local cert_dir="$1/cert" + mkdir -p $cert_dir + openssl genpkey -algorithm RSA -out $cert_dir/cert.key -pkeyopt rsa_keygen_bits:2048 -quiet + openssl req -new -key $cert_dir/cert.key -out $cert_dir/cert.csr -subj "/C=CN/ST=beijing/O=tquic/CN=example.org" + openssl x509 -req -in $cert_dir/cert.csr -signkey $cert_dir/cert.key -out $cert_dir/cert.crt +} + +generate_files() { + local data_dir="$1/data" + mkdir -p $data_dir + dd if=/dev/urandom of=$data_dir/1m bs=1M count=1 + dd if=/dev/urandom of=$data_dir/10m bs=1M count=10 +} + +test_multipath() { + local test_dir=$1 + local algor=$2 + echo "[-] Running multipath test for $algor" + + # prepare environment + local cert_dir="$test_dir/cert" + local data_dir="$test_dir/data" + local dump_dir="$test_dir/dump" + local qlog_dir="$test_dir/qlog" + + generate_cert $test_dir + generate_files $test_dir + + # start tquic server + $BIN_DIR/tquic_server -l 127.0.8.8:8443 --enable-multipath --multipath-algor $algor \ + --cert $cert_dir/cert.crt --key $cert_dir/cert.key \ + --root $data_dir --active-cid-limit 8 & + server_pid=$! + trap "kill $server_pid" RETURN + + # start tquic client + mkdir -p $dump_dir + $BIN_DIR/tquic_client -c 127.0.8.8:8443 --enable-multipath --multipath-algor $algor \ + --local-addresses 127.0.0.1,127.0.0.2,127.0.0.3,127.0.0.4 --active-cid-limit 8 \ + --qlog-dir $qlog_dir --log-file $test_dir/client.log --log-level trace \ + --dump-dir $dump_dir \ + https://example.org/10m + + # check files + if ! cmp -s $dump_dir/10m $data_dir/10m; then + echo "Files not same $dump_dir/1m:$data_dir/1m" + exit 1 + fi + + # check logs + grep "recv packet OneRTT" $test_dir/client.log | grep "local=.*" -o | sort | uniq -c + + echo "Test $algor OK" +} + +echo "$TEST_CASES" | sed 's/,/\n/g' | while read -r TEST_CASE; do + case $TEST_CASE in + multipath_minrtt) + test_multipath "$TEST_DIR/minrtt" minrtt + ;; + multipath_redundant) + test_multipath "$TEST_DIR/redundant" redundant + ;; + multipath_roundrobin) + test_multipath "$TEST_DIR/roundrobin" roundrobin + ;; + *) + echo "[x] Unknown test case $TEST_CASE" + ;; + esac +done +