From 6623b3097568f30c8e03aef0804e69b24c5d1ac6 Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Mon, 14 Oct 2024 11:16:49 +0200 Subject: [PATCH 01/10] feat: Fix and enable connection migration tests --- testcases.py | 46 ++++++++++++++++------------------------------ 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/testcases.py b/testcases.py index 7d841e9..3c72a79 100644 --- a/testcases.py +++ b/testcases.py @@ -1239,15 +1239,10 @@ def check(self) -> TestResult: self._server_trace()._get_direction_filter(Direction.FROM_SERVER) + " quic" ) - ports = list(set(getattr(p["udp"], "dstport") for p in tr_server)) - - logging.info("Server saw these client ports: %s", ports) - if len(ports) <= 1: - logging.info("Server saw only a single client port in use; test broken?") - return TestResult.FAILED - + cur = None last = None - num_migrations = 0 + paths = set() + challenges = set() for p in tr_server: cur = ( ( @@ -1261,9 +1256,9 @@ def check(self) -> TestResult: last = cur continue - if last != cur: + if last != cur and cur not in paths: + paths.add(last) last = cur - num_migrations += 1 # packet to different IP/port, should have a PATH_CHALLENGE frame if hasattr(p["quic"], "path_challenge.data") is False: logging.info( @@ -1272,26 +1267,19 @@ def check(self) -> TestResult: ) logging.info(p["quic"]) return TestResult.FAILED + else: + challenges.add(getattr(p["quic"], "path_challenge.data")) + + paths.add(cur) + + if len(paths) <= 1: + logging.info("Server saw the client use only a single path; test broken?") + return TestResult.FAILED tr_client = self._client_trace()._get_packets( self._client_trace()._get_direction_filter(Direction.FROM_CLIENT) + " quic" ) - challenges = list( - set( - getattr(p["quic"], "path_challenge.data") - for p in tr_server - if hasattr(p["quic"], "path_challenge.data") - ) - ) - if len(challenges) < num_migrations: - logging.info( - "Saw %d migrations, but only %d unique PATH_CHALLENGE frames", - len(challenges), - num_migrations, - ) - return TestResult.FAILED - responses = list( set( getattr(p["quic"], "path_response.data") @@ -1675,11 +1663,9 @@ def additional_containers() -> List[str]: TestCaseTransferCorruption, TestCaseIPv6, TestCaseV2, - # The next three tests are disabled due to Wireshark not being able - # to decrypt packets sent on the new path. - # TestCasePortRebinding, - # TestCaseAddressRebinding, - # TestCaseConnectionMigration, + TestCasePortRebinding, + TestCaseAddressRebinding, + TestCaseConnectionMigration, ] MEASUREMENTS = [ From 538c786473d819e769fcf918d66c22e6aa8ff32e Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Mon, 14 Oct 2024 14:19:00 +0200 Subject: [PATCH 02/10] Client might need to know which test this is For example, to avoid using zero-len CIDs. --- testcases.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/testcases.py b/testcases.py index 3c72a79..9c67616 100644 --- a/testcases.py +++ b/testcases.py @@ -1209,6 +1209,8 @@ def abbreviation(): @staticmethod def testname(p: Perspective): + if p is Perspective.CLIENT: + return "rebind-port" return "transfer" @staticmethod @@ -1305,6 +1307,12 @@ def name(): def abbreviation(): return "BA" + @staticmethod + def testname(p: Perspective): + if p is Perspective.CLIENT: + return "rebind-addr" + return "transfer" + @staticmethod def desc(): return "Transfer completes under frequent IP address and port rebindings on the client side." From 298264dd189fc356330f914879f946121f242b11 Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Tue, 15 Oct 2024 11:30:25 +0200 Subject: [PATCH 03/10] More fixes --- testcases.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/testcases.py b/testcases.py index 9c67616..ff0b5fe 100644 --- a/testcases.py +++ b/testcases.py @@ -1422,6 +1422,11 @@ def desc(): def scenario() -> str: return super(TestCaseTransfer, TestCaseTransfer).scenario() + @staticmethod + def urlprefix() -> str: + """URL prefix""" + return "https://server46:443/" + def get_paths(self): self._files = [ self._generate_random_file(2 * MB), @@ -1440,6 +1445,7 @@ def check(self) -> TestResult: ) last = None + paths = set() dcid = None for p in tr_client: cur = ( @@ -1455,7 +1461,8 @@ def check(self) -> TestResult: dcid = getattr(p["quic"], "dcid") continue - if last != cur: + if last != cur and cur not in paths: + paths.add(last) last = cur # packet to different IP/port, should have a new DCID if dcid == getattr(p["quic"], "dcid"): From c5b0ba1752db1d0bc13a1bfdc6de383a31d51553 Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Sat, 2 Nov 2024 15:53:26 +0000 Subject: [PATCH 04/10] More --- README.md | 24 +++++++++++++------- docker-compose.yml | 17 +++++++------- testcases.py | 55 ++++++++++++++++++++-------------------------- 3 files changed, 49 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 8696de2..98ce3d7 100644 --- a/README.md +++ b/README.md @@ -10,19 +10,20 @@ The Interop Test Runner aims to automatically generate an interop matrix by runn The Interop Runner is written in Python 3. You'll need to install the following softwares to run the interop test: -- Python3 modules. Run the following command: +* Python3 modules. Run the following command: -```bash -pip3 install -r requirements.txt -``` + ```bash + pip3 install -r requirements.txt + ``` -- [Docker](https://docs.docker.com/engine/install/) and [docker compose](https://docs.docker.com/compose/). +* [Docker](https://docs.docker.com/engine/install/) and [docker compose](https://docs.docker.com/compose/). -- [Development version of Wireshark](https://www.wireshark.org/download.html) (version 3.4.2 or newer). +* [Development version of Wireshark](https://www.wireshark.org/download.html) (version 3.4.2 or newer). ## Running the Interop Runner Run the interop tests: + ```bash python3 run.py ``` @@ -58,9 +59,10 @@ If you're not familiar with Docker, it might be helpful to have a look at the Do Implementers: Please feel free to add links to your implementation here! -Note that the [online interop](https://interop.seemann.io/) runner requires `linux/amd64` architecture, so if you build on a different architecture (e.g. "Apple silicon"), you would need to use `--platform linux/amd64` with `docker build` to create a compatible image. +Note that the [online interop](https://interop.seemann.io/) runner requires `linux/amd64` architecture, so if you build on a different architecture (e.g. "Apple silicon"), you would need to use `--platform linux/amd64` with `docker build` to create a compatible image. Even better, and the recommended approach, is to use a multi-platform build to provide both `amd64` and `arm64` images, so everybody can run the interop locally with your implementation. To build the multi-platform image, you can use the `docker buildx` command: -``` + +```bash docker buildx create --use docker buildx build --pull --push --platform linux/amd64,linux/arm64 -t . ``` @@ -101,3 +103,9 @@ 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. + +* **Port Rebinding** (`rebind-port`): In this test case, a NAT is simulated that changes the client port (as observed by the server) after the handshake. Server should perform path vaildation. + +* **Address Rebinding** (`rebind-addr`): In this test case, a NAT is simulated that changes the client IP address (as observed by the server) after the handshake. Server should perform path vaildation. + +* **Connection Migratioon** (`connectionmigration`): In this test case, the server is expected to provide its preferred addresses to the client during the handshake. The client is expected to perform active migration to one of those addresses. diff --git a/docker-compose.yml b/docker-compose.yml index 93664f0..f5c32d5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "2.4" - services: sim: image: martenseemann/quic-network-simulator @@ -10,7 +8,7 @@ services: environment: - WAITFORSERVER=$WAITFORSERVER - SCENARIO=$SCENARIO - cap_add: + cap_add: - NET_ADMIN - NET_RAW expose: @@ -43,7 +41,7 @@ services: - TESTCASE=$TESTCASE_SERVER depends_on: - sim - cap_add: + cap_add: - NET_ADMIN ulimits: memlock: 67108864 @@ -51,6 +49,9 @@ services: rightnet: ipv4_address: 193.167.100.100 ipv6_address: fd00:cafe:cafe:100::100 + extra_hosts: + - "server4:193.167.100.100" + - "server6:fd00:cafe:cafe:100::100" client: image: $CLIENT @@ -71,7 +72,7 @@ services: - REQUESTS=$REQUESTS depends_on: - sim - cap_add: + cap_add: - NET_ADMIN ulimits: memlock: 67108864 @@ -84,7 +85,7 @@ services: - "server6:fd00:cafe:cafe:100::100" - "server46:193.167.100.100" - "server46:fd00:cafe:cafe:100::100" - + iperf_server: image: martenseemann/quic-interop-iperf-endpoint container_name: iperf_server @@ -96,7 +97,7 @@ services: - IPERF_CONGESTION=$IPERF_CONGESTION depends_on: - sim - cap_add: + cap_add: - NET_ADMIN networks: rightnet: @@ -118,7 +119,7 @@ services: - IPERF_CONGESTION=$IPERF_CONGESTION depends_on: - sim - cap_add: + cap_add: - NET_ADMIN networks: leftnet: diff --git a/testcases.py b/testcases.py index ff0b5fe..f7aca8d 100644 --- a/testcases.py +++ b/testcases.py @@ -19,7 +19,7 @@ get_direction, get_packet_type, ) -from typing import List +from typing import List, Tuple from Crypto.Cipher import AES @@ -1228,6 +1228,23 @@ def scenario() -> str: """Scenario for the ns3 simulator""" return "rebind --delay=15ms --bandwidth=10Mbps --queue=25 --first-rebind=1s --rebind-freq=5s" + @staticmethod + def _path(p: List) -> Tuple[str, int, str, int]: + return ( + ( + getattr(p["ipv6"], "src") + if "IPV6" in str(p.layers) + else getattr(p["ip"], "src") + ), + int(getattr(p["udp"], "srcport")), + ( + getattr(p["ipv6"], "dst") + if "IPV6" in str(p.layers) + else getattr(p["ip"], "dst") + ), + int(getattr(p["udp"], "dstport")), + ) + def check(self) -> TestResult: if not self._keylog_file(): logging.info("Can't check test result. SSLKEYLOG required.") @@ -1246,14 +1263,7 @@ def check(self) -> TestResult: paths = set() challenges = set() for p in tr_server: - cur = ( - ( - getattr(p["ipv6"], "dst") - if "IPV6" in str(p.layers) - else getattr(p["ip"], "dst") - ), - int(getattr(p["udp"], "dstport")), - ) + cur = self._path(p) if last is None: last = cur continue @@ -1264,7 +1274,7 @@ def check(self) -> TestResult: # packet to different IP/port, should have a PATH_CHALLENGE frame if hasattr(p["quic"], "path_challenge.data") is False: logging.info( - "First server packet to new client destination %s did not contain a PATH_CHALLENGE frame", + "First server packet on new path %s did not contain a PATH_CHALLENGE frame", cur, ) logging.info(p["quic"]) @@ -1320,10 +1330,7 @@ def desc(): @staticmethod def scenario() -> str: """Scenario for the ns3 simulator""" - return ( - super(TestCaseAddressRebinding, TestCaseAddressRebinding).scenario() - + " --rebind-addr" - ) + return super(TestCaseAddressRebinding).scenario() + " --rebind-addr" def check(self) -> TestResult: if not self._keylog_file(): @@ -1399,7 +1406,7 @@ def check(self) -> TestResult: return TestResult.SUCCEEDED -class TestCaseConnectionMigration(TestCaseAddressRebinding): +class TestCaseConnectionMigration(TestCasePortRebinding): @staticmethod def name(): return "connectionmigration" @@ -1410,9 +1417,7 @@ def abbreviation(): @staticmethod def testname(p: Perspective): - if p is Perspective.CLIENT: - return "connectionmigration" - return "transfer" + return "connectionmigration" @staticmethod def desc(): @@ -1422,11 +1427,6 @@ def desc(): def scenario() -> str: return super(TestCaseTransfer, TestCaseTransfer).scenario() - @staticmethod - def urlprefix() -> str: - """URL prefix""" - return "https://server46:443/" - def get_paths(self): self._files = [ self._generate_random_file(2 * MB), @@ -1448,14 +1448,7 @@ def check(self) -> TestResult: paths = set() dcid = None for p in tr_client: - cur = ( - ( - getattr(p["ipv6"], "src") - if "IPV6" in str(p.layers) - else getattr(p["ip"], "src") - ), - int(getattr(p["udp"], "srcport")), - ) + cur = self._path(p) if last is None: last = cur dcid = getattr(p["quic"], "dcid") From 9dc67c27b352b88d9ebd8e08b1ccac1b11a94eaf Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Tue, 15 Oct 2024 11:30:25 +0200 Subject: [PATCH 05/10] More fixes --- testcases.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/testcases.py b/testcases.py index f7aca8d..3434d55 100644 --- a/testcases.py +++ b/testcases.py @@ -1427,6 +1427,11 @@ def desc(): def scenario() -> str: return super(TestCaseTransfer, TestCaseTransfer).scenario() + @staticmethod + def urlprefix() -> str: + """URL prefix""" + return "https://server46:443/" + def get_paths(self): self._files = [ self._generate_random_file(2 * MB), From 2324a3ddc4606e6fc2d21fca6cd6a4e17e41ab28 Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Sun, 3 Nov 2024 13:19:18 +0000 Subject: [PATCH 06/10] Fixes --- README.md | 2 +- testcases.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 98ce3d7..6502f33 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ following softwares to run the interop test: * [Docker](https://docs.docker.com/engine/install/) and [docker compose](https://docs.docker.com/compose/). -* [Development version of Wireshark](https://www.wireshark.org/download.html) (version 3.4.2 or newer). +* [Development version of Wireshark](https://www.wireshark.org/download.html) (version 3.5.0 or newer). ## Running the Interop Runner diff --git a/testcases.py b/testcases.py index 3434d55..1e57bfb 100644 --- a/testcases.py +++ b/testcases.py @@ -1330,7 +1330,10 @@ def desc(): @staticmethod def scenario() -> str: """Scenario for the ns3 simulator""" - return super(TestCaseAddressRebinding).scenario() + " --rebind-addr" + return ( + super(TestCaseAddressRebinding, TestCaseAddressRebinding).scenario() + + " --rebind-addr" + ) def check(self) -> TestResult: if not self._keylog_file(): From 028d6f143f3b3d611baae5323f8b4685d8c6e6a9 Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Mon, 4 Nov 2024 10:07:43 +0000 Subject: [PATCH 07/10] Do not require path validation if only the port rebinds --- README.md | 2 +- testcases.py | 45 ++++++++++++++++++++++----------------------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 6502f33..8c84111 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ Currently disabled due to #20. * **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. -* **Port Rebinding** (`rebind-port`): In this test case, a NAT is simulated that changes the client port (as observed by the server) after the handshake. Server should perform path vaildation. +* **Port Rebinding** (`rebind-port`): In this test case, a NAT is simulated that changes the client port (as observed by the server) after the handshake. * **Address Rebinding** (`rebind-addr`): In this test case, a NAT is simulated that changes the client IP address (as observed by the server) after the handshake. Server should perform path vaildation. diff --git a/testcases.py b/testcases.py index 1e57bfb..8630c64 100644 --- a/testcases.py +++ b/testcases.py @@ -1228,21 +1228,19 @@ def scenario() -> str: """Scenario for the ns3 simulator""" return "rebind --delay=15ms --bandwidth=10Mbps --queue=25 --first-rebind=1s --rebind-freq=5s" + @staticmethod + def _addr(p: List, which: str) -> str: + return ( + getattr(p["ipv6"], which) + if "IPV6" in str(p.layers) + else getattr(p["ip"], which) + ) + @staticmethod def _path(p: List) -> Tuple[str, int, str, int]: return ( - ( - getattr(p["ipv6"], "src") - if "IPV6" in str(p.layers) - else getattr(p["ip"], "src") - ), - int(getattr(p["udp"], "srcport")), - ( - getattr(p["ipv6"], "dst") - if "IPV6" in str(p.layers) - else getattr(p["ip"], "dst") - ), - int(getattr(p["udp"], "dstport")), + (TestCasePortRebinding._addr(p, "src"), int(getattr(p["udp"], "srcport"))), + (TestCasePortRebinding._addr(p, "dst"), int(getattr(p["udp"], "dstport"))), ) def check(self) -> TestResult: @@ -1270,18 +1268,19 @@ def check(self) -> TestResult: if last != cur and cur not in paths: paths.add(last) + last_dst = last[1][0] last = cur - # packet to different IP/port, should have a PATH_CHALLENGE frame - if hasattr(p["quic"], "path_challenge.data") is False: - logging.info( - "First server packet on new path %s did not contain a PATH_CHALLENGE frame", - cur, - ) - logging.info(p["quic"]) - return TestResult.FAILED - else: - challenges.add(getattr(p["quic"], "path_challenge.data")) - + if last_dst != cur[1][0]: + # packet to different IP, should have a PATH_CHALLENGE frame + if hasattr(p["quic"], "path_challenge.data") is False: + logging.info( + "First server packet on new path %s did not contain a PATH_CHALLENGE frame", + cur, + ) + logging.info(p["quic"]) + return TestResult.FAILED + else: + challenges.add(getattr(p["quic"], "path_challenge.data")) paths.add(cur) if len(paths) <= 1: From 4ba6e90fda2d07f276bda8974b9f0e52732f55c0 Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Mon, 4 Nov 2024 16:32:34 +0000 Subject: [PATCH 08/10] Fixes --- testcases.py | 42 ++++++++---------------------------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/testcases.py b/testcases.py index 8630c64..1629042 100644 --- a/testcases.py +++ b/testcases.py @@ -1209,8 +1209,6 @@ def abbreviation(): @staticmethod def testname(p: Perspective): - if p is Perspective.CLIENT: - return "rebind-port" return "transfer" @staticmethod @@ -1283,8 +1281,9 @@ def check(self) -> TestResult: challenges.add(getattr(p["quic"], "path_challenge.data")) paths.add(cur) + logging.info("Server saw these paths used: %s", paths) if len(paths) <= 1: - logging.info("Server saw the client use only a single path; test broken?") + logging.info("Server saw only a single path in use; test broken?") return TestResult.FAILED tr_client = self._client_trace()._get_packets( @@ -1318,8 +1317,6 @@ def abbreviation(): @staticmethod def testname(p: Perspective): - if p is Perspective.CLIENT: - return "rebind-addr" return "transfer" @staticmethod @@ -1335,33 +1332,7 @@ def scenario() -> str: ) def check(self) -> TestResult: - if not self._keylog_file(): - logging.info("Can't check test result. SSLKEYLOG required.") - return TestResult.UNSUPPORTED - - tr_server = self._server_trace()._get_packets( - self._server_trace()._get_direction_filter(Direction.FROM_SERVER) + " quic" - ) - - ips = set() - for p in tr_server: - ip_vers = "ip" - if "IPV6" in str(p.layers): - ip_vers = "ipv6" - ips.add(getattr(p[ip_vers], "dst")) - - logging.info("Server saw these client addresses: %s", ips) - if len(ips) <= 1: - logging.info( - "Server saw only a single client IP address in use; test broken?" - ) - return TestResult.FAILED - - result = super(TestCaseAddressRebinding, self).check() - if result != TestResult.SUCCEEDED: - return result - - return TestResult.SUCCEEDED + return super(TestCaseAddressRebinding, self).check() class TestCaseIPv6(TestCaseTransfer): @@ -1408,7 +1379,7 @@ def check(self) -> TestResult: return TestResult.SUCCEEDED -class TestCaseConnectionMigration(TestCasePortRebinding): +class TestCaseConnectionMigration(TestCaseAddressRebinding): @staticmethod def name(): return "connectionmigration" @@ -1419,7 +1390,10 @@ def abbreviation(): @staticmethod def testname(p: Perspective): - return "connectionmigration" + if p is Perspective.SERVER: + # Server needs to send preferred addresses + return "connectionmigration" + return "transfer" @staticmethod def desc(): From 86427d1335ad06041e3858f3882b6541b2913aed Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Tue, 5 Nov 2024 10:33:36 +0000 Subject: [PATCH 09/10] Require path challenge on port change --- README.md | 2 +- testcases.py | 22 ++++++++++------------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8c84111..6502f33 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ Currently disabled due to #20. * **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. -* **Port Rebinding** (`rebind-port`): In this test case, a NAT is simulated that changes the client port (as observed by the server) after the handshake. +* **Port Rebinding** (`rebind-port`): In this test case, a NAT is simulated that changes the client port (as observed by the server) after the handshake. Server should perform path vaildation. * **Address Rebinding** (`rebind-addr`): In this test case, a NAT is simulated that changes the client IP address (as observed by the server) after the handshake. Server should perform path vaildation. diff --git a/testcases.py b/testcases.py index 1629042..ced187e 100644 --- a/testcases.py +++ b/testcases.py @@ -1266,19 +1266,17 @@ def check(self) -> TestResult: if last != cur and cur not in paths: paths.add(last) - last_dst = last[1][0] last = cur - if last_dst != cur[1][0]: - # packet to different IP, should have a PATH_CHALLENGE frame - if hasattr(p["quic"], "path_challenge.data") is False: - logging.info( - "First server packet on new path %s did not contain a PATH_CHALLENGE frame", - cur, - ) - logging.info(p["quic"]) - return TestResult.FAILED - else: - challenges.add(getattr(p["quic"], "path_challenge.data")) + # Packet on new path, should have a PATH_CHALLENGE frame + if hasattr(p["quic"], "path_challenge.data") is False: + logging.info( + "First server packet on new path %s did not contain a PATH_CHALLENGE frame", + cur, + ) + logging.info(p["quic"]) + return TestResult.FAILED + else: + challenges.add(getattr(p["quic"], "path_challenge.data")) paths.add(cur) logging.info("Server saw these paths used: %s", paths) From cde227039831c482684d9bae6c16e3a01f7cec7a Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Tue, 17 Dec 2024 12:37:12 +0200 Subject: [PATCH 10/10] Update README.md Co-authored-by: Piotr Sikora --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6502f33..13b4998 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ following softwares to run the interop test: * [Docker](https://docs.docker.com/engine/install/) and [docker compose](https://docs.docker.com/compose/). -* [Development version of Wireshark](https://www.wireshark.org/download.html) (version 3.5.0 or newer). +* [Development version of Wireshark](https://www.wireshark.org/download.html) (version 4.5.0 or newer). ## Running the Interop Runner