Skip to content

Commit 3517764

Browse files
authored
Add support for DTLS timeouts (#1180)
Add support for DTLS timeouts When performing a DTLS handshake, the DTLS state machine may need to be updated based on the passage of time, for instance in response to packet loss. OpenSSL supports this by means of the `DTLSv1_get_timeout` and `DTLSv1_handle_timeout` methods, both of which are included in cryptography's bindings. This change adds Python wrappers for these methods in the `Connection` class.
1 parent 669969e commit 3517764

File tree

3 files changed

+76
-4
lines changed

3 files changed

+76
-4
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ Deprecations:
1616
Changes:
1717
^^^^^^^^
1818

19+
- Add ``OpenSSL.SSL.Connection.DTLSv1_get_timeout`` and ``OpenSSL.SSL.Connection.DTLSv1_handle_timeout``
20+
to support DTLS timeouts `#1180 <https://github.com/pyca/pyopenssl/pull/1180>`_.
21+
1922
23.0.0 (2023-01-01)
2023
-------------------
2124

src/OpenSSL/SSL.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2159,6 +2159,37 @@ def DTLSv1_listen(self):
21592159
if result < 0:
21602160
self._raise_ssl_error(self._ssl, result)
21612161

2162+
def DTLSv1_get_timeout(self):
2163+
"""
2164+
Determine when the DTLS SSL object next needs to perform internal
2165+
processing due to the passage of time.
2166+
2167+
When the returned number of seconds have passed, the
2168+
:meth:`DTLSv1_handle_timeout` method needs to be called.
2169+
2170+
:return: The time left in seconds before the next timeout or `None`
2171+
if no timeout is currently active.
2172+
"""
2173+
ptv_sec = _ffi.new("time_t *")
2174+
ptv_usec = _ffi.new("long *")
2175+
if _lib.Cryptography_DTLSv1_get_timeout(self._ssl, ptv_sec, ptv_usec):
2176+
return ptv_sec[0] + (ptv_usec[0] / 1000000)
2177+
else:
2178+
return None
2179+
2180+
def DTLSv1_handle_timeout(self):
2181+
"""
2182+
Handles any timeout events which have become pending on a DTLS SSL
2183+
object.
2184+
2185+
:return: `True` if there was a pending timeout, `False` otherwise.
2186+
"""
2187+
result = _lib.DTLSv1_handle_timeout(self._ssl)
2188+
if result < 0:
2189+
self._raise_ssl_error(self._ssl, result)
2190+
else:
2191+
return bool(result)
2192+
21622193
def bio_shutdown(self):
21632194
"""
21642195
If the Connection was created with a memory BIO, this method can be

tests/test_ssl.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import gc
1010
import select
1111
import sys
12+
import time
1213
import uuid
1314
from errno import (
1415
EAFNOSUPPORT,
@@ -4369,10 +4370,11 @@ class TestDTLS:
43694370
# new versions of OpenSSL, this is unnecessary, but harmless, because the
43704371
# DTLS state machine treats it like a network hiccup that duplicated a
43714372
# packet, which DTLS is robust against.
4372-
def test_it_works_at_all(self):
4373-
# arbitrary number larger than any conceivable handshake volley
4374-
LARGE_BUFFER = 65536
43754373

4374+
# Arbitrary number larger than any conceivable handshake volley.
4375+
LARGE_BUFFER = 65536
4376+
4377+
def test_it_works_at_all(self):
43764378
s_ctx = Context(DTLS_METHOD)
43774379

43784380
def generate_cookie(ssl):
@@ -4403,7 +4405,7 @@ def verify_cookie(ssl, cookie):
44034405

44044406
def pump_membio(label, source, sink):
44054407
try:
4406-
chunk = source.bio_read(LARGE_BUFFER)
4408+
chunk = source.bio_read(self.LARGE_BUFFER)
44074409
except WantReadError:
44084410
return False
44094411
# I'm not sure this check is needed, but I'm not sure it's *not*
@@ -4483,3 +4485,39 @@ def pump():
44834485
assert 0 < c.get_cleartext_mtu() < 500
44844486
except NotImplementedError: # OpenSSL 1.1.0 and earlier
44854487
pass
4488+
4489+
def test_timeout(self, monkeypatch):
4490+
c_ctx = Context(DTLS_METHOD)
4491+
c = Connection(c_ctx)
4492+
4493+
# No timeout before the handshake starts.
4494+
assert c.DTLSv1_get_timeout() is None
4495+
assert c.DTLSv1_handle_timeout() is False
4496+
4497+
# Start handshake and check there is data to send.
4498+
c.set_connect_state()
4499+
try:
4500+
c.do_handshake()
4501+
except SSL.WantReadError:
4502+
pass
4503+
assert c.bio_read(self.LARGE_BUFFER)
4504+
4505+
# There should now be an active timeout.
4506+
seconds = c.DTLSv1_get_timeout()
4507+
assert seconds is not None
4508+
4509+
# Handle the timeout and check there is data to send.
4510+
time.sleep(seconds)
4511+
assert c.DTLSv1_handle_timeout() is True
4512+
assert c.bio_read(self.LARGE_BUFFER)
4513+
4514+
# After the maximum number of allowed timeouts is reached,
4515+
# DTLSv1_handle_timeout will return -1.
4516+
#
4517+
# Testing this directly is prohibitively time consuming as the timeout
4518+
# duration is doubled on each retry, so the best we can do is to mock
4519+
# this condition.
4520+
monkeypatch.setattr(_lib, "DTLSv1_handle_timeout", lambda x: -1)
4521+
4522+
with pytest.raises(Error):
4523+
c.DTLSv1_handle_timeout()

0 commit comments

Comments
 (0)