r/delphi • u/henrickhellstrom • 10h ago
We wrote our own QUIC/HTTP/3 stack in Object Pascal for Delphi, and found an OpenSSL QUIC bug along the way
Disclosure: I develop StreamSec Tools, a commercial security library for Delphi (TLS, SSH, X.509/PKIX, CMS, HTTP). Version 4.1.5.362 came out on 26 October and adds QUIC and HTTP/3. The QUIC part is written from scratch in Object Pascal, not wrapped around MsQuic or OpenSSL.
What's in it
- An RFC 9000 transport: packet protection, loss recovery, NewReno with pacing, connection migration, ECN, key updates, resumption and opt-in 0-RTT, on the library's own TLS 1.3 and crypto.
- Cross-platform: Win32/Win64, macOS, Linux64, iOS and Android, Delphi XE3 to 13 Florence (Linux from 10.3 Rio, mobile from 10.4 Sydney). IOCP, epoll and kqueue underneath.
- An HTTP/3 server on the same request router as the HTTP/1.1 and HTTP/2 servers, so existing handlers answer HTTP/3 unchanged, and an HTTP/3 client that finds a server's endpoint through Alt-Svc.
How fast is it?
Two Windows PCs over Wi-Fi, each library sending bulk data to each, 8 interleaved rounds per combination, medians. These are the runs sending to a StreamSec or MsQuic receiver:
| Sender | Throughput (MiB/s) | Sender CPU (CPU-s per GiB) |
|---|---|---|
| MsQuic 2.6.3 | 41.0 - 46.0 | about 3 |
| StreamSec | 35.3 - 41.0 | 11 - 16 |
| OpenSSL 3.5.9 | 22.5 - 31.8 | 9 - 15 |
So ahead of OpenSSL on throughput and at 82-95% of MsQuic, with CPU cost in OpenSSL's range. MsQuic is still far more CPU-efficient than either.
The OpenSSL bug
During interop testing, OpenSSL's QUIC client stalled against our server in every run. The cause: when the packet carrying an OpenSSL receiver's latest MAX_STREAM_DATA is lost, OpenSSL doesn't send the update again unless that packet also carried data for the stream. RFC 9000 section 13.3 says it must. With a short request and a long response, the download can stall for good.
We reproduced it with 3.5.7 and 3.5.9, with OpenSSL at both ends through a relay dropping 5% of the client's packets: 6 of 13 runs stalled unpatched, and 13 of 13 completed with a two-place patch. It's reported with the reproducer and the patch, and OpenSSL has triaged it as a bug on 3.5 through 4.1 and master: https://github.com/openssl/openssl/issues/33373 (the OpenSSL numbers above are with that patch applied).
Happy to answer questions about the implementation.
- Announcement, with the change log and SBOM: https://support.streamsec.net/forum/?cmd=viewtopic§ion_id=4&topic_id=16
- HTTPS, HTTP/2 and HTTP/3: https://www.streamsec.com/strsecivhttps.php
- TLS: https://www.streamsec.com/strsecivtls.php
- Evaluation builds: https://www.streamsec.com/downloadstrseciv.php