Repository navigation
Changed
Block2Accumulator's max_blocks default goes from 32 to 64. This is a public API change: a caller outside this package that took the old default now permits twice as many blocks under the same unchanged 64 KiB byte cap. Thanks @Mandevilla57, whose measurement on their own oven is why it moved.
probe_dtls_port can report fewer attempts than retries + 1, and discover_ocf_responder_ports can send fewer rounds than rounds. Both now decide before transmitting whether enough of the budget survives to read a reply in, and stop if it does not, so no datagram goes out that cannot be answered in time. Neither extends the bound its timeout documents.
Fixed
The Block2 block cap refused transfers that sat inside the byte cap beside it. @Mandevilla57 measured a 33,884-byte /device/0?if=oic.if.b batch from an LCD_A311D_OV_QMD_EU_22K oven on their own appliance: 34 blocks, refused with BlockwiseError at block 31 by a count of 32, while the payload cap next to it allowed 64 KiB, which is 64 blocks at the size this client asks for. Their fix scoped to DtlsCoapSession, which was right, because nothing measured approaches the other two places the same pair of bounds appeared.
Those other two carried the same disagreement, so the count is now derived from the payload cap at one place and read from there by all three: the session's bound, the module default every external caller gets, and the plaintext directory read in ocf_discovery. Both bounds now land on the same block, and a later change to the requested block size carries the count with it. Setting each one by hand would leave them free to drift apart again, which is how the first one arrived.
Two UDP loops sent a datagram and only then worked out whether any time was left to read a reply in. Both divided a total budget into fixed slices, computing the deadline after the flight had gone out, so an attempt whose read overran its own slice left the next one's deadline already in the past, and the next flight was sent into it anyway. A read does return somewhat past the timeout it was given, and that overrun is roughly constant while a slice is not, so a slice only has to be small for one overrun to swallow it whole. retries and rounds both accept up to four and timeout is the caller's to choose, so small slices are reachable through the public API.
Measured with a 40 ms overrun, the worst a UDP recvfrom on this machine overshot a 100 ms timeout:
| configuration | before | after | |
|---|---|---|---|
dtls_probe |
retries=4, timeout=0.15, 30 ms slices |
5 ClientHellos, 3 read for | 2 flights, both read for |
ocf_multicast |
rounds=4, timeout=0.12, 30 ms rounds |
9 datagrams, 1 round in 3 unread | 6 datagrams, every round read |
The probe's waste is one unicast ClientHello per unread attempt. The multicast loop sends three requests per round to the group, so a wasted round is three datagrams every device on the segment has to process with nothing waiting for an answer.
Neither is reachable at this package's defaults. probe_dtls_port defaults to timeout=3.0, retries=2, giving 1 s slices, and discover_ocf_responder_ports to timeout=3.0, rounds=2, giving 1.5 s windows. A 40 ms overrun is nowhere near either, and nothing in this package passes a short timeout.
DtlsPskClient.recv() raised on a stored failure before returning application data it had already buffered. One call drains every queued datagram, so an authenticated reply and a teardown that followed it are both processed before the caller sees either, and the reply was lost to the error. Data now comes first and the failure is raised on the call after the buffer empties, which is what the two stacks this engine stands in for do by construction: mbedtls_ssl_read returns after a single mbedtls_ssl_read_record, as does OpenSSL, so neither can retract data it has already handed over.
On the appliance firmware the sequence looks unreachable, so this fixes no behaviour that can be demonstrated against these devices. The only fatal-alert sender in its DTLS adapter is one macro, every one of that macro's call sites is in the handshake path, and the established-session read path removes the peer and returns a failure carrying no alert. An authenticated close_notify was always handled correctly.
The reassembly table's cap now guards the insert instead of the whole function, so it holds the documented number of incomplete handshake messages exactly, and a fragment of a message already being reassembled is accepted when the table is full.
Notes
1343 tests pass on 3.11 at this package's dependency floor and on 3.14.
Against a Samsung dryer and oven, the bridge demo ran 19 consecutive 60-second polling windows on each appliance, 18 minutes without an error, a timeout or a reconnection, discovering both secure ports from each device's own plaintext directory.
The retransmission divergence is not reproducible against an appliance, because a real recvfrom does not overshoot by a whole slice, so it is pinned in tests driven by an injected clock instead of on hardware. The wider block count rests on no measurement here: the largest plaintext directory measured on these appliances sits well inside the old bound, so that figure states what the code now permits and not something observed.
Thanks to @Jason-Morcos, whose report of an intermittent test failure on #121 is what surfaced the probe's retransmission loop.