diff --git a/.github/workflows/interop.yml b/.github/workflows/interop.yml index ad550b6..2a028ba 100644 --- a/.github/workflows/interop.yml +++ b/.github/workflows/interop.yml @@ -9,7 +9,7 @@ on: # To trigger a rebuild of Wireshark increment this value. # The rebuild will then build the current master of Wireshark and save it under the new key. env: - WIRESHARK_CACHEKEY: 7 + WIRESHARK_CACHEKEY: 8 jobs: wireshark: @@ -161,7 +161,7 @@ jobs: timeout-minutes: 45 strategy: fail-fast: false - matrix: + matrix: server: ${{ fromJson(needs.config.outputs.servers) }} client: ${{ fromJson(needs.config.outputs.clients) }} name: (${{ matrix.server }} - ${{ matrix.client }}) @@ -287,7 +287,7 @@ jobs: remote_host: interop.seemann.io remote_user: ${{ secrets.INTEROP_SEEMANN_IO_USER }} remote_key: ${{ secrets.INTEROP_SEEMANN_IO_SSH_KEY }} - - name: Point interop.seemann.io to the latest result + - name: Point interop.seemann.io to the latest result uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0 if: ${{ github.event_name == 'schedule' }} with: diff --git a/README.md b/README.md index 8696de2..13b4998 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 4.5.0 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 fc69e11..b38c24d 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 @@ -1244,6 +1244,21 @@ 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 ( + (TestCasePortRebinding._addr(p, "src"), int(getattr(p["udp"], "srcport"))), + (TestCasePortRebinding._addr(p, "dst"), int(getattr(p["udp"], "dstport"))), + ) + def check(self) -> TestResult: super().check() if not self._keylog_file(): @@ -1258,59 +1273,40 @@ 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 = ( - ( - 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 - 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 + # Packet on new path, 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"]) return TestResult.FAILED + else: + 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 only a single path in use; 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") @@ -1336,6 +1332,10 @@ def name(): def abbreviation(): return "BA" + @staticmethod + def testname(p: Perspective): + return "transfer" + @staticmethod def desc(): return "Transfer completes under frequent IP address and port rebindings on the client side." @@ -1435,7 +1435,8 @@ def abbreviation(): @staticmethod def testname(p: Perspective): - if p is Perspective.CLIENT: + if p is Perspective.SERVER: + # Server needs to send preferred addresses return "connectionmigration" return "transfer" @@ -1447,6 +1448,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), @@ -1466,22 +1472,17 @@ def check(self) -> TestResult: ) last = None + 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") 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"): @@ -1699,11 +1700,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 = [