Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Fix and enable connection migration tests #410

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down Expand Up @@ -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 <name:tag> .
```
Expand Down Expand Up @@ -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.
17 changes: 9 additions & 8 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: "2.4"

services:
sim:
image: martenseemann/quic-network-simulator
Expand All @@ -10,7 +8,7 @@ services:
environment:
- WAITFORSERVER=$WAITFORSERVER
- SCENARIO=$SCENARIO
cap_add:
cap_add:
- NET_ADMIN
- NET_RAW
expose:
Expand Down Expand Up @@ -43,14 +41,17 @@ services:
- TESTCASE=$TESTCASE_SERVER
depends_on:
- sim
cap_add:
cap_add:
- NET_ADMIN
ulimits:
memlock: 67108864
networks:
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
Expand All @@ -71,7 +72,7 @@ services:
- REQUESTS=$REQUESTS
depends_on:
- sim
cap_add:
cap_add:
- NET_ADMIN
ulimits:
memlock: 67108864
Expand All @@ -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
Expand All @@ -96,7 +97,7 @@ services:
- IPERF_CONGESTION=$IPERF_CONGESTION
depends_on:
- sim
cap_add:
cap_add:
- NET_ADMIN
networks:
rightnet:
Expand All @@ -118,7 +119,7 @@ services:
- IPERF_CONGESTION=$IPERF_CONGESTION
depends_on:
- sim
cap_add:
cap_add:
- NET_ADMIN
networks:
leftnet:
Expand Down
101 changes: 50 additions & 51 deletions testcases.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
get_direction,
get_packet_type,
)
from typing import List
from typing import List, Tuple

from Crypto.Cipher import AES

Expand Down Expand Up @@ -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():
Expand All @@ -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
larseggert marked this conversation as resolved.
Show resolved Hide resolved

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")
Expand All @@ -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."
Expand Down Expand Up @@ -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"

Expand All @@ -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),
Expand All @@ -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"):
Expand Down Expand Up @@ -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 = [
Expand Down
Loading