Repository navigation
ext/openssl: add a dtls:// stream transport. Open as Draft. - #22638
GianfriAur wants to merge 27 commits into
Conversation
bukka
left a comment
There was a problem hiding this comment.
Hmm the amount of duplication is quite big. I think this is more a good PoC but it's very far from anything that we could merge. We might need to re-architecture the whole xp_ssl for this. I will need to do more research and thinking to see what would be the best way.
| return -1; | ||
| } | ||
|
|
||
| dtlssock->s.socket = php_network_connect_socket_to_host(host, (unsigned short)portno, |
There was a problem hiding this comment.
I don't think this is the right approach. We should re-use (and possibly extend if needed) the underlaying udp stream so it works in the same way as tls that is build on top tcp.
Thinking about it, we should really go with enable_crypto support from the beginning as it will mirror better the TLS code.
There was a problem hiding this comment.
Okay, great. I’ve already made an attempts. I’ll work on it over the next few days.
| return -1; | ||
| } | ||
| for (;;) { | ||
| int ret = DTLSv1_listen(ssl, client_addr); |
Okay, do you want me to try drafting an |
Yeah I think it would be actually better. |
|
Honestly I'm not a big fan of adding is_dgram to php_netstream_data_t, but I couldn't make dtls:// reuse the socket transport the clean way tls:// reuses it for tcp://. It comes down to an asymmetry in the generic socket transport. tls:// gets the reuse for free because TCP is the default socket type there — the comment in xp_socket.c even spells it out: That said, I'm not thrilled with it, if anyone has a cleaner idea, it's very welcome. |
da9a678 to
29ec86b
Compare
|
@bukka, I've sketched out an xp_common; let me know what you think and what changes you'd make. The approach: I moved into xp_common.{c,h} only the machinery that xp_ssl and xp_dtls genuinely share as-is, fingerprint matching, peer-name resolution, the passphrase callback, local cert/key loading, the verify callback + enable/disable peer verification, the cipher metadata helper, and the I deliberately kept the setup_client_session / setup_server_session orchestration per-transport rather than merging it: DTLS has no cross-connection internal cache (fresh context per accepted peer), so the cache-mode policy actually differs, and unifying it would mean re-introducing those |
openssl: Add DTLS version floor and peer_fingerprint to dtls:// openssl: Validate peer_fingerprint openssl: Fix dtls:// non-blocking I/O, handshake timeout and tests
The BIO no longer does IO: it serves records from a receive queue and appends to a send queue owned by the connection (xp_bio.c), and the stream moves ciphertext between the queues and the transport around each SSL call, so no call into OpenSSL waits. The transport is the socket or an inner stream given by the inner_stream context option. DTLS shares the stream implementation of xp_ssl.c: the datagram schemes select the DTLS methods and versions, and xp_dtls.c keeps only the server port, which demultiplexes the peers of one UDP socket, runs their handshakes with a cookie exchange and hands them to accept().
… transport tests The dtls:// tests now follow the tls:// semantics of peer name verification, fingerprints and session capture. New tests cover a server with several peers on one socket, DTLS 1.3, DTLS over udp:// with stream_socket_enable_crypto(), a non-blocking handshake, and TLS over an inner stream, both a tcp:// stream and a user wrapper.
The inner stream cannot be closed from under the stream carrying its ciphertext through it, a dtls:// connection reports the peer of its own address rather than of the shared socket, and a shutdown of a port stream leaves the socket to the other peers. dtlsv1.3:// is registered only when the library has DTLS 1.3.
29ec86b to
d4f377d
Compare
The test bound the server to 0.0.0.0 and the harness hands the bound address to the client, so the client connected to dtls://0.0.0.0:<port>. Linux and macOS route that to loopback, Winsock rejects it with WSAEADDRNOTAVAIL, which failed the test on Windows before any DTLS code ran. Bind to 127.0.0.1 like the other dtls:// server tests.
|
I had a deeper look into this and realised that DTLSv1_listen wasn't really the way forward given that it cannot work with DTLS 1.3. Unfortunately SSL_new_listener for DTLS was just introduced in 4.1 so we couldn't properly support older version. So another solution was needed. As I have been working on IO Hooks, I needed to introduce custom BIO and planned for some rewrite of that. The fact that you also needed user wrapper integration also makes it necessary. This actually gives possibility to create a custom listener and map it to peers ourselves which is exactly what I did. There is one omission in OpenSSL as it ignores SSL_OP_COOKIE_EXCHANGE for DTLS 1.3 which I'm trying to address in openssl/openssl#33093 so will see if it gets to 4.1 as a bug fix. This does not impact client - it's just for server that needs to verify address (default). The current version of this patch will default to DTLS 1.2 for server unless crypto_method explicitly includes DTLS 1.3. We can update it once that OpenSSL is resolved. I also aligned the verify and session options with tls:// logic to make it a bit more consistent. |
|
|
||
| #ifdef HAVE_DTLS | ||
| if (sslsock->port != NULL) { | ||
| php_openssl_dtls_detach(stream, sslsock); |
There was a problem hiding this comment.
If sslsock->port != NULL, php_openssl_dtls_detach is always called twice when close_handle is ture ,
the first one at line 3207.
|
@bukka First of all, thank you very much for the beautiful refactor. That said, I quickly read through the code and pointed out the only thing I noticed at a glance. I also ran some tests and realized there's a small leak issue; in one case, sharing a stream and closing one causes a segfault. I think it's due to 'inner_stream', calling it a "leak" might be a bit of an exaggeration, perhaps, but it’s certainly not the kind of behavior I would expect. Here is a snippet of code.
As for the segfault, it occurs regardless of the stream, even during legitimate operations. |
Adds DTLS to
ext/openssl: thedtls://,dtlsv1.2://anddtlsv1.3://stream transports(client and server) and DTLS over
udp://throughstream_socket_enable_crypto(). Thesslcontext options,
Openssl\Sessionand the stream API work for DTLS as they do fortls://.Design
The implementation is split by layer rather than by protocol, so DTLS adds no second copy of the
option handling or of the IO loops:
xp_bio.c, the ciphertext transport. OpenSSL never does IO itself. Its BIO serves recordsfrom a receive queue and appends to a send queue owned by the connection, and the stream moves
ciphertext between the queues and the transport around each
SSL_*()call. No call into OpenSSLever waits, and the socket stays non-blocking once crypto is set up, with the stream's blocking
mode emulated by waiting with a monotonic deadline. The transport is the stream's socket, or any
other stream given by the new
inner_streamcontext option (atcp://stream or a user wrapper),which makes TLS and DTLS over an arbitrary stream possible. DTLS queues whole datagrams with their
peer address and folds the retransmit timer into every wait.
xp_ssl.c, one stream implementation for TLS and DTLS. The datagram schemes select the DTLSmethods and version range; everything else (verification, SNI, ALPN, PSK, sessions, early data,
ciphers, security level, meta data) is shared. The old
gettimeofday()loops and the switching ofthe descriptor mode are gone.
xp_dtls.c, the server port. One UDP socket carries every peer of a server. The port receivesthe datagrams, routes them by peer address to their connection, runs the handshakes of new peers
with a cookie exchange (
SSL_OP_COOKIE_EXCHANGE, timestamped HMAC cookies, a pending cap andtimeout) and hands the connections that completed to
stream_socket_accept()as streams sharingthe socket. It uses neither
DTLSv1_listen()(DTLS 1.2 only, single peer) nor the OpenSSL 4.1listener API, so it works with every supported OpenSSL from 1.1.1.
Semantics
tls://:verify_peer_nameis separate fromverify_peer, apeer_fingerprintcheck comes on top of
verify_peer => false, sessions are captured withsession_new_cb, andthe handshake completes inside
stream_socket_accept().dtls://client negotiates DTLS 1.2 or 1.3 (OpenSSL 4.1+). A server offers DTLS 1.3 only whenasked with an explicit
crypto_method, because outside the OpenSSL listener a DTLS 1.3 servercannot validate the peer address before its first flight.
sslcontext options:inner_stream,dtls_link_mtu,dtls_max_pending(default 256),dtls_pending_timeout(seconds, default 30),keying_material_labelandkeying_material_length(RFC 5705 export instream_get_meta_data()['crypto']).STREAM_CRYPTO_METHOD_DTLSv1_2_*,STREAM_CRYPTO_METHOD_DTLSv1_3_*andSTREAM_CRYPTO_METHOD_DTLS_ANY_*. The stream type ofudp://streams isudp_socket/dtlswhenthe extension is loaded, as
tcp://istcp_socket/ssl.Tests
ext/openssl/tests/dtls_*: client againstopenssl s_server, PHP server and client, mutualauthentication, fingerprints, session resumption on both sides, MTU, robustness against bogus
datagrams, a server with several peers on one socket, DTLS 1.3, DTLS over
udp://, a non-blockinghandshake;
tls_inner_stream*.phptfor TLS over atcp://stream and over a user wrapper. Verifiedagainst OpenSSL 1.1.1, 3.0 and the 4.2-dev tree (for DTLS 1.3).
Discussed on internals: https://externals.io/message/131514.
TODO before removing Draft status
UPGRADINGandNEWS