Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 30 additions & 14 deletions testcases.py
Original file line number Diff line number Diff line change
Expand Up @@ -1283,7 +1283,8 @@ def check(self) -> TestResult:
cur = None
last = None
paths = set()
challenges = set()
# Track PATH_CHALLENGE data per path that needs validation.
path_challenges = {}
for p in tr_server:
cur = self._path(p)
if last is None:
Expand All @@ -1293,17 +1294,23 @@ def check(self) -> TestResult:
if last != cur and cur not in paths:
paths.add(last)
last = cur
# Packet on new path, should have a PATH_CHALLENGE frame
# Initialize challenge tracking for this new path.
path_challenges[cur] = set()
# First 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"))

# Capture all PATH_CHALLENGE frames for paths needing validation.
if cur in path_challenges and hasattr(p["quic"], "path_challenge.data"):
path_challenges[cur].add(getattr(p["quic"], "path_challenge.data"))

paths.add(cur)
final_path = cur

logging.info("Server saw these paths used: %s", paths)
if len(paths) <= 1:
Expand All @@ -1314,18 +1321,27 @@ def check(self) -> TestResult:
self._client_trace()._get_direction_filter(Direction.FROM_CLIENT) + " quic"
)

responses = list(
set(
getattr(p["quic"], "path_response.data")
for p in tr_client
if hasattr(p["quic"], "path_response.data")
)
responses = set(
getattr(p["quic"], "path_response.data")
for p in tr_client
if hasattr(p["quic"], "path_response.data")
)

unresponded = [c for c in challenges if c not in responses]
if unresponded != []:
logging.info("PATH_CHALLENGE without a PATH_RESPONSE: %s", unresponded)
return TestResult.FAILED
# Verify each new path has at least one PATH_CHALLENGE that was responded to.
# This tolerates packet loss while still proving path validation succeeded.
# Exclude the final path, since the peer may close the connection successfully
# without responding to a late-arriving PATH_CHALLENGE.
for path, challenges in path_challenges.items():
if path == final_path:
continue
if not any(c in responses for c in challenges):
logging.info(
"No PATH_RESPONSE received for any PATH_CHALLENGE on path %s "
"(challenges sent: %s)",
path,
challenges,
)
return TestResult.FAILED

return TestResult.SUCCEEDED

Expand Down
22 changes: 22 additions & 0 deletions trace.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
import asyncio
import datetime
import logging
from enum import Enum
from typing import List, Optional, Tuple

# Workaround for pyshark compatibility with Python 3.14+.
# Child watcher APIs were removed in Python 3.14.
if not hasattr(asyncio, "SafeChildWatcher"):

class _DummyChildWatcher:
def attach_loop(self, loop):
pass

def close(self):
pass

asyncio.SafeChildWatcher = _DummyChildWatcher
asyncio.set_child_watcher = lambda w: None
asyncio.get_child_watcher = _DummyChildWatcher

# Ensure an event loop exists for pyshark (required for Python 3.10+).
try:
asyncio.get_running_loop()
except RuntimeError:
asyncio.set_event_loop(asyncio.new_event_loop())

import pyshark

IP4_CLIENT = "193.167.0.100"
Expand Down
Loading