[PHP-DEV] [RFC idea] DTLS support in ext/openssl (Openssl\Dtls)

Hi all,

I’d like to gauge interest in adding DTLS support to ext/openssl before
writing a formal RFC.

Problem

PHP can speak TLS, but only as a stream wrapper where OpenSSL owns the
socket end to end. There are two gaps:

  • No DTLS (RFC 6347, the UDP counterpart of TLS) at all.
  • No way to run (D)TLS over a transport the application controls.

Several modern protocols are built on DTLS where the application must own
the packet flow: WebRTC data channels and DTLS-SRTP (RFC 5764), CoAP over
DTLS, SIP media keying, and any case where DTLS records are multiplexed
with other traffic on one UDP port. Today these are only reachable from
PHP through FFI to libssl or an external process. DTLS has been an open
request since 2018 (bug #76629) with no implementation so far.

Proposal (sketch)

A small, transport-agnostic DTLS endpoint class whose packet I/O goes
through memory BIOs, so the application moves datagrams in/out itself:

namespace Openssl;

final class Dtls {
public const int HANDSHAKE_ERROR = -1;
public const int HANDSHAKE_CONTINUE = 0;
public const int HANDSHAKE_FINISHED = 1;

public function __construct(bool $isServer = false,
?string $certificate = null, ?string $privateKey = null) {}

public function getFingerprint(?string $digestAlgo = null): string {}
public function getPeerFingerprint(?string $digestAlgo = null): ?string {}
public function isHandshakeFinished(): bool {}
public function handshake(): int {} // HANDSHAKE_* constants
public function feed(string $datagram): int {}
public function pull(): ?string {}
public function write(string $data): int {}
public function read(): string|false {}
public function exportKeys(string $label, int $length): string|false {}
}

The application drives the handshake by pumping datagrams between peers
(feed/pull), verifies the peer out of band via getPeerFingerprint(), then
exchanges data with write()/read(). exportKeys() exposes
SSL_export_keying_material (RFC 5705), which is what DTLS-SRTP needs.

Status

I’ve already written a working proof-of-concept against master, with
.phpt tests; the full ext/openssl suite is green. I’m sharing it only to
make the discussion concrete – I’m very open to changing the shape (this
is the point of asking first), and I’ll only open a PR/RFC properly if
there’s interest. I can post the branch/PR link on request.

Questions

  • Is DTLS something we want in core ext/openssl, or is this better left
    to userland/FFI?
  • Is the object + memory-BIO design the right direction? Should the
    constructor also accept OpenSSLCertificate/OpenSSLAsymmetricKey
    objects in addition to PEM?
  • How should we treat the server-side HelloVerifyRequest cookie
    exchange (DoS amplification mitigation) – required for v1, or
    acceptable as future scope with a documented caveat?
  • (Cc ext/openssl maintainers – your view would be especially valuable.)

Thanks for any feedback.

Gianfrancesco Aurecchia

Hi Gianfrancesco,

I don't have any expertise of the actual protocol, but do have an opinion on the PHP surface design. I apologise if the below feels too much like bikeshedding...

On 24 June 2026 12:45:05 BST, Gianfrancesco Aurecchia <gianfri.aur@gmail.com> wrote:

Should the constructor also accept
OpenSSLCertificate/OpenSSLAsymmetricKey
objects in addition to PEM?

I would say *instead of* a string. Accepting a string means the constructor has to document the expected format, produce appropriate parse errors, etc; accepting an object reduces that to a standard type check. And if the user already has an object, serialising it and then reparsing it is duplicated effort.

In general, I would prefer a design which used "real" types throughout instead of strings and ints, except where performance is critical:

       public const int HANDSHAKE_ERROR = -1;
       public const int HANDSHAKE_CONTINUE = 0;
       public const int HANDSHAKE_FINISHED = 1;

These should be cases on an enum; probably unbacked, unless there's some reason applications need to work with these integer codes.

       public function getFingerprint(?string $digestAlgo = null): string

Perhaps the supported algorithms could be an enum as well, backed by string.

public function feed(string $datagram): int {}
public function pull(): ?string {}

If $datagram has to be in a specific format, would a lightweight DtlsDatagram object be useful? Again, it would delegate the parsing/validation to a specific factory, and give flexibility for extra ways of creating or reusing material without going via strings and revalidating.

       public function read(): string|false {}
       public function exportKeys(string $label, int $length):
string|false {}

If the false case here is an expected condition, like EOF, "?string" might be better. If it's just a signal for unexpected errors, throwing an exception would be better.

Hopefully someone with more knowledge of the problem space can comment more on the core idea.

Thanks,

Rowan Tommins
[IMSoP]

Hi,

On Wed, Jun 24, 2026 at 1:45 PM Gianfrancesco Aurecchia <gianfri.aur@gmail.com> wrote:

Hi all,

I’d like to gauge interest in adding DTLS support to ext/openssl before
writing a formal RFC.

Problem

PHP can speak TLS, but only as a stream wrapper where OpenSSL owns the
socket end to end. There are two gaps:

  • No DTLS (RFC 6347, the UDP counterpart of TLS) at all.
  • No way to run (D)TLS over a transport the application controls.

Several modern protocols are built on DTLS where the application must own
the packet flow: WebRTC data channels and DTLS-SRTP (RFC 5764), CoAP over
DTLS, SIP media keying, and any case where DTLS records are multiplexed
with other traffic on one UDP port. Today these are only reachable from
PHP through FFI to libssl or an external process. DTLS has been an open
request since 2018 (bug #76629) with no implementation so far.

Proposal (sketch)

A small, transport-agnostic DTLS endpoint class whose packet I/O goes
through memory BIOs, so the application moves datagrams in/out itself:

namespace Openssl;

final class Dtls {
public const int HANDSHAKE_ERROR = -1;
public const int HANDSHAKE_CONTINUE = 0;
public const int HANDSHAKE_FINISHED = 1;

public function __construct(bool $isServer = false,
?string $certificate = null, ?string $privateKey = null) {}

public function getFingerprint(?string $digestAlgo = null): string {}
public function getPeerFingerprint(?string $digestAlgo = null): ?string {}
public function isHandshakeFinished(): bool {}
public function handshake(): int {} // HANDSHAKE_* constants
public function feed(string $datagram): int {}
public function pull(): ?string {}
public function write(string $data): int {}
public function read(): string|false {}
public function exportKeys(string $label, int $length): string|false {}
}

The application drives the handshake by pumping datagrams between peers
(feed/pull), verifies the peer out of band via getPeerFingerprint(), then
exchanges data with write()/read(). exportKeys() exposes
SSL_export_keying_material (RFC 5705), which is what DTLS-SRTP needs.

Status

I’ve already written a working proof-of-concept against master, with
.phpt tests; the full ext/openssl suite is green. I’m sharing it only to
make the discussion concrete – I’m very open to changing the shape (this
is the point of asking first), and I’ll only open a PR/RFC properly if
there’s interest. I can post the branch/PR link on request.

Questions

  • Is DTLS something we want in core ext/openssl, or is this better left
    to userland/FFI?

I think it can be exposed in ext/openssl but not in this way (see below)

  • Is the object + memory-BIO design the right direction? Should the
    constructor also accept OpenSSLCertificate/OpenSSLAsymmetricKey
    objects in addition to PEM?

I would prefer just stream exposure so dtls:// stream instead. See udp:// for how it would be used. I haven’t done any testing with udp streams but the design should be similar like we have for tls that is build on top of tcp. It might not be exactly small work to do it cleanly though.

  • How should we treat the server-side HelloVerifyRequest cookie
    exchange (DoS amplification mitigation) – required for v1, or
    acceptable as future scope with a documented caveat?

we could possibly add some context option for it but haven’t thought much about it.

Kind regards,

Jakub

Hi Jakub,

Thanks a lot for looking at this. Your view as the ext/openssl maintainer is exactly what I was hoping for.

I’ll be honest: I hadn’t considered exposing DTLS as a stream wrapper (a ‘dtls://’ built like ‘tls://’ is over ‘tcp://’). It’s a really good idea. For the common case of a dedicated UDP socket that carries only DTLS, that’s clearly the most idiomatic and discoverable API in PHP, and much closer to what people already know from ‘tls://’ than what I proposed.

So rather than seeing it as an alternative, I think we should take the opportunity and do both, because the two genuinely complement each other and cover different needs:

  • dtls://’ stream wrapper: owns a UDP socket, mirrors ‘tls://’. The right default for the simple “one socket, DTLS only” case (CoAP-style, point-to-point, a DTLS server on its own port, etc.).
  • Low-level, application-driven engine (memory BIOs, ‘feed()’/‘pull()’): for the cases where the application must own packet I/O.

That second layer isn’t a stylistic preference; it’s a hard requirement of the motivating use case. In WebRTC, DTLS records are multiplexed with other protocols on the same UDP 5-tuple: STUN (ICE) and SRTP/SCTP (media/data) share one socket and are demultiplexed by the application from the first byte of each datagram (RFC 7983 / RFC 5764). A ‘dtls://’ stream that owns its socket can’t serve that, since it would swallow the STUN and SRTP/SCTP packets too, with nowhere for the application to route them. The same holds for anything that shares a transport with DTLS.

The nice part is that these aren’t two separate implementations. The stream wrapper can be built on top of the same low-level engine: the engine drives the handshake and the records, and the wrapper just adds a UDP socket and the familiar stream API around it. One core, two surfaces: the stream for the 90% simple case (exactly the ‘tls://’-style ergonomics you have in mind), and the driver for the multiplexed or embedded cases the stream can’t reach (WebRTC, and anything where DTLS coexists with other traffic on one socket).

Does that direction sound right to you? I’m very flexible on the surface API. Rowan’s points about certificate/key objects and enums fit either shape, and I’ll fold them in. The only property I’d want to preserve is the ability to run DTLS without owning the socket, so the WebRTC case (the main motivation) stays on the table.

For context, I already have the application-driven engine working end-to-end against Chrome and Firefox via a pure-PHP WebRTC data channel, so I can speak to the multiplexing requirement concretely: https://github.com/GianfriAur/php-webrtc-datachannel

Since the engine is already prototyped, I’m happy to also draft the ‘dtls://’ stream wrapper on top of it, so we can evaluate both surfaces against real code rather than in the abstract. If the layered direction sounds reasonable to you, I’ll put together a proof of concept of the wrapper and share it.

Thanks again, and let me know what you think.

Gianfrancesco

Il giorno mer 24 giu 2026 alle ore 23:44 Jakub Zelenka <bukka@php.net> ha scritto:

Hi,

On Wed, Jun 24, 2026 at 1:45 PM Gianfrancesco Aurecchia <gianfri.aur@gmail.com> wrote:

Hi all,

I’d like to gauge interest in adding DTLS support to ext/openssl before
writing a formal RFC.

Problem

PHP can speak TLS, but only as a stream wrapper where OpenSSL owns the
socket end to end. There are two gaps:

  • No DTLS (RFC 6347, the UDP counterpart of TLS) at all.
  • No way to run (D)TLS over a transport the application controls.

Several modern protocols are built on DTLS where the application must own
the packet flow: WebRTC data channels and DTLS-SRTP (RFC 5764), CoAP over
DTLS, SIP media keying, and any case where DTLS records are multiplexed
with other traffic on one UDP port. Today these are only reachable from
PHP through FFI to libssl or an external process. DTLS has been an open
request since 2018 (bug #76629) with no implementation so far.

Proposal (sketch)

A small, transport-agnostic DTLS endpoint class whose packet I/O goes
through memory BIOs, so the application moves datagrams in/out itself:

namespace Openssl;

final class Dtls {
public const int HANDSHAKE_ERROR = -1;
public const int HANDSHAKE_CONTINUE = 0;
public const int HANDSHAKE_FINISHED = 1;

public function __construct(bool $isServer = false,
?string $certificate = null, ?string $privateKey = null) {}

public function getFingerprint(?string $digestAlgo = null): string {}
public function getPeerFingerprint(?string $digestAlgo = null): ?string {}
public function isHandshakeFinished(): bool {}
public function handshake(): int {} // HANDSHAKE_* constants
public function feed(string $datagram): int {}
public function pull(): ?string {}
public function write(string $data): int {}
public function read(): string|false {}
public function exportKeys(string $label, int $length): string|false {}
}

The application drives the handshake by pumping datagrams between peers
(feed/pull), verifies the peer out of band via getPeerFingerprint(), then
exchanges data with write()/read(). exportKeys() exposes
SSL_export_keying_material (RFC 5705), which is what DTLS-SRTP needs.

Status

I’ve already written a working proof-of-concept against master, with
.phpt tests; the full ext/openssl suite is green. I’m sharing it only to
make the discussion concrete – I’m very open to changing the shape (this
is the point of asking first), and I’ll only open a PR/RFC properly if
there’s interest. I can post the branch/PR link on request.

Questions

  • Is DTLS something we want in core ext/openssl, or is this better left
    to userland/FFI?

I think it can be exposed in ext/openssl but not in this way (see below)

  • Is the object + memory-BIO design the right direction? Should the
    constructor also accept OpenSSLCertificate/OpenSSLAsymmetricKey
    objects in addition to PEM?

I would prefer just stream exposure so dtls:// stream instead. See udp:// for how it would be used. I haven’t done any testing with udp streams but the design should be similar like we have for tls that is build on top of tcp. It might not be exactly small work to do it cleanly though.

  • How should we treat the server-side HelloVerifyRequest cookie
    exchange (DoS amplification mitigation) – required for v1, or
    acceptable as future scope with a documented caveat?

we could possibly add some context option for it but haven’t thought much about it.

Kind regards,

Jakub

Hi Rowan,

Thanks, this is exactly the kind of API-shaping feedback I was hoping for.

Things I’m happy to take:

  • Certificate and key as ‘OpenSSLCertificate’ / ‘OpenSSLAsymmetricKey’ objects rather than PEM strings: yes, that aligns with the rest of ext/openssl. I’d probably still accept PEM as a convenience, but make the objects the primary form.
  • Returning ‘null’ rather than ‘false’ for an expected empty result (no datagram pending, no application data yet): agreed, null is clearer there.

On the enums (the handshake state, and digest selection): I like enums, and personally I’d reach for them too. My hesitation is that I’d rather not diverge from how ext/openssl is consolidated today: the extension exposes this kind of thing as integer constants (‘OPENSSL_ALGO_', 'OPENSSL_’) and selects digests by name (string), not through enums. Introducing enums just for DTLS would create a new convention in one corner of the extension. Whether ext/openssl should move towards enums more broadly feels like a maintainer-level decision, and Jakub’s feedback would be valuable there; he may well already be considering something along these lines. I’d keep that question separate from this proposal, so the DTLS work doesn’t hinge on a wider style change, and for now follow the extension’s existing convention.

On a ‘DtlsDatagram’ value object vs raw strings: here I’d lean towards keeping raw strings, for two reasons. First, ‘feed()’ and ‘pull()’ sit on the hot path (one call per packet), so wrapping every datagram in an object adds allocation for what is essentially a byte buffer. Second, binary I/O across PHP (fread/fwrite, the socket and stream functions, the openssl_* functions) is consistently done with strings, so a datagram object would be a bit of an outlier; PHP strings already are the byte-buffer type. I’m open if there’s a concrete benefit I’m missing, for example carrying metadata alongside the bytes.

On direction: Jakub suggested also exposing a ‘dtls://’ stream wrapper alongside the low-level engine, which I think is a good idea. I’m waiting for his feedback on that before settling the overall shape. Your points apply to the engine/class surface either way, and I’ll fold the agreed ones in.

Thanks again for the careful read.

Gianfrancesco

Il giorno mer 24 giu 2026 alle ore 23:08 Rowan Tommins [IMSoP] <imsop.php@rwec.co.uk> ha scritto:

Hi Gianfrancesco,

I don’t have any expertise of the actual protocol, but do have an opinion on the PHP surface design. I apologise if the below feels too much like bikeshedding…

On 24 June 2026 12:45:05 BST, Gianfrancesco Aurecchia <gianfri.aur@gmail.com> wrote:

Should the constructor also accept
OpenSSLCertificate/OpenSSLAsymmetricKey
objects in addition to PEM?

I would say instead of a string. Accepting a string means the constructor has to document the expected format, produce appropriate parse errors, etc; accepting an object reduces that to a standard type check. And if the user already has an object, serialising it and then reparsing it is duplicated effort.

In general, I would prefer a design which used “real” types throughout instead of strings and ints, except where performance is critical:

public const int HANDSHAKE_ERROR = -1;
public const int HANDSHAKE_CONTINUE = 0;
public const int HANDSHAKE_FINISHED = 1;

These should be cases on an enum; probably unbacked, unless there’s some reason applications need to work with these integer codes.

public function getFingerprint(?string $digestAlgo = null): string

Perhaps the supported algorithms could be an enum as well, backed by string.

public function feed(string $datagram): int {}
public function pull(): ?string {}

If $datagram has to be in a specific format, would a lightweight DtlsDatagram object be useful? Again, it would delegate the parsing/validation to a specific factory, and give flexibility for extra ways of creating or reusing material without going via strings and revalidating.

public function read(): string|false {}
public function exportKeys(string $label, int $length):
string|false {}

If the false case here is an expected condition, like EOF, “?string” might be better. If it’s just a signal for unexpected errors, throwing an exception would be better.

Hopefully someone with more knowledge of the problem space can comment more on the core idea.

Thanks,

Rowan Tommins
[IMSoP]

Hi,

please don’t top post.

On Thu, Jun 25, 2026 at 10:13 AM Gianfrancesco Aurecchia <gianfri.aur@gmail.com> wrote:

Hi Jakub,

Thanks a lot for looking at this. Your view as the ext/openssl maintainer is exactly what I was hoping for.

I’ll be honest: I hadn’t considered exposing DTLS as a stream wrapper (a ‘dtls://’ built like ‘tls://’ is over ‘tcp://’). It’s a really good idea. For the common case of a dedicated UDP socket that carries only DTLS, that’s clearly the most idiomatic and discoverable API in PHP, and much closer to what people already know from ‘tls://’ than what I proposed.

I thought about this and one thing to note is that this should be future proof solution that will support DTLS 1.3 which is currently in works and should be part of OpenSSL 4.1. Currently it doesn’t look that it will support DTLSv1_listen so I would recommend to study it’s implementation to make sure that the design is good for 1.3 as well. See feature/dtls-1.3 and this PR (handling the listen logic) for more details: https://github.com/openssl/openssl/pull/31137 .

So rather than seeing it as an alternative, I think we should take the opportunity and do both, because the two genuinely complement each other and cover different needs:

  • dtls://’ stream wrapper: owns a UDP socket, mirrors ‘tls://’. The right default for the simple “one socket, DTLS only” case (CoAP-style, point-to-point, a DTLS server on its own port, etc.).
  • Low-level, application-driven engine (memory BIOs, ‘feed()’/‘pull()’): for the cases where the application must own packet I/O.

Definitely not do both of them together as it would be too much to reveiw and I don’t think the low level solution is optimal anyway (see below).

That second layer isn’t a stylistic preference; it’s a hard requirement of the motivating use case. In WebRTC, DTLS records are multiplexed with other protocols on the same UDP 5-tuple: STUN (ICE) and SRTP/SCTP (media/data) share one socket and are demultiplexed by the application from the first byte of each datagram (RFC 7983 / RFC 5764). A ‘dtls://’ stream that owns its socket can’t serve that, since it would swallow the STUN and SRTP/SCTP packets too, with nowhere for the application to route them. The same holds for anything that shares a transport with DTLS.

We are actually looking to the IO hooks where we plan to add support for IO offloading which would basically need to use custom SSL BIO . It was initially meant for io_uring but if allowed limitation per stream, then it should cover your use without exposing some extra unnecessary class.

The nice part is that these aren’t two separate implementations. The stream wrapper can be built on top of the same low-level engine: the engine drives the handshake and the records, and the wrapper just adds a UDP socket and the familiar stream API around it. One core, two surfaces: the stream for the 90% simple case (exactly the ‘tls://’-style ergonomics you have in mind), and the driver for the multiplexed or embedded cases the stream can’t reach (WebRTC, and anything where DTLS coexists with other traffic on one socket).

I’m not sure it would be overlapping that much.

Does that direction sound right to you? I’m very flexible on the surface API. Rowan’s points about certificate/key objects and enums fit either shape, and I’ll fold them in. The only property I’d want to preserve is the ability to run DTLS without owning the socket, so the WebRTC case (the main motivation) stays on the table.

Yeah and that’s exactly what will be possible to do with IO offloading without any need for any extra API.

For context, I already have the application-driven engine working end-to-end against Chrome and Firefox via a pure-PHP WebRTC data channel, so I can speak to the multiplexing requirement concretely: https://github.com/GianfriAur/php-webrtc-datachannel

Since the engine is already prototyped, I’m happy to also draft the ‘dtls://’ stream wrapper on top of it, so we can evaluate both surfaces against real code rather than in the abstract. If the layered direction sounds reasonable to you, I’ll put together a proof of concept of the wrapper and share it.

I’d prefer to start only with stream and then we can discuss the offloading but for that stream implementation is prerequisite.

Kind regards,

Jakub

Hi Jakub

Il giorno sab 27 giu 2026 alle ore 19:09 Jakub Zelenka <bukka@php.net> ha scritto:

Hi,

please don’t top post.

I apologize for top-posting in my previous replies, bad habits.

I thought about this and one thing to note is that this should be future proof solution that will support DTLS 1.3 which is currently in works and should be part of OpenSSL 4.1. Currently it doesn’t look that it will support DTLSv1_listen so I would recommend to study it’s implementation to make sure that the design is good for 1.3 as well. See feature/dtls-1.3 and this PR (handling the listen logic) for more details: https://github.com/openssl/openssl/pull/31137 .

Good call. I’ll study the feature/dtls-1.3 branch and the listen PR (#31137) before settling on any shape

Definitely not do both of them together as it would be too much to review and I don’t think the low level solution is optimal anyway (see below).

Agreed. With offloading heading this way, a parallel low-level class doesn’t earn its keep, let’s scope this to the dtls:// stream wrapper first and treat offloading as the follow-up, with the stream as its prerequisite. I’ll drop the standalone engine from the proposal.

We are actually looking to the IO hooks where we plan to add support for IO offloading which would basically need to use custom SSL BIO . It was initially meant for io_uring but if allowed limitation per stream, then it should cover your use without exposing some extra unnecessary class.

Works for me in principle: if the offloading hooks let the application provide the I/O for an individual stream, my case is covered without a separate API.

I’m not sure it would be overlapping that much.

Fair. And if offloading covers the app-driven case anyway, there’s no second surface left for it to overlap with.

Yeah and that’s exactly what will be possible to do with IO offloading without any need for any extra API.

Good, then I’ll drop my architecture entirely and go with your approach: a single dtls:// stream wrapper, with the multiplexed case handled later through offloading rather than a separate low-level surface. No second class to maintain or review.

I’d prefer to start only with stream and then we can discuss the offloading but for that stream implementation is prerequisite.

On process: would you rather I open a formal RFC now, or hold off until I’ve prototyped the dtls:// wrapper on top of the polling/offloading primitives and we’ve agreed on the shape? Since the wrapper depends on those landing first, my instinct is to settle the design and get a working prototype before formalising an RFC rather than opening a concept-only one now, but I’ll follow whatever sequencing you prefer.

And since you maintain ext/openssl and own the stream rework this sits on: if I take on the dtls:// wrapper once the primitives are far enough along, would you be open to steering the design and reviewing the implementation? I’m glad to do the implementation work myself — I’d just want your direction and review so it fits the streams architecture you have in mind.

Kind regards,

Jakub

Thanks,
Gianfrancesco

Hi,

On Mon, Jun 29, 2026 at 10:55 AM Gianfrancesco Aurecchia <gianfri.aur@gmail.com> wrote:

On process: would you rather I open a formal RFC now, or hold off until I’ve prototyped the dtls:// wrapper on top of the polling/offloading primitives and we’ve agreed on the shape? Since the wrapper depends on those landing first, my instinct is to settle the design and get a working prototype before formalising an RFC rather than opening a concept-only one now, but I’ll follow whatever sequencing you prefer.

I’m not sure this even needs RFC. By default it doesn’t - only if any core dev requests it. But for me the user space exposure will be relatively small and we have got already some API standards from Session RFC so we might not need RFC. It should just start with PR though.

And since you maintain ext/openssl and own the stream rework this sits on: if I take on the dtls:// wrapper once the primitives are far enough along, would you be open to steering the design and reviewing the implementation? I’m glad to do the implementation work myself — I’d just want your direction and review so it fits the streams architecture you have in mind.

Yeah I can help with review but my time is quite limited so might be a bit slow over the summer.

Kind regards,

Jakub

Hi Jakub,

Il giorno lun 29 giu 2026 alle ore 12:21 Jakub Zelenka <bukka@php.net> ha scritto:

Hi,

I’m not sure this even needs RFC. By default it doesn’t - only if any core dev requests it. But for me the user space exposure will be relatively small and we have got already some API standards from Session RFC so we might not need RFC. It should just start with PR though.

Sounds like a good plan. I’ll skip the RFC, following the API conventions of the Session RFC (https://wiki.php.net/rfc/tls_session_resumption_api). I’ll first study feature/dtls-1.3 and #31137 so the design is valid for version 1.3, and I’ll come back to this thread once I have a working draft PR with the feature in place for review. We’ll take it from there.

Yeah I can help with review but my time is quite limited so might be a bit slow over the summer.

Of course, no problem at all, take whatever time you need. I’m in no rush and happy to work around your availability over the summer.

Kind regards

Gianfrancesco

Hi Jakub,

Quick follow-up:

Small change of plan from my last message: instead of validating for DTLS 1.3 first, I used some free time these days to get a working 1.2 draft up. DTLS turned out to be a bit trickier than I’d imagined, but something sensible came out of it, and I’d really value your read on the direction before I invest in 1.3 (which needs OpenSSL 4.1, still unreleased). If you’d rather I sort out 1.3 first as I said, just tell me.

I already have something you can build and test (branch below). In the meantime let me walk you through the choices, and whenever you think it’s the right moment I’ll prepare the draft PR.

Approach

A single ‘dtls://’ wrapper (plus ‘dtlsv1.2://’), as you suggested, no dedicated class. It lives in a self-contained ‘xp_dtls.c’: I don’t touch ‘xp_ssl.c’. I reuse the naming and conventions of ‘xp_ssl.c’ as much as possible (the ‘GET_VER_OPT’ macros, the ‘sockop_*’ handlers, the passphrase callback, the same struct with ‘php_netstream_data_t’ as the first member), so the file feels familiar to anyone who already knows ‘xp_ssl’ and the review is quicker. But isolated: everyone’s TLS path stays at zero risk and the diff is small and reviewable.

Another reason I didn’t put it inside ‘xp_ssl.c’: that one belongs to the TCP context (byte stream), while DTLS lives over UDP (datagrams). It’s a different transport context, so a dedicated file is its natural home.

Architectural choices

In short:

  • Stream-only, no new PHP functions: just constants, context options and metadata; everything goes through existing functions.
  • Server: one peer per accepted socket, to avoid ‘SO_REUSEPORT’ (I’ll leave the multi-peer case to the offloading work, as you mentioned).
  • Floor at DTLS 1.2 (1.0 is deprecated, RFC 8996). I’ll add DTLS 1.3 before the PR (I still have to get my hands on it), but I’d rather have your feedback on the 1.2 setup first, so I don’t invest in 1.3 on a base you might want different.
  • Session resumption: I plan to align with the RFC (https://wiki.php.net/rfc/tls_session_resumption_api) extending it to DTLS. Here too I’ll dive in after your first feedback, like for 1.3, so I don’t build on it before knowing whether the direction works for you.
  • ‘local_cert’/‘local_pk’ as PEM file paths, consistent with ‘tls://’.

The only point worth expanding on is the core change: to make ‘enable_crypto’ work over a ‘udp://’ (the way ‘ext/openssl’ overrides ‘tcp://’ for TLS) you need the ‘STREAM_CRYPTO_METHOD_DTLS_*’ constants in ‘main/streams/php_stream_transport.h’. They’re purely additive (no existing logic touched) and it’s the only point outside ‘ext/openssl’. I’ve already implemented it, but I’m not 100% sure it’s the right approach: I’d lean toward deferring it and keeping the first round to the ‘dtls://’ scheme only (zero core), leaving the ‘enable_crypto’ over ‘udp://’ layer for later. But if you take a look and it convinces you, all the better to keep it in.

The branch is here: https://github.com/GianfriAur/php-src/tree/feature/openssl-dtls-stream (branch ‘feature/openssl-dtls-stream’).

Where I’d like your input

  1. The additive core touch for ‘enable_crypto’ over ‘udp://’: I’ve implemented it but I’m not 100% sure it’s fine. I’d lean toward deferring it (first round ‘dtls://’ scheme only), but if you take a look and it convinces you, all the better to keep it in. What do you think?
  2. Does the 1.2 approach look right to you? (I’ll add DTLS 1.3 and OpenSSL 4.1 before the PR, I just wanted your OK on the direction before investing in it.)
  3. On certs/keys: do I stay on PEM paths like ‘tls://’, or is it worth going for objects right away?

No rush given the summer, and honestly I won’t be as lucky with free time myself in the coming days either: I’ll take my cue from your feedback before formalizing anything. Thanks again for offering to take a look.

Kind regards

Gianfrancesco

Hi,

On Wed, Jul 1, 2026 at 5:34 PM Gianfrancesco Aurecchia <gianfri.aur@gmail.com> wrote:

Hi Jakub,

Quick follow-up:

Small change of plan from my last message: instead of validating for DTLS 1.3 first, I used some free time these days to get a working 1.2 draft up. DTLS turned out to be a bit trickier than I’d imagined, but something sensible came out of it, and I’d really value your read on the direction before I invest in 1.3 (which needs OpenSSL 4.1, still unreleased). If you’d rather I sort out 1.3 first as I said, just tell me.

So the reason why I mentioned to check DTLS 1.3 is because DTLSv1_listen is not currently supported there so it would be probably better to go through SSL_accept (with SSL_OP_COOKIE_EXCHANGE)). So the same should be used for DTLS 1.2 if possible.

I already have something you can build and test (branch below). In the meantime let me walk you through the choices, and whenever you think it’s the right moment I’ll prepare the draft PR.

Approach

A single ‘dtls://’ wrapper (plus ‘dtlsv1.2://’), as you suggested, no dedicated class. It lives in a self-contained ‘xp_dtls.c’: I don’t touch ‘xp_ssl.c’. I reuse the naming and conventions of ‘xp_ssl.c’ as much as possible (the ‘GET_VER_OPT’ macros, the ‘sockop_*’ handlers, the passphrase callback, the same struct with ‘php_netstream_data_t’ as the first member), so the file feels familiar to anyone who already knows ‘xp_ssl’ and the review is quicker. But isolated: everyone’s TLS path stays at zero risk and the diff is small and reviewable.

Another reason I didn’t put it inside ‘xp_ssl.c’: that one belongs to the TCP context (byte stream), while DTLS lives over UDP (datagrams). It’s a different transport context, so a dedicated file is its natural home.

I can see the reasoning for separating but there seems to be lots of duplications so it might be good to have some xp_common.c with some shared helpers and restructure it a bit. But this can be probably added later. Makes sense to start with this and then we can see what could be shared.

Architectural choices

In short:

  • Stream-only, no new PHP functions: just constants, context options and metadata; everything goes through existing functions.
  • Server: one peer per accepted socket, to avoid ‘SO_REUSEPORT’ (I’ll leave the multi-peer case to the offloading work, as you mentioned).
  • Floor at DTLS 1.2 (1.0 is deprecated, RFC 8996). I’ll add DTLS 1.3 before the PR (I still have to get my hands on it), but I’d rather have your feedback on the 1.2 setup first, so I don’t invest in 1.3 on a base you might want different.
  • Session resumption: I plan to align with the RFC (https://wiki.php.net/rfc/tls_session_resumption_api) extending it to DTLS. Here too I’ll dive in after your first feedback, like for 1.3, so I don’t build on it before knowing whether the direction works for you.

It’s not necessary but might be better to include it or at least research to get an idea about the whole design.

  • ‘local_cert’/‘local_pk’ as PEM file paths, consistent with ‘tls://’.

The only point worth expanding on is the core change: to make ‘enable_crypto’ work over a ‘udp://’ (the way ‘ext/openssl’ overrides ‘tcp://’ for TLS) you need the ‘STREAM_CRYPTO_METHOD_DTLS_*’ constants in ‘main/streams/php_stream_transport.h’. They’re purely additive (no existing logic touched) and it’s the only point outside ‘ext/openssl’. I’ve already implemented it, but I’m not 100% sure it’s the right approach: I’d lean toward deferring it and keeping the first round to the ‘dtls://’ scheme only (zero core), leaving the ‘enable_crypto’ over ‘udp://’ layer for later. But if you take a look and it convinces you, all the better to keep it in.

The branch is here: https://github.com/GianfriAur/php-src/tree/feature/openssl-dtls-stream (branch ‘feature/openssl-dtls-stream’).

Please create a draft PR for this so we can discuss it there. It’s much easier than here.

Where I’d like your input

  1. The additive core touch for ‘enable_crypto’ over ‘udp://’: I’ve implemented it but I’m not 100% sure it’s fine. I’d lean toward deferring it (first round ‘dtls://’ scheme only), but if you take a look and it convinces you, all the better to keep it in. What do you think?

I guess it would be good to support so it’s consistent but it’s not necessary if it would be a pain to do.

  1. Does the 1.2 approach look right to you? (I’ll add DTLS 1.3 and OpenSSL 4.1 before the PR, I just wanted your OK on the direction before investing in it.)

I haven’t had time for a deep look but from what I see, the main issue might be usage DTLSv1_listen as I mentioned. I will try to check it out more next week and will comment on the PR.

  1. On certs/keys: do I stay on PEM paths like ‘tls://’, or is it worth going for objects right away?

Keep it consistent with tls to not over complicate it.

No rush given the summer, and honestly I won’t be as lucky with free time myself in the coming days either: I’ll take my cue from your feedback before formalizing anything. Thanks again for offering to take a look.

I should be able to check in my short DTLS slot which I have every week (except holiday) around Friday.

Kind regards,

Jakub

That second layer isn’t a stylistic preference; it’s a hard requirement of the motivating use case. In WebRTC, DTLS records are multiplexed with other protocols on the same UDP 5-tuple: STUN (ICE) and SRTP/SCTP (media/data) share one socket and are demultiplexed by the application from the first byte of each datagram (RFC 7983 / RFC 5764). A ‘dtls://’ stream that owns its socket can’t serve that, since it would swallow the STUN and SRTP/SCTP packets too, with nowhere for the application to route them. The same holds for anything that shares a transport with DTLS.

We are actually looking to the IO hooks where we plan to add support for IO offloading which would basically need to use custom SSL BIO . It was initially meant for io_uring but if allowed limitation per stream, then it should cover your use without exposing some extra unnecessary class.

Just a little follow up on this. I spoke with Arnaud, who is working on IO hooks, and he got actually a better idea that we could allow using user stream wrapper as an underlaying stream which would cover this much better. So it could be used like this:

$transport = fopen(“my-dtls-transport://…”);
stream_socket_enable_crypto($transport, true, STREAM_CRYPTO_METHOD_DTLS_CLIENT);
fwrite($transport, “some data”);

It means that for your use case, we will probably need stream_socket_enable_crypto. This will still need a custom BIO and might not be completely straight forward so not something to look now. Just keep that in mind.

Kind regards,

Jakub

Hi Jakub

Il giorno ven 3 lug 2026 alle ore 23:43 Jakub Zelenka <bukka@php.net> ha scritto:

So the reason why I mentioned to check DTLS 1.3 is because DTLSv1_listen is not currently supported there so it would be probably better to go through SSL_accept (with SSL_OP_COOKIE_EXCHANGE)). So the same should be used for DTLS 1.2 if possible.

Good point, you’re right. I’ll remove DTLSv1_listen and switch the server to SSL_accept with SSL_OP_COOKIE_EXCHANGE, so the same accept path works for DTLS 1.2 now and 1.3 later.

I can see the reasoning for separating but there seems to be lots of duplications so it might be good to have some xp_common.c with some shared helpers and restructure it a bit. But this can be probably added later. Makes sense to start with this and then we can see what could be shared.

Great, I’ll proceed like this: after this PR is approved, I’ll move on to xp_common.c in a separate PR.

It’s not necessary but might be better to include it or at least research to get an idea about the whole design.

I’ll look into it and see if it’s feasible without risking regressions, major changes, or core modifications. If it is feasible, I’ll include it directly in this PR; otherwise, it can wait for the future.

Please create a draft PR for this so we can discuss it there. It’s much easier than here.

Sure, I’ll prepare the draft as soon as I wrap up the final details and get it ready for DTLS 1.3, I don’t think it will take too long.

I guess it would be good to support so it’s consistent but it’s not necessary if it would be a pain to do.

I’ve already tried doing that; it’s a somewhat lengthy task, especially since the risk of regression is very high. I’d prefer to include it in a future PR that I’ll create after this one.

Keep it consistent with tls to not over complicate it.

Great, then I’ll leave it as it is now (exactly like tls://).

Thank you again for your valuable time and advice.

Kind regards,

Gianfrancesco

Il giorno ven 3 lug 2026 alle ore 23:51 Jakub Zelenka <bukka@php.net> ha scritto:

Just a little follow up on this. I spoke with Arnaud, who is working on IO hooks, and he got actually a better idea that we could allow using user stream wrapper as an underlaying stream which would cover this much better. So it could be used like this:

$transport = fopen(“my-dtls-transport://…”);
stream_socket_enable_crypto($transport, true, STREAM_CRYPTO_METHOD_DTLS_CLIENT);
fwrite($transport, “some data”);

It means that for your use case, we will probably need stream_socket_enable_crypto. This will still need a custom BIO and might not be completely straight forward so not something to look now. Just keep that in mind.

Thanks to you and Arnaud, nice idea. Using a user stream wrapper as the underlying transport and running DTLS on top of it via ‘stream_socket_enable_crypto’ (through a custom BIO) covers the case more flexibly, leaving the application in control of the transport.

If I understand correctly: the app implements its own stream wrapper, then ‘stream_socket_enable_crypto($transport, true, STREAM_CRYPTO_METHOD_DTLS_CLIENT)’ enables DTLS on top, with a custom BIO that reads/writes through the underlying stream (instead of the datagram BIO on the socket). Is that right?

Thanks again, I’ll keep it in mind.

Kind regards,

Gianfrancesco

Hi,

Quick follow-up:

Please create a draft PR for this so we can discuss it there. It’s much easier than here.

I’ve opened the PR as a draft: https://github.com/php/php-src/pull/22638

Kind regards,

Gianfrancesco