Merge pull request #42 from Moballo-LLC/codex/py-08a1-completed-handshake

fix(protocol): retain completed DTLS handshakes
This commit is contained in:
Quite Yellow authored and GitHub committed 2026-08-17 19:48:44 +01:00
commit a3bec9470d
4 files changed
+23 -11

No files matched your search

+4
View File
@@ -57,6 +57,10 @@ retransmissions within that same deadline:
sess.connect(timeout=4.0) sess.connect(timeout=4.0)
``` ```
The deadline stops further setup, retries, and network waits. If OpenSSL
reports that the handshake completed at the deadline boundary, the completed
session is retained rather than torn down as a timeout.
Connection attempts can also use a one-way cancellation signal. The signal is Connection attempts can also use a one-way cancellation signal. The signal is
backed by a socketpair, so setting it wakes the network wait immediately while backed by a socketpair, so setting it wakes the network wait immediately while
OpenSSL retains control of DTLS retransmission timing: OpenSSL retains control of DTLS retransmission timing:
+5 -3
View File
@@ -34,14 +34,16 @@ def _drive_dtls_handshake(
bounded only by its deadline, while the diagnostic probe retains its bounded only by its deadline, while the diagnostic probe retains its
explicit retry budget. explicit retry budget.
Return ``True`` only when the handshake completes before the deadline. Return ``True`` once OpenSSL reports the handshake complete. The deadline
TLS and socket failures are left to the caller to classify. prevents another setup, retry, or network-wait iteration; it does not tear
down a session that completed while ``do_handshake()`` was running. TLS and
socket failures are left to the caller to classify.
""" """
retransmits = 0 retransmits = 0
while time.monotonic() < deadline: while time.monotonic() < deadline:
try: try:
connection.do_handshake() connection.do_handshake()
return time.monotonic() < deadline return True
except SSL.WantReadError: except SSL.WantReadError:
pass pass
+5 -3
View File
@@ -322,13 +322,15 @@ class DtlsCoapSession:
timeout: float | None = None, timeout: float | None = None,
cancel: ConnectCancellation | None = None, cancel: ConnectCancellation | None = None,
): ):
"""Perform a cancellable DTLS handshake within a monotonic deadline. """Perform a cancellable DTLS handshake using a monotonic deadline.
``timeout`` overrides ``HANDSHAKE_TIMEOUT_S`` for this call. OpenSSL ``timeout`` overrides ``HANDSHAKE_TIMEOUT_S`` for this call. OpenSSL
owns DTLS retransmission timing while every receive is capped by the owns DTLS retransmission timing while every receive is capped by the
remaining budget, so wall-clock adjustments cannot change the bound. remaining budget, so wall-clock adjustments cannot change the bound.
A ``ConnectCancellation`` wakes the network wait immediately and does Once OpenSSL reports completion, that completed session is retained
not alter an already established session. even if the call returns just after the deadline. A
``ConnectCancellation`` wakes the network wait immediately and does not
alter an already established session.
""" """
handshake_timeout = _validate_handshake_timeout( handshake_timeout = _validate_handshake_timeout(
timeout, self.HANDSHAKE_TIMEOUT_S) timeout, self.HANDSHAKE_TIMEOUT_S)
+9 -5
View File
@@ -323,12 +323,12 @@ def test_connect_services_openssl_retransmit_timer(monkeypatch):
@pytest.mark.parametrize("success_delay", (0.1, 0.2)) @pytest.mark.parametrize("success_delay", (0.1, 0.2))
def test_handshake_success_at_or_after_deadline_is_rejected( def test_handshake_success_at_or_after_deadline_is_retained(
monkeypatch, monkeypatch,
success_delay, success_delay,
): ):
clock = _Clock() clock = _Clock()
connection, sock, _endpoint, _open_calls = _install_handshake( connection, sock, endpoint, _open_calls = _install_handshake(
monkeypatch, monkeypatch,
clock, clock,
) )
@@ -338,7 +338,11 @@ def test_handshake_success_at_or_after_deadline_is_rejected(
connection.do_handshake = late_success connection.do_handshake = late_success
with pytest.raises(SessionTimeoutError): session = _session()
_session().connect(timeout=0.1) session.connect(timeout=0.1)
assert sock.closed assert session.conn is connection
assert session.sock is sock
assert session.endpoint is endpoint
assert session.dest == endpoint.sockaddr
assert not sock.closed