From 41e3362e25ecdf1c09a018ed16008c260567efba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B2=81=E4=B8=80?= Date: Fri, 13 Oct 2023 18:49:33 +0800 Subject: [PATCH 1/2] [+]Add mp hsk(mpack), pa, ps, mp transfer testcase [+]Add cid list to complete testcase of pathabandon & pathstatus [+] Add comment of mp testcase [~]fix gramma mistake [~] change the time to check ACK_MP [~]change the path identity to ip:port address [=]Add comment and err process of pid_addr_dict generator [=]Add comment on mp casetest --- README.md | 8 ++ testcases.py | 349 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 357 insertions(+) diff --git a/README.md b/README.md index ef67ada7..d4b46437 100644 --- a/README.md +++ b/README.md @@ -98,3 +98,11 @@ Currently disabled due to #20. * **Handshake Loss** (`multiconnect`): Tests resilience of the handshake to high loss. The client is expected to establish multiple connections, sequential or in parallel, and use each connection to download a single file. * **V2** (`v2`): In this test, client starts connecting server in QUIC v1 with `version_information` transport parameter that includes QUIC v2 (`0x6b3343cf`) in `other_versions` field. Server should select QUIC v2 in compatible version negotiation. Client is expected to download one small file in QUIC v2. + +* **Multipath Handshake** (`mphandshake`): Tests the successful completion of multipath handshake. The client is expected to establish a multipath QUIC connection to the server and download one or multiple small files. Servers should not send a Retry packet in this test case. Both endpoint must support multipath transmission and use `ACK_MP` frame instead after handshake is done. + +* **Multipath Transfer** (`mptransfer`): Tests whether each path could be used to transfer data streams. The client is exepcted to establish a multipath QUIC connection, and use multiple path to concurrently download the files. + +* **Multipath PATH ABANDON** (`mppathabandon`): Tests that the path could be abandoned normally. The client is expected to establish a multipath QUIC connection. One of the path should be cut off before the transmission finished. After that, the transmission should be completed without using the abandoned path. + +* **Multipath PATH STATUS** (`mppathstatus`): Tests that the path status could be changed. The client is expected to establish a multipath QUIC connection, and set of the path keep `standby` before the transmission finished. The transmission should be completed without sending probing packet on the standby path. \ No newline at end of file diff --git a/testcases.py b/testcases.py index a9ccb14f..e13665d8 100644 --- a/testcases.py +++ b/testcases.py @@ -274,6 +274,76 @@ def cleanup(self): if self._download_dir: self._download_dir.cleanup() self._download_dir = None + + def _get_pid_addr_set(self): + # generate a dictionary of path_id - physical address + initial_cid = [] + # get cid from both server and client in initial packet + for p in self._server_trace().get_initial(Direction.ALL): + if len(initial_cid) < 2 and hasattr(p, "quic.scid"): + initial_cid.append(getattr(p, "quic.scid")) + + pid_cid_dict = {} + pid_cid_dict["0"] = initial_cid + + cid_addr_dict = {} + + src_addr = "" + dst_addr = "" + for p in self._server_trace().get_raw_packets(): + for layer in p.layers: + if layer.layer_name == "ip": + src_addr = getattr(layer, "ip.src_host") + dst_addr = getattr(layer, "ip.dst_host") + if layer.layer_name == "udp" and src_addr != "" and dst_addr != "": + src_addr = src_addr + ":" + getattr(layer, "udp.srcport") + dst_addr = dst_addr + ":" + getattr(layer, "udp.dstport") + if layer.layer_name == "quic": + cid = getattr(layer, "quic.dcid") + if cid_addr_dict.get(cid) == None: + cid_addr_dict[str(cid)] = {src_addr, dst_addr} + src_addr = "" + dst_addr = "" + + quic_frame_list = [] + for i in layer.frame.all_fields: + quic_frame_list.append(i.get_default_value()) + + if "NEW_CONNECTION_ID" in quic_frame_list: + path_id = getattr(layer, "quic.nci.sequence") + new_cid = getattr(layer, "quic.nci.connection_id") + if pid_cid_dict.get(path_id) == None: + pid_cid_dict[str(path_id)] = [new_cid] + elif len(pid_cid_dict.get(path_id)) < 2: + cid_list_of_path = pid_cid_dict.get(path_id) + cid_list_of_path.append(new_cid) + pid_cid_dict[str(path_id)] = cid_list_of_path + + pid_addr_dict = {} + # generate path_id - physical address dictionary with pid_cid_dict and cid_addr_dict + for pid, cids in pid_cid_dict.items(): + for cid in cids: + + curr_addr_set = cid_addr_dict.get(cid) + if curr_addr_set == None: + break + + if pid_addr_dict.get(pid) == None: + # check if here's the same physical address in the dictionary + has_same_addr = False + for addr_set in pid_addr_dict.values(): + if curr_addr_set == addr_set: + has_same_addr = True + break + + if has_same_addr == True: + print(pid_addr_dict) + logging.info("There's different path use same physical address") + return TestResult.FAILED + + pid_addr_dict[pid] = curr_addr_set + + return pid_addr_dict @abc.abstractmethod def get_paths(self): @@ -1534,6 +1604,279 @@ def _get_packet_versions(self, packets: List) -> set: return set([hex(int(p.version, 0)) for p in packets]) + +class TestCaseMultipathStatus(TestCase): + @staticmethod + def name(): + return "mppathstatus" + + @staticmethod + def abbreviation(): + return "PS" + + @staticmethod + def desc(): + return "Multipath path status" + + def get_paths(self): + self._files = [self._generate_random_file(1 * KB)] + return self._files + + def check(self) -> TestResult: + if not self._check_version_and_files(): + return TestResult.FAILED + + # get pid_address set to create connection between pid and ip:port address + pid_addr_set = self._get_pid_addr_set() + if pid_addr_set == TestResult.FAILED: + return TestResult.FAILED + + for p in self._server_trace().get_1rtt(): + if hasattr(p, "quic.frame"): + for i in p.frame.all_fields: + if "ACK_MP" in i.get_default_value(): + is_mp_transfer = True + + if is_mp_transfer == False: + logging.info("MP handshake failed. Please check whether both endpoint are enable multipath.") + return TestResult.FAILED + + # signal for whether some path is been set "standby" + is_path_standby = False + # pid of path standby + standby_pid = "" + + src_addr = "" + dst_addr = "" + for p in self._server_trace().get_raw_packets(): + for layer in p.layers: + if layer.layer_name == "ip": + src_addr = getattr(layer, "ip.src_host") + dst_addr = getattr(layer, "ip.dst_host") + if layer.layer_name == "udp" and src_addr != "" and dst_addr != "": + src_addr = src_addr + ":" + getattr(layer, "udp.srcport") + dst_addr = dst_addr + ":" + getattr(layer, "udp.dstport") + if layer.layer_name == "quic" and hasattr( + layer, "quic.frame" + ): + quic_frame_list = [] + for i in layer.frame.all_fields: + quic_frame_list.append(i.get_default_value()) + if is_path_standby == True: + curr_pid = None + for key, value in pid_addr_set.items(): + if value != None and src_addr in value and dst_addr in value: + curr_pid = key + + if curr_pid == standby_pid and "STREAM" in quic_frame_list: + print("path " + str(abandoned_pid) + " is expected to be abandoned") + return TestResult.FAILED + if "PATH_STATUS" in quic_frame_list: + + path_status = getattr(layer, "quic.mp_ps_path_status") + logging.info("path status: %s", path_status) + + # if a path is marked as "standby", record this path's information + if int(path_status) == 1: + is_path_standby = True + standby_pid = getattr(layer, "quic.mp_ps_dcid_sequence_number") + + if is_path_standby == True: + return TestResult.SUCCEEDED + + logging.info("There's no PATH_STATUS frame received") + return TestResult.FAILED + +class TestCaseMultipathPathAbandon(TestCase): + @staticmethod + def name(): + return "mppathabandon" + + @staticmethod + def abbreviation(): + return "PA" + + @staticmethod + def desc(): + return "Multipath path abandon" + + def get_paths(self): + self._files = [self._generate_random_file(1 * KB)] + return self._files + + def check(self) -> TestResult: + if not self._check_version_and_files(): + return TestResult.FAILED + + # get pid_address set to create connection between pid and ip:port address + pid_addr_set = self._get_pid_addr_set() + if pid_addr_set == TestResult.FAILED: + return TestResult.FAILED + + for p in self._server_trace().get_1rtt(): + if hasattr(p, "quic.frame"): + for i in p.frame.all_fields: + if "ACK_MP" in i.get_default_value(): + is_mp_transfer = True + + if is_mp_transfer == False: + logging.info("MP handshake failed. Please check whether both endpoint are enable multipath.") + return TestResult.FAILED + + # signal for whether some path is been abandoned + is_path_abandoned = False + # path id of abandoned path + abandoned_pid = "" + src_addr = "" + dst_addr = "" + for p in self._server_trace().get_raw_packets(): + for layer in p.layers: + if layer.layer_name == "ip": + src_addr = getattr(layer, "ip.src_host") + dst_addr = getattr(layer, "ip.dst_host") + if layer.layer_name == "udp" and src_addr != "" and dst_addr != "": + src_addr = src_addr + ":" + getattr(layer, "udp.srcport") + dst_addr = dst_addr + ":" + getattr(layer, "udp.dstport") + if layer.layer_name == "quic" and hasattr( + layer, "quic.frame" + ): + if is_path_abandoned == True: + curr_pid = None + for key, value in pid_addr_set.items(): + if value != None and src_addr in value and dst_addr in value: + curr_pid = key + if curr_pid == abandoned_pid: + print("path " + str(abandoned_pid) + " is expected to be abandoned") + return TestResult.FAILED + + for i in layer.frame.all_fields: + if "PATH_ABANDON" in i.get_default_value(): + logging.info("%s", i.get_default_value()) + is_path_abandoned = True + abandoned_pid = getattr(layer, "quic.mp_pa_dcid_sequence_number") + + if is_path_abandoned: + return TestResult.SUCCEEDED + + logging.info("There's no PATH_ABANDON frame received") + return TestResult.FAILED + + + +class TestCaseMultipathHandshake(TestCase): + @staticmethod + def name(): + return "mphandshake" + + @staticmethod + def abbreviation(): + return "MP" + + @staticmethod + def desc(): + return "Multipath negotiation and data transfer" + + def get_paths(self): + self._files = [self._generate_random_file(1 * KB)] + return self._files + + def check(self) -> TestResult: + if not self._check_version_and_files(): + return TestResult.FAILED + if self._retry_sent(): + logging.info("Didn't expect a Retry to be sent.") + return TestResult.FAILED + num_handshakes = self._count_handshakes() + if num_handshakes != 1: + logging.info("Expected exactly 1 handshake. Got: %d", num_handshakes) + return TestResult.FAILED + + # check whether there's enable multipath param from client + c_enable_multipath = False + for p in self._server_trace().get_initial(Direction.FROM_CLIENT): + if hasattr(p, "tls.quic.parameter.enable_multipath"): + c_enable_multipath = True + logging.debug("Client enable multipath") + + # check whether there's enable multipath param from server + s_enable_multipath = False + res = False + for p in self._client_trace().get_handshake(Direction.FROM_SERVER): + if hasattr(p, "tls.quic.parameter.enable_multipath"): + s_enable_multipath = True + logging.debug("Server enable multipath") + + for p in self._server_trace().get_1rtt(Direction.ALL): + if hasattr(p, "quic.frame"): + quic_frame = getattr(p, "quic.frame") + # if receive "ACK_MP" frame, it indicate the success of mp handshake + if quic_frame == 'ACK_MP': + res = c_enable_multipath and s_enable_multipath + + + if not res: + logging.info("negotiation failed, server enable_multipath: %d; client enable_multipath: %d; if send ACK_MP: %d", s_enable_multipath, c_enable_multipath, res) + return TestResult.FAILED + + logging.info("Multipath handshake complete") + return TestResult.SUCCEEDED + + +class TestCaseMultipathTransfer(TestCase): + @staticmethod + def name(): + return "mptransfer" + + @staticmethod + def abbreviation(): + return "MPT" + + @staticmethod + def desc(): + return "Multipath transfer data on more than one path" + + def get_paths(self): + self._files = [self._generate_random_file(1 * KB)] + return self._files + + def check(self) -> TestResult: + if not self._check_version_and_files(): + return TestResult.FAILED + # get pid_address set to create connection between pid and ip:port address + pid_addr_set = self._get_pid_addr_set() + if pid_addr_set == TestResult.FAILED: + return TestResult.FAILED + + send_path = [] + src_addr = "" + dst_addr = "" + for p in self._server_trace().get_raw_packets(): + for layer in p.layers: + if layer.layer_name == "ip": + src_addr = getattr(layer, "ip.src_host") + dst_addr = getattr(layer, "ip.dst_host") + if layer.layer_name == "udp" and src_addr != "" and dst_addr != "": + src_addr = src_addr + ":" + getattr(layer, "udp.srcport") + dst_addr = dst_addr + ":" + getattr(layer, "udp.dstport") + if layer.layer_name == "quic" and hasattr( + layer, "quic.frame" + ): + for i in layer.frame.all_fields: + if "STREAM" in i.get_default_value(): + for key, value in pid_addr_set.items(): + if value != None and src_addr in value and dst_addr in value and key not in send_path: + send_path.append(key) + + # if receive stream frame from more than one path, it indicate success of mp transfer + if len(send_path) > 1: + return TestResult.SUCCEEDED + logging.info("Only sent data use single path.") + + return TestResult.FAILED + + + + class MeasurementGoodput(Measurement): FILESIZE = 10 * MB _result = 0.0 @@ -1648,6 +1991,12 @@ def additional_containers() -> List[str]: # TestCasePortRebinding, # TestCaseAddressRebinding, # TestCaseConnectionMigration, + + # Current Wireshark version cannot support the following tests + TestCaseMultipathHandshake, + TestCaseMultipathTransfer, + TestCaseMultipathStatus, + TestCaseMultipathPathAbandon, ] MEASUREMENTS = [ From 5254e47fc25602b27cf24f4468ec72cf8c1ca1d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B2=81=E4=B8=80?= Date: Wed, 1 Nov 2023 19:09:19 +0800 Subject: [PATCH 2/2] [~]Change the condition of passing mp transfer case test --- testcases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testcases.py b/testcases.py index e13665d8..d256cc65 100644 --- a/testcases.py +++ b/testcases.py @@ -1862,7 +1862,7 @@ def check(self) -> TestResult: layer, "quic.frame" ): for i in layer.frame.all_fields: - if "STREAM" in i.get_default_value(): + if "STREAM" in i.get_default_value() or "ACK_MP" in i.get_default_value(): for key, value in pid_addr_set.items(): if value != None and src_addr in value and dst_addr in value and key not in send_path: send_path.append(key)