Commit 2d0439e8e9 for openssl.org
commit 2d0439e8e99df35622da4f37f615c1119daf2448
Author: Ryan Hooper <ryanh@openssl.foundation>
Date: Wed May 27 10:10:42 2026 -0400
DTLS 1.3 SSL Listener Demo
Demo utilizing the SSL Listener for DTLS. It runs in multiple
threads that will echo what a client sends.
Currently up to 10 clients can make a connection. A client can
close the connection by sending kill. If a client sends
killall the server will close all the connections and free
up all the resources being used.
Fixes: openssl/project#1957
Assisted-by: Claude:claude-opus-4-7
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
Reviewed-by: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Reviewed-by: Matt Caswell <matt@openssl.foundation>
Merge-date: Thu Aug 20 09:50:36 2026
Merged-from: https://github.com/openssl/openssl/pull/31983
diff --git a/demos/build.info b/demos/build.info
index 59c0c4e1b4..15948e7f12 100644
--- a/demos/build.info
+++ b/demos/build.info
@@ -20,6 +20,9 @@ ENDIF
IF[{- !$disabled{"dgram"} -}]
SUBDIRS=guide
SUBDIRS=dtlsecho
+ IF[{- !$disabled{"threads"} -}]
+ SUBDIRS=dtlslistenerecho
+ ENDIF
ENDIF
IF[{- !$disabled{"des"} -}]
diff --git a/demos/dtlsecho/main.c b/demos/dtlsecho/main.c
index 47d3a57e9e..88db9891ba 100644
--- a/demos/dtlsecho/main.c
+++ b/demos/dtlsecho/main.c
@@ -25,7 +25,7 @@
#define closesocket(s) close(s)
#else
-#include <winsock.h>
+#include <winsock2.h>
#include <ws2tcpip.h>
#endif
@@ -41,37 +41,49 @@ typedef unsigned char flag;
*/
static volatile flag server_running = true;
-static SOCKET create_socket(flag isServer)
+static SOCKET create_socket(void)
{
- SOCKET s;
- int optval = 1;
- struct sockaddr_in addr;
+ SOCKET s = INVALID_SOCKET;
+ BIO_ADDRINFO *res = NULL;
+ const BIO_ADDR *addr;
+ char port_str[6];
- s = socket(AF_INET, SOCK_DGRAM, 0);
- if (s == INVALID_SOCKET) {
- perror("Unable to create socket");
+ /*
+ * Resolve the wildcard address for our port. Requesting AF_INET6 gives a
+ * single socket that, BIO_listen will clear IPV6_V6ONLY below, and the
+ * socket accepts both IPv6 and IPv4 clients.
+ */
+ BIO_snprintf(port_str, sizeof(port_str), "%d", server_port);
+ if (!BIO_lookup_ex(NULL, port_str, BIO_LOOKUP_SERVER, AF_INET6,
+ SOCK_DGRAM, 0, &res)) {
+ fprintf(stderr, "Unable to resolve local address\n");
+ ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
+ addr = BIO_ADDRINFO_address(res);
- if (isServer) {
- addr.sin_family = AF_INET;
- addr.sin_port = htons(server_port);
- addr.sin_addr.s_addr = INADDR_ANY;
-
- /* Reuse the address; good for quick restarts */
- if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (void *)&optval,
- sizeof(optval))
- < 0) {
- perror("setsockopt(SO_REUSEADDR) failed");
- exit(EXIT_FAILURE);
- }
+ s = BIO_socket(BIO_ADDRINFO_family(res), SOCK_DGRAM, 0, 0);
+ if (s == INVALID_SOCKET) {
+ fprintf(stderr, "Unable to create socket\n");
+ ERR_print_errors_fp(stderr);
+ BIO_ADDRINFO_free(res);
+ exit(EXIT_FAILURE);
+ }
- if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
- perror("Unable to bind");
- exit(EXIT_FAILURE);
- }
+ /*
+ * BIO_listen binds with SO_REUSEADDR and, since we do not pass
+ * BIO_SOCK_V6_ONLY, clears IPV6_V6ONLY to give us a dual-stack socket
+ * that serves both IPv6 and IPv4 clients.
+ */
+ if (!BIO_listen((int)s, addr, BIO_SOCK_REUSEADDR)) {
+ fprintf(stderr, "Unable to bind\n");
+ ERR_print_errors_fp(stderr);
+ BIO_closesocket((int)s);
+ BIO_ADDRINFO_free(res);
+ exit(EXIT_FAILURE);
}
+ BIO_ADDRINFO_free(res);
return s;
}
@@ -172,7 +184,6 @@ int main(int argc, char **argv)
char *rem_server_name = NULL;
- struct sockaddr_in addr;
int received_new_session_ack = 0;
#if !defined(OPENSSL_SYS_WINDOWS)
@@ -212,7 +223,7 @@ int main(int argc, char **argv)
configure_server_context(ssl_ctx);
/* Create server socket; will bind to server port */
- server_skt = create_socket(true);
+ server_skt = create_socket();
if (server_skt == INVALID_SOCKET) {
perror("Unable to create server socket");
exit(EXIT_FAILURE);
@@ -286,32 +297,47 @@ int main(int argc, char **argv)
/* Else client */
else {
BIO *bio;
+ BIO_ADDRINFO *res = NULL;
+ const BIO_ADDRINFO *ai = NULL;
+ char port_str[6];
printf("We are the client\n\n");
/* Configure client context so we verify the server correctly */
configure_client_context(ssl_ctx);
- /* Create "bare" UDP socket */
- client_skt = create_socket(false);
- if (client_skt == INVALID_SOCKET) {
- perror("Unable to accept");
- exit(EXIT_FAILURE);
+ /* Resolve server hostname or IP address (IPv4 or IPv6) */
+ BIO_snprintf(port_str, sizeof(port_str), "%d", server_port);
+ if (!BIO_lookup(rem_server_name, port_str, BIO_LOOKUP_CLIENT,
+ AF_UNSPEC, SOCK_DGRAM, &res)) {
+ fprintf(stderr, "Unable to resolve server: %s\n", rem_server_name);
+ ERR_print_errors_fp(stderr);
+ goto exit;
}
- /* Set up server address */
- memset(&addr, 0, sizeof(addr));
- addr.sin_family = AF_INET;
- inet_pton(AF_INET, rem_server_name, &addr.sin_addr.s_addr);
- addr.sin_port = htons(server_port);
+ /*
+ * Iterate over the resolved addresses and connect to the first one
+ * that works, creating a UDP socket of the matching address family for
+ * each attempt. For UDP, BIO_connect just sets the default peer address.
+ */
+ for (ai = res; ai != NULL; ai = BIO_ADDRINFO_next(ai)) {
+ client_skt = BIO_socket(BIO_ADDRINFO_family(ai), SOCK_DGRAM, 0, 0);
+ if (client_skt == INVALID_SOCKET)
+ continue;
+ if (BIO_connect((int)client_skt, BIO_ADDRINFO_address(ai), 0))
+ break;
+ BIO_closesocket((int)client_skt);
+ client_skt = INVALID_SOCKET;
+ }
+ BIO_ADDRINFO_free(res);
- /* Connect the UDP socket to the server (sets default peer address) */
- if (connect(client_skt, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
- perror("Unable to UDP connect to server");
+ if (client_skt == INVALID_SOCKET) {
+ fprintf(stderr, "Unable to UDP connect to server: %s\n",
+ rem_server_name);
+ ERR_print_errors_fp(stderr);
goto exit;
- } else {
- printf("UDP connection to server successful\n");
}
+ printf("UDP connection to server successful\n");
/* Create a datagram BIO for the connected socket */
bio = BIO_new_dgram((int)client_skt, BIO_NOCLOSE);
diff --git a/demos/dtlslistenerecho/README.md b/demos/dtlslistenerecho/README.md
new file mode 100644
index 0000000000..d13a2afc24
--- /dev/null
+++ b/demos/dtlslistenerecho/README.md
@@ -0,0 +1,117 @@
+OpenSSL DTLS Listener Server/Client
+===================================
+
+This project implements a simple echo application utilizing DTLS SSL Listener.
+
+It is a console application, with command line parameters determining the mode
+of operation (client or server). Start it with no parameters to see usage.
+
+The server code utilizes the SSL Listener to setup a DTLS Server object that
+can handle multiple client connections using a thread-per-connection model.
+Each accepted connection is handled in its own dedicated thread using the
+platform's native threads.
+
+The client will send application data to the server and the server will simply
+respond to the client with an echo of that data.
+
+Features
+--------
+
+- Up to 10 concurrent DTLS client connections (MAX_CONNECTIONS)
+- Thread-per-connection architecture using native OS threads
+- Non-blocking I/O using SSL_poll() within each connection thread
+- Supports both DTLS 1.2 (HelloVerifyRequest) and DTLS 1.3 (HelloRetryRequest)
+- Client option to specify DTLS protocol version (dtls12 or dtls13)
+- Active shutdown signaling for clean thread termination
+- Server-wide shutdown via "killall" command
+
+Limitations
+-----------
+
+- Maximum 10 concurrent client connections (defined by MAX_CONNECTIONS)
+- Additional connection attempts while at capacity will be rejected with an
+ error message printed to the server console
+- Connections do not stay open indefinitely: if a client sends no data for
+ CLIENT_IDLE_TIMEOUT_SEC (90 seconds), the server abandons the connection and
+ frees its thread slot. A stalled handshake is likewise time-bounded rather
+ than retried for minutes (see dtls_timer_cb() in main.c)
+
+The code demonstrates
+---------------------
+
+- DTLS Server using SSL Listener APIs to establish Connections
+- DTLS Server validating Clients via HRR/HVR
+- Thread-per-connection model for handling multiple clients
+- Clients sending data to an established Server
+- Server utilizing SSL_poll() within each thread for read readiness
+- Server sending data to an established Client
+- Client-side DTLS version selection via command-line argument
+- Using SSL_CTX_set_min_proto_version() and SSL_CTX_set_max_proto_version()
+- Bounding the handshake retransmit backoff via DTLS_set_timer_cb()
+- Active thread shutdown via signaling mechanism
+- Graceful server shutdown with client disconnection
+
+Running
+-------
+
+First, change to the demo directory:
+
+```console
+cd demos/dtlslistenerecho
+```
+
+### Start the Server
+
+```console
+./dtlslistenerecho s
+```
+
+### Connect Multiple Clients (in separate terminals)
+
+You can connect up to 10 clients simultaneously:
+
+```console
+./dtlslistenerecho c localhost
+./dtlslistenerecho c localhost
+./dtlslistenerecho c localhost
+```
+
+Each client can send messages independently and receive echoes.
+
+### Specify DTLS Protocol Version
+
+You can optionally specify the DTLS protocol version for the client:
+
+```console
+# Connect using DTLS 1.2
+./dtlslistenerecho c localhost dtls12
+
+# Connect using DTLS 1.3
+./dtlslistenerecho c localhost dtls13
+
+# Connect using default (negotiates highest available)
+./dtlslistenerecho c localhost
+```
+
+Special Commands
+----------------
+
+- Type "kill" in a client to disconnect that client only (server continues running)
+- Type "killall" in a client to disconnect all clients and shutdown the server gracefully
+
+When "killall" is received:
+1. The server sets a shutdown flag
+2. All connection threads are signaled to terminate
+3. Each thread completes its current operation and exits cleanly
+4. The server closes the listener and exits
+
+The cert.pem and key.pem files included are self signed certificates with the
+"Common Name" of 'localhost'.
+
+The client verifies the server's certificate against the hostname you pass on
+the command line (via SSL_set1_dnsname()), so that name must match the
+certificate. With the bundled certificate you must use 'localhost'. Note that
+the hostname is matched strictly as a DNS name: an IP address literal such as
+'127.0.0.1' will not verify, even though it resolves to the same host.
+
+Best to create the 'pem' files using an actual hostname.
diff --git a/demos/dtlslistenerecho/build.info b/demos/dtlslistenerecho/build.info
new file mode 100644
index 0000000000..a7c44df043
--- /dev/null
+++ b/demos/dtlslistenerecho/build.info
@@ -0,0 +1,11 @@
+#
+# To run the demos when linked with a shared library (default) ensure that
+# libcrypto and libssl are on the library path. For example:
+#
+# LD_LIBRARY_PATH=../.. ./dtlslistenerecho
+
+PROGRAMS{noinst} = dtlslistenerecho
+
+INCLUDE[dtlslistenerecho]=../../include
+SOURCE[dtlslistenerecho]=main.c
+DEPEND[dtlslistenerecho]=../../libcrypto ../../libssl
diff --git a/demos/dtlslistenerecho/cert.pem b/demos/dtlslistenerecho/cert.pem
new file mode 100644
index 0000000000..834d46285f
--- /dev/null
+++ b/demos/dtlslistenerecho/cert.pem
@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFkzCCA3ugAwIBAgIUQJ8FQFwuVg1UlnIBam0+liL0RSQwDQYJKoZIhvcNAQEL
+BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
+GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X
+DTIyMDIwMjE0MzgzNloXDTMyMDEzMTE0MzgzNlowWTELMAkGA1UEBhMCQVUxEzAR
+BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5
+IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
+MIICCgKCAgEAyPZfTbR9lVvpHxIGRzpYb1gYFjPZ7yTXYZVKEqQLVxw/O2L32ufa
+lODiYJr/pKu9++9T+JrmRnonYlyl0uFta3w4rMY9PzHsT7jIZJByoFdraNz1SnxF
+1UaHjzF9fjIA0/n/ZGVJDZCCYulpcVkpW14oNG4tTW5IefYUH3GxmPZ5godhWEla
+6OXl3+9xkGd5yXq1O4VZbsekcVcZlznuq7blmvs3UrjrEZ5xgmCd8kNzy/E9APKY
+SSGx87/U9yyiz5GAphgSNTqAfEWqpzouMv+hUm/J5NuZCXbOPYbE7zfDDauspYiY
+/wdGty9ZvDy5g+fFz8sZig1OWuHqvU8QGoIfVRCxjhX3+p0/KshGDBWjLHek+8Wh
+IZHmuf1LgT+gOzN3dxxVEcphSiJX0eZ/OhBelowrdabEycm2WAk3qs/tUDMbWh6V
+VSH22ODLX/cBrSAY2sk2EU8Mz5Mbm6gFTcJhqOBgVn5g8/3QCAhFG3xq/2LKZ+za
+itAKbaeQqyAw5G/+oc7mKCjUqSKE92n6FKZRsJrB+vfy3AQYyqJevHcIf2nbBimX
+vb4/rDed/gvSOVGIXIUiUlFHgg8DoVZqMrfJ+y/xwr+Ya+AX8n6J8EB2It3W4EEf
+nmosupBcZPb6U2VrtpEe/199nPj2ZXQHGLLQfw8lYjvZDghCFiP0o8cCAwEAAaNT
+MFEwHQYDVR0OBBYEFDClIPCiAkevl1qh188Ycjz5IZ/DMB8GA1UdIwQYMBaAFDCl
+IPCiAkevl1qh188Ycjz5IZ/DMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
+BQADggIBAGQm27D74xUm1X99GbDQpgJenUIu+Bszk8qXCsPfyyQZy4H6+70uXlkC
+ASf/keQjrPzrco3rBelbtlCGqqWhsCznVplrjNIntBAkLD0fU3z92SjsMvHEcBDa
+Nu6aXExN9gv85EBJHNnj16hqjo8Mk+ydNQ8BtcnZa4zi7GdVh29KbPuEzeoyRnXP
+xh5yHUj5Bs6hEUbirhm1WLEK8bvfWykfEJiGOQO8MHAeYK1uPFXDmswgTwJFzZyA
+6LSXYbmGOnCM8yAmVXHMnXXCKd+DQFyQ0KrXDiixyTinYFtrONBkNNt/7SnCjJt5
+H3LRTNuoZvvGmaS7GxbIMemBjLdrigKicVZunEPGFRTEL7K+spmSMnpAiITStxjR
+70wHEe3M9IUbJximKaxvMhXhP0VSPJGOzgG304A2MqMS7UPBDzD/pz5c7gn7ILfM
+LcxzStnQcbTqqmdpNVlMv31YpOk5nel5RY3UmwKbQkix6UAo/CJmC1Q3yLU8uG5O
+6j7vS8t0wOYcVTAA845JU8C7V5yy6UeCB9F2oGDgVwCe6U8bzTIoCDnkzIKO7LlS
+734KP+fNK9LatNzpPQWW+1SK4XEZBNLOMePwu560GLVzPgr9ji0z83E+0yAcWrAO
+4gKT+/h3Ep1Ut73daskFAvNJFFt/5Rm+xZECHrxRkXqW1AN/2eXX
+-----END CERTIFICATE-----
diff --git a/demos/dtlslistenerecho/key.pem b/demos/dtlslistenerecho/key.pem
new file mode 100644
index 0000000000..75b86c3a38
--- /dev/null
+++ b/demos/dtlslistenerecho/key.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+DDFU5HazFnn1G0/rg2SnKHTRLV2E9g==
+-----END PRIVATE KEY-----
diff --git a/demos/dtlslistenerecho/main.c b/demos/dtlslistenerecho/main.c
new file mode 100644
index 0000000000..53e117b371
--- /dev/null
+++ b/demos/dtlslistenerecho/main.c
@@ -0,0 +1,1051 @@
+/*
+ * Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License"). You may not use
+ * this file except in compliance with the License. You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+#include <openssl/bio.h>
+#include <openssl/crypto.h>
+
+#if !defined(OPENSSL_SYS_WINDOWS)
+#include <unistd.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <poll.h>
+#include <pthread.h>
+
+#define SOCKET int
+#define INVALID_SOCKET (-1)
+#define closesocket(s) close(s)
+
+#else
+#include <winsock2.h>
+#include <ws2tcpip.h>
+#include <conio.h>
+#endif
+
+#if defined(OPENSSL_SYS_WINDOWS)
+typedef HANDLE thread_t;
+#else
+typedef pthread_t thread_t;
+#endif
+
+static const int server_port = 4433;
+
+#define MAX_CONNECTIONS 10
+#define POLL_TIMEOUT_SEC 5
+/* Abandon a connection whose client sends nothing for 90 seconds. */
+#define CLIENT_IDLE_TIMEOUT_SEC 90
+
+/*
+ * Cap the DTLS handshake retransmit backoff. The library default doubles the
+ * timeout up to 60s; across the retransmits DTLS allows before giving up that
+ * lets a dead peer stall a handshake for nearly 8 minutes. Capping each backoff
+ * at 8s bounds the handshake to about 87s (1 + 2 + 4 + 8 x 10), keeping it in
+ * line with CLIENT_IDLE_TIMEOUT_SEC. See dtls_timer_cb().
+ */
+#define DTLS_MAX_RETRANSMIT_TIMEOUT_US (8 * 1000000u)
+
+/*
+ * Per-thread state for connection handlers
+ */
+struct connection_thread_args {
+ SSL *conn;
+ int thread_idx;
+ thread_t thread;
+ int active;
+ int shutdown_requested;
+ int finished;
+ int *server_shutdown;
+};
+
+static CRYPTO_RWLOCK *atomic_lock = NULL;
+
+static SSL_CTX *create_context(bool isServer)
+{
+ SSL_CTX *ctx;
+
+ if (isServer) {
+ ctx = SSL_CTX_new(DTLS_server_method());
+ } else {
+ ctx = SSL_CTX_new(DTLS_client_method());
+ }
+
+ if (ctx == NULL) {
+ fprintf(stderr, "Unable to create SSL context\n");
+ ERR_print_errors_fp(stderr);
+ exit(EXIT_FAILURE);
+ }
+
+ return ctx;
+}
+
+/*
+ * DTLS retransmit timer callback. Installed with DTLS_set_timer_cb(), it is
+ * invoked for each handshake flight to choose the next retransmit interval.
+ * timer_us holds the previous interval (0 on the first call). We start at 1s
+ * and double, but cap the backoff so a stalled handshake is abandoned in a
+ * reasonable time rather than the library default of nearly 8 minutes.
+ */
+static unsigned int dtls_timer_cb(SSL *s, unsigned int timer_us)
+{
+ unsigned int next = (timer_us == 0) ? 1000000u : timer_us * 2;
+
+ if (next > DTLS_MAX_RETRANSMIT_TIMEOUT_US)
+ next = DTLS_MAX_RETRANSMIT_TIMEOUT_US;
+ return next;
+}
+
+static int create_dtls_listener(SSL_CTX *ssl_ctx, int port,
+ SSL **listener, SOCKET *server_fd)
+{
+ BIO *listener_bio = NULL;
+ BIO_ADDRINFO *res = NULL;
+ const BIO_ADDR *addr;
+ char port_str[6];
+ int ret = 0;
+
+ *listener = NULL;
+ *server_fd = INVALID_SOCKET;
+
+ /*
+ * Resolve the wildcard address for our port. Requesting AF_INET6 gives a
+ * single socket that, since we do not pass BIO_SOCK_V6_ONLY to BIO_listen,
+ * serves both IPv6 and IPv4 clients.
+ */
+ BIO_snprintf(port_str, sizeof(port_str), "%d", port);
+ if (!BIO_lookup_ex(NULL, port_str, BIO_LOOKUP_SERVER, AF_INET6,
+ SOCK_DGRAM, 0, &res)) {
+ fprintf(stderr, "Unable to resolve local address\n");
+ ERR_print_errors_fp(stderr);
+ goto err;
+ }
+ addr = BIO_ADDRINFO_address(res);
+
+ *server_fd = BIO_socket(BIO_ADDRINFO_family(res), SOCK_DGRAM, 0, 0);
+ if (*server_fd == INVALID_SOCKET) {
+ fprintf(stderr, "Unable to create UDP socket\n");
+ ERR_print_errors_fp(stderr);
+ goto err;
+ }
+
+ /*
+ * BIO_listen binds the socket. Pass BIO_SOCK_NONBLOCK so the listener is
+ * non-blocking. Omitting BIO_SOCK_V6_ONLY clears IPV6_V6ONLY, giving a
+ * dual-stack socket that serves both IPv6 and IPv4 clients.
+ */
+ if (!BIO_listen((int)*server_fd, addr,
+ BIO_SOCK_REUSEADDR | BIO_SOCK_NONBLOCK)) {
+ fprintf(stderr, "Unable to bind socket\n");
+ ERR_print_errors_fp(stderr);
+ goto err;
+ }
+
+ printf("Server bound to port %d\n", port);
+
+ /* Create a datagram BIO and attach the socket */
+ listener_bio = BIO_new_dgram((int)*server_fd, BIO_NOCLOSE);
+ if (listener_bio == NULL) {
+ fprintf(stderr, "Unable to create datagram BIO\n");
+ ERR_print_errors_fp(stderr);
+ goto err;
+ }
+
+ /* Create the DTLS listener with HVR (DTLS 1.2) and HRR (DTLS 1.3) cookie validation */
+ *listener = SSL_new_listener(ssl_ctx, SSL_LISTENER_FLAG_REQUIRE_HVR | SSL_LISTENER_FLAG_REQUIRE_HRR);
+ if (*listener == NULL) {
+ fprintf(stderr, "Unable to create DTLS listener\n");
+ ERR_print_errors_fp(stderr);
+ goto err;
+ }
+
+ /*
+ * Attach the BIO to the listener for both read and write. Because rbio and
+ * wbio are the same BIO, each set0 call takes one reference, so bump the
+ * reference count once beforehand.
+ */
+ if (!BIO_up_ref(listener_bio)) {
+ fprintf(stderr, "Unable to increment BIO reference count\n");
+ ERR_print_errors_fp(stderr);
+ goto err;
+ }
+ SSL_set0_rbio(*listener, listener_bio);
+ SSL_set0_wbio(*listener, listener_bio);
+ listener_bio = NULL; /* Both references transferred to listener */
+
+ /* Start listening for incoming connections */
+ if (SSL_listen(*listener) != 1) {
+ fprintf(stderr, "SSL_listen failed\n");
+ ERR_print_errors_fp(stderr);
+ goto err;
+ }
+
+ ret = 1;
+
+err:
+ BIO_free(listener_bio);
+ BIO_ADDRINFO_free(res);
+ if (ret == 0) {
+ SSL_free(*listener);
+ *listener = NULL;
+ if (*server_fd != INVALID_SOCKET)
+ BIO_closesocket((int)*server_fd);
+ *server_fd = INVALID_SOCKET;
+ }
+ return ret;
+}
+
+/*
+ * Thread worker: handles a single DTLS connection. Completes the handshake,
+ * then reads data and echoes it back until the peer disconnects, a shutdown is
+ * requested, or the client goes idle.
+ */
+static void handle_connection(struct connection_thread_args *conn_args)
+{
+ SSL_POLL_ITEM item;
+ struct timeval timeout;
+ size_t result_count, readbytes, written;
+ char buf[1500];
+ int ret, err;
+ int shutdown_requested = 0;
+ time_t idle_deadline;
+
+ printf("Thread %d: Starting connection handler\n", conn_args->thread_idx);
+
+ /* Complete the handshake */
+ while ((ret = SSL_accept(conn_args->conn)) != 1) {
+ CRYPTO_atomic_load_int(&conn_args->shutdown_requested,
+ &shutdown_requested, atomic_lock);
+ if (shutdown_requested) {
+ printf("Thread %d: Shutdown requested during handshake\n", conn_args->thread_idx);
+ goto done;
+ }
+ err = SSL_get_error(conn_args->conn, ret);
+ if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) {
+ printf("Thread %d: Handshake failed\n", conn_args->thread_idx);
+ ERR_print_errors_fp(stderr);
+ goto done;
+ }
+
+ /*
+ * Wait for the socket to become ready rather than busy-looping. Size
+ * the wait to the DTLS retransmit timer so we wake when a flight is
+ * due for retransmission; fall back to a fixed interval if no timer is
+ * armed.
+ */
+ item.desc = SSL_as_poll_descriptor(conn_args->conn);
+ item.events = (err == SSL_ERROR_WANT_READ) ? SSL_POLL_EVENT_R
+ : SSL_POLL_EVENT_W;
+ item.revents = 0;
+ if (!DTLSv1_get_timeout(conn_args->conn, &timeout)) {
+ timeout.tv_sec = POLL_TIMEOUT_SEC;
+ timeout.tv_usec = 0;
+ }
+ if (!SSL_poll(&item, 1, sizeof(item), &timeout, 0, &result_count)) {
+ printf("Thread %d: SSL_poll failed during handshake\n", conn_args->thread_idx);
+ ERR_print_errors_fp(stderr);
+ goto done;
+ }
+ /*
+ * No inbound datagram before the timer expired: loop back into
+ * SSL_accept() so the library retransmits the last flight. A dead peer
+ * eventually exhausts the DTLS retransmit budget, which surfaces as a
+ * fatal error from SSL_accept() above and ends the loop.
+ */
+ if (result_count == 0)
+ continue;
+ }
+
+ printf("Thread %d: Handshake completed\n", conn_args->thread_idx);
+
+ /* Setup poll item for reading */
+ item.desc = SSL_as_poll_descriptor(conn_args->conn);
+ item.events = SSL_POLL_EVENT_R;
+
+ idle_deadline = time(NULL) + CLIENT_IDLE_TIMEOUT_SEC;
+
+ /* Main read/echo loop */
+ while (!shutdown_requested) {
+ item.revents = 0;
+ timeout.tv_sec = POLL_TIMEOUT_SEC;
+ timeout.tv_usec = 0;
+
+ if (!SSL_poll(&item, 1, sizeof(item), &timeout, 0, &result_count)) {
+ printf("Thread %d: SSL_poll failed\n", conn_args->thread_idx);
+ ERR_print_errors_fp(stderr);
+ break;
+ }
+
+ /* Check shutdown after poll returns */
+ CRYPTO_atomic_load_int(&conn_args->shutdown_requested,
+ &shutdown_requested, atomic_lock);
+ if (shutdown_requested) {
+ printf("Thread %d: Shutdown requested\n", conn_args->thread_idx);
+ break;
+ }
+
+ /* No data this round; abandon if the client has been idle too long. */
+ if (result_count == 0 || (item.revents & SSL_POLL_EVENT_R) == 0) {
+ if (time(NULL) >= idle_deadline) {
+ printf("Thread %d: Client idle timeout, abandoning\n", conn_args->thread_idx);
+ break;
+ }
+ continue;
+ }
+
+ ret = SSL_read_ex(conn_args->conn, buf, sizeof(buf) - 1, &readbytes);
+ if (ret != 1) {
+ err = SSL_get_error(conn_args->conn, ret);
+ if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE)
+ continue;
+ if (err == SSL_ERROR_ZERO_RETURN)
+ printf("Thread %d: Client closed connection\n", conn_args->thread_idx);
+ else
+ printf("Thread %d: Read error\n", conn_args->thread_idx);
+ break;
+ }
+
+ buf[readbytes] = '\0';
+
+ /* Received data: the client is alive, so push out the idle deadline. */
+ idle_deadline = time(NULL) + CLIENT_IDLE_TIMEOUT_SEC;
+
+ /* Check for kill command - exits this thread only */
+ if (strcmp(buf, "kill\n") == 0 || strcmp(buf, "kill\r\n") == 0) {
+ printf("Thread %d: Kill command received, disconnecting\n", conn_args->thread_idx);
+ break;
+ }
+
+ /* Check for killall command - signals server-wide shutdown */
+ if (strcmp(buf, "killall\n") == 0 || strcmp(buf, "killall\r\n") == 0) {
+ printf("Thread %d: Killall command received, initiating server shutdown\n", conn_args->thread_idx);
+ if (conn_args->server_shutdown != NULL)
+ CRYPTO_atomic_store_int(conn_args->server_shutdown, 1,
+ atomic_lock);
+ break;
+ }
+
+ printf("Thread %d: Received: %s", conn_args->thread_idx, buf);
+
+ /* Echo back */
+ while (!SSL_write_ex(conn_args->conn, buf, readbytes, &written)) {
+ err = SSL_get_error(conn_args->conn, 0);
+ if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) {
+ printf("Thread %d: Write failed\n", conn_args->thread_idx);
+ ERR_print_errors_fp(stderr);
+ goto done;
+ }
+ }
+ }
+
+done:
+ /* Send graceful shutdown to client */
+ printf("Thread %d: Sending shutdown to client\n", conn_args->thread_idx);
+ SSL_shutdown(conn_args->conn);
+
+ printf("Thread %d: Exiting\n", conn_args->thread_idx);
+ CRYPTO_atomic_store_int(&conn_args->finished, 1, atomic_lock);
+}
+
+/*
+ * Platform thread glue. handle_connection() does the real work; these wrappers
+ * adapt it to the native thread entry-point signature and start/join threads.
+ */
+#if defined(OPENSSL_SYS_WINDOWS)
+
+static DWORD WINAPI thread_run(LPVOID arg)
+{
+ handle_connection(arg);
+ OPENSSL_thread_stop();
+ return 0;
+}
+
+static int run_thread(thread_t *t, void *arg)
+{
+ *t = CreateThread(NULL, 0, thread_run, arg, 0, NULL);
+ return *t != NULL;
+}
+
+static int wait_for_thread(thread_t thread)
+{
+ int ok = WaitForSingleObject(thread, INFINITE) == 0;
+
+ /* Release the handle so a long-running server does not leak them. */
+ CloseHandle(thread);
+ return ok;
+}
+
+#else
+
+static void *thread_run(void *arg)
+{
+ handle_connection(arg);
+ OPENSSL_thread_stop();
+ return NULL;
+}
+
+static int run_thread(thread_t *t, void *arg)
+{
+ return pthread_create(t, NULL, thread_run, arg) == 0;
+}
+
+static int wait_for_thread(thread_t thread)
+{
+ return pthread_join(thread, NULL) == 0;
+}
+
+#endif
+
+/*
+ * Find a free slot in the thread array.
+ * Returns the index of a free slot, or -1 if all slots are in use.
+ */
+static int find_free_thread_slot(struct connection_thread_args *threads)
+{
+ int i;
+
+ for (i = 0; i < MAX_CONNECTIONS; i++) {
+ if (!threads[i].active)
+ return i;
+ }
+ return -1;
+}
+
+/*
+ * Clean up finished threads.
+ * Called from main thread after each poll to reclaim resources from
+ * threads that have set their finished flag.
+ */
+static void cleanup_finished_threads(struct connection_thread_args *threads)
+{
+ int i;
+
+ for (i = 0; i < MAX_CONNECTIONS; i++) {
+ int finished = 0;
+
+ if (threads[i].active)
+ CRYPTO_atomic_load_int(&threads[i].finished, &finished, atomic_lock);
+ if (threads[i].active && finished) {
+ wait_for_thread(threads[i].thread);
+ SSL_free(threads[i].conn);
+ threads[i].active = 0;
+ threads[i].conn = NULL;
+ threads[i].finished = 0;
+ printf("Main: Cleaned up thread %d\n", i);
+ }
+ }
+}
+
+/*
+ * Signal all threads to shut down and wait for them to finish.
+ * Called when the server is exiting.
+ */
+static void shutdown_all_threads(struct connection_thread_args *threads)
+{
+ int i;
+
+ /* Signal all threads to terminate */
+ for (i = 0; i < MAX_CONNECTIONS; i++) {
+ if (threads[i].active)
+ CRYPTO_atomic_store_int(&threads[i].shutdown_requested, 1,
+ atomic_lock);
+ }
+
+ /* Join and clean up all threads */
+ for (i = 0; i < MAX_CONNECTIONS; i++) {
+ if (threads[i].active) {
+ wait_for_thread(threads[i].thread);
+ SSL_free(threads[i].conn);
+ threads[i].active = 0;
+ threads[i].conn = NULL;
+ printf("Main: Shut down thread %d\n", i);
+ }
+ }
+}
+
+static void run_server(void)
+{
+ SSL_CTX *ssl_ctx = NULL;
+ SSL *listener = NULL;
+ SSL *new_conn = NULL;
+ SOCKET server_fd = INVALID_SOCKET;
+ int server_shutdown = 0;
+ int shutdown_seen = 0;
+ struct connection_thread_args conn_threads[MAX_CONNECTIONS];
+ SSL_POLL_ITEM listener_item;
+ struct timeval timeout;
+ size_t result_count;
+ int slot;
+
+ /* Initialize thread array */
+ memset(conn_threads, 0, sizeof(conn_threads));
+
+ atomic_lock = CRYPTO_THREAD_lock_new();
+ if (atomic_lock == NULL) {
+ fprintf(stderr, "Unable to create lock\n");
+ goto err;
+ }
+
+ ssl_ctx = create_context(true);
+
+ /* Set the key and cert */
+ if (SSL_CTX_use_certificate_chain_file(ssl_ctx, "cert.pem") <= 0) {
+ ERR_print_errors_fp(stderr);
+ goto err;
+ }
+
+ if (SSL_CTX_use_PrivateKey_file(ssl_ctx, "key.pem", SSL_FILETYPE_PEM) <= 0) {
+ ERR_print_errors_fp(stderr);
+ goto err;
+ }
+
+ /* Create the DTLS listener with socket and BIO */
+ if (!create_dtls_listener(ssl_ctx, server_port, &listener, &server_fd)) {
+ goto err;
+ }
+
+ printf("DTLS listener started on port %d (max %d connections)\n",
+ server_port, MAX_CONNECTIONS);
+
+ /* Setup poll item for listener */
+ listener_item.desc = SSL_as_poll_descriptor(listener);
+ listener_item.events = SSL_POLL_EVENT_IC;
+
+ while (!shutdown_seen) {
+ /* Clean up any finished threads first */
+ cleanup_finished_threads(conn_threads);
+
+ /* Poll listener for incoming connections */
+ listener_item.revents = 0;
+ timeout.tv_sec = POLL_TIMEOUT_SEC;
+ timeout.tv_usec = 0;
+
+ if (!SSL_poll(&listener_item, 1, sizeof(listener_item),
+ &timeout, 0, &result_count)) {
+ ERR_print_errors_fp(stderr);
+ break;
+ }
+
+ /* Check if shutdown was requested by a connection thread */
+ CRYPTO_atomic_load_int(&server_shutdown, &shutdown_seen, atomic_lock);
+ if (shutdown_seen) {
+ printf("Main: Server shutdown requested\n");
+ break;
+ }
+
+ /* Timeout - no incoming connection, loop again */
+ if (result_count == 0 || (listener_item.revents & SSL_POLL_EVENT_IC) == 0)
+ continue;
+
+ /* Accept new connection */
+ new_conn = SSL_accept_connection(listener, SSL_ACCEPT_CONNECTION_NO_BLOCK);
+ if (new_conn == NULL) {
+ fprintf(stderr, "SSL_accept_connection failed\n");
+ ERR_print_errors_fp(stderr);
+ continue;
+ }
+
+ /* Bound the handshake retransmit backoff (see dtls_timer_cb). */
+ DTLS_set_timer_cb(new_conn, dtls_timer_cb);
+
+ /* Find free slot */
+ slot = find_free_thread_slot(conn_threads);
+ if (slot < 0) {
+ fprintf(stderr, "Connection limit reached (%d), dropping new connection\n",
+ MAX_CONNECTIONS);
+ SSL_free(new_conn);
+ continue;
+ }
+
+ /* Initialize thread args and spawn thread */
+ conn_threads[slot].conn = new_conn;
+ conn_threads[slot].thread_idx = slot;
+ conn_threads[slot].shutdown_requested = 0;
+ conn_threads[slot].finished = 0;
+ conn_threads[slot].server_shutdown = &server_shutdown;
+
+ conn_threads[slot].active = 1;
+ if (!run_thread(&conn_threads[slot].thread, &conn_threads[slot])) {
+ fprintf(stderr, "Failed to start thread for slot %d\n", slot);
+ SSL_free(new_conn);
+ conn_threads[slot].conn = NULL;
+ conn_threads[slot].active = 0;
+ continue;
+ }
+
+ printf("Main: Spawned thread %d for new connection\n", slot);
+ }
+
+ printf("Server exiting...\n");
+
+err:
+ /* Signal all threads to shut down and clean up */
+ shutdown_all_threads(conn_threads);
+
+ CRYPTO_THREAD_lock_free(atomic_lock);
+ SSL_free(listener);
+ SSL_CTX_free(ssl_ctx);
+ if (server_fd != INVALID_SOCKET)
+ BIO_closesocket((int)server_fd);
+}
+
+/*
+ * Create a DTLS client connection to the server.
+ */
+static int create_dtls_client(SSL_CTX *ssl_ctx, const char *server_name, int port,
+ SSL **client, SOCKET *client_fd)
+{
+ BIO *client_bio = NULL;
+ BIO_ADDRINFO *res = NULL;
+ const BIO_ADDRINFO *ai;
+ char port_str[6];
+ int ret = 0;
+
+ *client = NULL;
+ *client_fd = INVALID_SOCKET;
+
+ /*
+ * Resolve the server address (IPv4 or IPv6) and connect a UDP socket of the
+ * matching family to the first address that works. For UDP, BIO_connect
+ * just records the default peer.
+ */
+ BIO_snprintf(port_str, sizeof(port_str), "%d", port);
+ if (!BIO_lookup(server_name, port_str, BIO_LOOKUP_CLIENT, AF_UNSPEC,
+ SOCK_DGRAM, &res)) {
+ fprintf(stderr, "Unable to resolve server: %s\n", server_name);
+ ERR_print_errors_fp(stderr);
+ goto err;
+ }
+
+ for (ai = res; ai != NULL; ai = BIO_ADDRINFO_next(ai)) {
+ *client_fd = BIO_socket(BIO_ADDRINFO_family(ai), SOCK_DGRAM, 0, 0);
+ if (*client_fd == INVALID_SOCKET)
+ continue;
+ if (BIO_connect((int)*client_fd, BIO_ADDRINFO_address(ai), 0))
+ break;
+ BIO_closesocket((int)*client_fd);
+ *client_fd = INVALID_SOCKET;
+ }
+ BIO_ADDRINFO_free(res);
+ res = NULL;
+ if (*client_fd == INVALID_SOCKET) {
+ fprintf(stderr, "Unable to UDP connect to server: %s\n", server_name);
+ ERR_print_errors_fp(stderr);
+ goto err;
+ }
+
+ /* Set socket to non-blocking mode */
+ if (!BIO_socket_nbio((int)*client_fd, 1)) {
+ fprintf(stderr, "Unable to set socket to non-blocking\n");
+ ERR_print_errors_fp(stderr);
+ goto err;
+ }
+
+ /*
+ * Create the datagram BIO. Because the socket is already connected, the BIO
+ * auto-detects the peer (via getpeername), so no BIO_dgram_set_peer call is
+ * needed.
+ */
+ client_bio = BIO_new_dgram((int)*client_fd, BIO_NOCLOSE);
+ if (client_bio == NULL) {
+ fprintf(stderr, "Unable to create datagram BIO\n");
+ ERR_print_errors_fp(stderr);
+ goto err;
+ }
+
+ /* Create the SSL client */
+ *client = SSL_new(ssl_ctx);
+ if (*client == NULL) {
+ fprintf(stderr, "Unable to create SSL client\n");
+ ERR_print_errors_fp(stderr);
+ goto err;
+ }
+
+ /* Bound the handshake retransmit backoff (see dtls_timer_cb). */
+ DTLS_set_timer_cb(*client, dtls_timer_cb);
+
+ /*
+ * Attach the BIO to the SSL for both read and write. Because rbio and wbio
+ * are the same BIO, each set0 call takes one reference, so bump the
+ * reference count once beforehand.
+ */
+ if (!BIO_up_ref(client_bio)) {
+ fprintf(stderr, "Unable to increment BIO reference count\n");
+ ERR_print_errors_fp(stderr);
+ goto err;
+ }
+ SSL_set0_rbio(*client, client_bio);
+ SSL_set0_wbio(*client, client_bio);
+ client_bio = NULL; /* Both references transferred to the SSL */
+
+ if (!SSL_set1_dnsname(*client, server_name)) {
+ ERR_print_errors_fp(stderr);
+ goto err;
+ }
+
+ ret = 1;
+
+err:
+ BIO_free(client_bio);
+ BIO_ADDRINFO_free(res);
+ if (ret == 0) {
+ SSL_free(*client);
+ *client = NULL;
+ if (*client_fd != INVALID_SOCKET)
+ BIO_closesocket((int)*client_fd);
+ *client_fd = INVALID_SOCKET;
+ }
+ return ret;
+}
+
+/*
+ * Perform the DTLS handshake with the server.
+ * Uses SSL_poll to wait for the connection to be ready.
+ */
+static int do_client_handshake(SSL *client)
+{
+ SSL_POLL_ITEM item;
+ struct timeval timeout;
+ size_t result_count;
+ int ret, err;
+
+ while ((ret = SSL_connect(client)) != 1) {
+ err = SSL_get_error(client, ret);
+ if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) {
+ fprintf(stderr, "Handshake failed with err=%d\n", err);
+ ERR_print_errors_fp(stderr);
+ return 0;
+ }
+
+ /*
+ * Poll for the socket to be ready. Size the wait to the DTLS
+ * retransmit timer so we wake when a flight is due for retransmission;
+ * fall back to a fixed interval if no timer is armed.
+ */
+ item.desc = SSL_as_poll_descriptor(client);
+ item.events = (err == SSL_ERROR_WANT_READ) ? SSL_POLL_EVENT_R : SSL_POLL_EVENT_W;
+ item.revents = 0;
+
+ if (!DTLSv1_get_timeout(client, &timeout)) {
+ timeout.tv_sec = POLL_TIMEOUT_SEC;
+ timeout.tv_usec = 0;
+ }
+
+ if (!SSL_poll(&item, 1, sizeof(item), &timeout, 0, &result_count)) {
+ fprintf(stderr, "SSL_poll failed during handshake\n");
+ ERR_print_errors_fp(stderr);
+ return 0;
+ }
+
+ /*
+ * No inbound datagram before the timer expired: loop back into
+ * SSL_connect() so the library retransmits the last flight. A dead
+ * peer eventually exhausts the DTLS retransmit budget, which surfaces
+ * as a fatal error from SSL_connect() above and ends the loop.
+ */
+ if (result_count == 0)
+ continue;
+ }
+
+ return 1;
+}
+
+/*
+ * Send one line of user input to the server. Returns 1 to keep the client loop
+ * running, or 0 when the client should stop.
+ */
+static int process_line(SSL *client, const char *line, size_t len)
+{
+ size_t written;
+
+ if (!SSL_write_ex(client, line, len, &written)) {
+ fprintf(stderr, "Failed to send data\n");
+ ERR_print_errors_fp(stderr);
+ return 0;
+ }
+
+ if ((len == sizeof("kill\n") - 1 && memcmp(line, "kill\n", len) == 0)
+ || (len == sizeof("kill\r\n") - 1 && memcmp(line, "kill\r\n", len) == 0)) {
+ printf("Sent kill command, disconnecting\n");
+ return 0;
+ }
+
+ if ((len == sizeof("killall\n") - 1 && memcmp(line, "killall\n", len) == 0)
+ || (len == sizeof("killall\r\n") - 1
+ && memcmp(line, "killall\r\n", len) == 0)) {
+ printf("Sent killall command, server will shutdown\n");
+ return 0;
+ }
+
+ return 1;
+}
+
+static void run_client(char *rem_server_name, int dtls_version)
+{
+ SSL_CTX *ssl_ctx = NULL;
+ SSL *client = NULL;
+ SOCKET client_fd = INVALID_SOCKET;
+ char input_buf[1500];
+ char recv_buf[1500];
+ size_t readbytes;
+ int ret, err;
+ int has_server_data = 0;
+ int has_user_input = 0;
+#if !defined(OPENSSL_SYS_WINDOWS)
+ struct pollfd pfds[2];
+ BIO *rbio;
+ BIO_POLL_DESCRIPTOR rdesc;
+ int ssl_fd;
+ size_t input_used = 0;
+ ssize_t n;
+ char *nl;
+#else
+ fd_set read_fds;
+ struct timeval timeout;
+#endif
+
+ ssl_ctx = create_context(false);
+
+ /* Apply DTLS version constraint if specified */
+ if (dtls_version != 0) {
+ if (!SSL_CTX_set_min_proto_version(ssl_ctx, dtls_version)
+ || !SSL_CTX_set_max_proto_version(ssl_ctx, dtls_version)) {
+ ERR_print_errors_fp(stderr);
+ goto err;
+ }
+ printf("Forcing %s\n",
+ dtls_version == DTLS1_2_VERSION ? "DTLS 1.2" : "DTLS 1.3");
+ }
+
+ /* Abort the handshake if the server certificate cannot be verified. */
+ SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, NULL);
+
+ /*
+ * This is an atypical use case for real applications, which normally load a
+ * directory of trusted roots. Here we trust the server's certificate directly.
+ */
+ if (!SSL_CTX_load_verify_locations(ssl_ctx, "cert.pem", NULL)) {
+ ERR_print_errors_fp(stderr);
+ goto err;
+ }
+
+ /* Create DTLS client connection */
+ if (!create_dtls_client(ssl_ctx, rem_server_name, server_port, &client, &client_fd)) {
+ goto err;
+ }
+
+ printf("Connecting to %s:%d...\n", rem_server_name, server_port);
+
+ /* Perform handshake */
+ if (!do_client_handshake(client)) {
+ goto err;
+ }
+
+ printf("Connected! Type messages to send (or 'kill' to disconnect, 'killall' to shutdown server):\n");
+
+#if !defined(OPENSSL_SYS_WINDOWS)
+ /* Get the SSL socket fd for polling */
+ rbio = SSL_get_rbio(client);
+ if (rbio == NULL || !BIO_get_rpoll_descriptor(rbio, &rdesc)
+ || rdesc.type != BIO_POLL_DESCRIPTOR_TYPE_SOCK_FD) {
+ fprintf(stderr, "Failed to get SSL socket fd\n");
+ goto err;
+ }
+ ssl_fd = rdesc.value.fd;
+
+ /* Setup poll fds: [0] = stdin, [1] = SSL socket */
+ pfds[0].fd = STDIN_FILENO;
+ pfds[0].events = POLLIN;
+ pfds[1].fd = ssl_fd;
+ pfds[1].events = POLLIN;
+#else
+ /*
+ * Make stdin unbuffered so fgets() reads only up to the newline and does
+ * not pull following lines out of the console into the stdio buffer, where
+ * _kbhit() cannot see them. Any remaining lines then stay in the console
+ * buffer and are processed on subsequent loop iterations.
+ */
+ setvbuf(stdin, NULL, _IONBF, 0);
+#endif
+
+ /* Main loop: poll on both stdin and SSL connection */
+ while (1) {
+ has_server_data = 0;
+ has_user_input = 0;
+
+#if !defined(OPENSSL_SYS_WINDOWS)
+ pfds[0].revents = 0;
+ pfds[1].revents = 0;
+
+ ret = poll(pfds, 2, POLL_TIMEOUT_SEC * 1000);
+ if (ret < 0) {
+ perror("poll failed");
+ break;
+ }
+
+ has_server_data = (pfds[1].revents & POLLIN) != 0;
+ has_user_input = (pfds[0].revents & POLLIN) != 0;
+#else
+ /*
+ * Windows: use select() for socket and _kbhit() for console input.
+ * We can't easily poll stdin and a socket together on Windows, so we
+ * use a short timeout on select() and check for keyboard input.
+ */
+ FD_ZERO(&read_fds);
+ FD_SET(client_fd, &read_fds);
+ timeout.tv_sec = 0;
+ timeout.tv_usec = 100000; /* 100ms */
+
+ ret = select(0, &read_fds, NULL, NULL, &timeout);
+ if (ret < 0) {
+ fprintf(stderr, "select failed\n");
+ break;
+ }
+
+ has_server_data = (ret > 0 && FD_ISSET(client_fd, &read_fds));
+ has_user_input = _kbhit();
+#endif
+
+ /* Check for server data/shutdown first */
+ if (has_server_data) {
+ ret = SSL_read_ex(client, recv_buf, sizeof(recv_buf) - 1, &readbytes);
+ if (ret != 1) {
+ err = SSL_get_error(client, ret);
+ if (err == SSL_ERROR_ZERO_RETURN) {
+ printf("Server closed connection\n");
+ break;
+ } else if (err == SSL_ERROR_WANT_READ
+ || err == SSL_ERROR_WANT_WRITE) {
+ /*
+ * No progress possible yet. SSL_read_ex() can ask to write
+ * (e.g. a DTLS retransmission or post-handshake message)
+ * when the socket is momentarily unwritable. Either way,
+ * go back to polling and retry.
+ */
+ continue;
+ } else {
+ fprintf(stderr, "Read error from server\n");
+ ERR_print_errors_fp(stderr);
+ break;
+ }
+ }
+ recv_buf[readbytes] = '\0';
+ printf("Server: %s", recv_buf);
+ }
+
+ /* Check for user input */
+ if (has_user_input) {
+#if !defined(OPENSSL_SYS_WINDOWS)
+ n = read(STDIN_FILENO, input_buf + input_used,
+ sizeof(input_buf) - input_used);
+ if (n <= 0) {
+ if (n == 0)
+ printf("EOF received, exiting\n");
+ else
+ perror("Failed to read from stdin");
+ break;
+ }
+ input_used += (size_t)n;
+
+ while ((nl = memchr(input_buf, '\n', input_used)) != NULL) {
+ size_t linelen = (size_t)(nl - input_buf) + 1;
+ int keep_going = process_line(client, input_buf, linelen);
+
+ memmove(input_buf, input_buf + linelen, input_used - linelen);
+ input_used -= linelen;
+ if (!keep_going)
+ goto err;
+ }
+
+ /* A single line that fills the buffer without a newline */
+ if (input_used == sizeof(input_buf)) {
+ if (!process_line(client, input_buf, input_used))
+ goto err;
+ input_used = 0;
+ }
+#else
+ if (fgets(input_buf, sizeof(input_buf), stdin) == NULL) {
+ printf("EOF received, exiting\n");
+ break;
+ }
+ if (!process_line(client, input_buf, strlen(input_buf)))
+ break;
+#endif
+ }
+ }
+
+err:
+ /* Send a graceful shutdown (close_notify) to the server before freeing. */
+ if (client != NULL)
+ SSL_shutdown(client);
+ SSL_free(client);
+ SSL_CTX_free(ssl_ctx);
+ if (client_fd != INVALID_SOCKET)
+ BIO_closesocket((int)client_fd);
+}
+
+static void usage(void)
+{
+ printf("Usage: dtlslistenerecho s\n");
+ printf(" --or--\n");
+ printf(" dtlslistenerecho c hostname [dtls12|dtls13]\n");
+ printf(" c=client, s=server, hostname=hostname of server\n");
+ printf(" dtls12=force DTLS 1.2, dtls13=force DTLS 1.3 (optional)\n");
+ exit(EXIT_FAILURE);
+}
+
+int main(int argc, char **argv)
+{
+ bool isServer;
+ char *rem_server_name = NULL;
+ int dtls_version = 0;
+
+ /* Need to know if client or server */
+ if (argc < 2) {
+ usage();
+ /* NOTREACHED */
+ }
+
+ isServer = (argv[1][0] == 's') ? true : false;
+
+ /* If client get remote server address */
+ if (!isServer) {
+ if (argc < 3) {
+ usage();
+ /* NOTREACHED */
+ }
+ rem_server_name = argv[2];
+
+ /* Check for optional DTLS version argument */
+ if (argc >= 4) {
+ if (strcmp(argv[3], "dtls12") == 0) {
+ dtls_version = DTLS1_2_VERSION;
+ } else if (strcmp(argv[3], "dtls13") == 0) {
+ dtls_version = DTLS1_3_VERSION;
+ } else {
+ fprintf(stderr, "Unknown protocol version: %s\n", argv[3]);
+ usage();
+ /* NOTREACHED */
+ }
+ }
+ }
+
+ if (isServer) {
+ run_server();
+ } else {
+ run_client(rem_server_name, dtls_version);
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/doc/man7/ossl-guide-dtlsv13.pod b/doc/man7/ossl-guide-dtlsv13.pod
index af80511476..b565337b42 100644
--- a/doc/man7/ossl-guide-dtlsv13.pod
+++ b/doc/man7/ossl-guide-dtlsv13.pod
@@ -115,9 +115,80 @@ finished sending Early Data.
=head1 HELLO RETRY REQUEST
-To utilize HelloRetryRequest and cookie exchange to validate the client in DTLSv1.3,
-use the L<SSL_stateless(3)> function. L<DTLSv1_listen(3)> will still use HelloVerifyRequest to
-validate the client.
+For DTLSv1.3 server applications, use L<SSL_new_listener(3)> with the
+B<SSL_LISTENER_FLAG_REQUIRE_HRR> flag to enable HelloRetryRequest cookie validation.
+This provides connection demultiplexing for multiple clients on a single UDP socket.
+
+Note that L<DTLSv1_listen(3)> only supports DTLS 1.0/1.2 with HelloVerifyRequest and
+cannot be used with DTLSv1.3.
+
+=head1 DTLS LISTENER API
+
+For DTLSv1.3 server applications that need to handle multiple clients on a single
+UDP socket, the SSL Listener API provides connection demultiplexing and address
+validation. Unlike L<DTLSv1_listen(3)> which only supports DTLS 1.0/1.2 with
+HelloVerifyRequest, the listener API fully supports DTLSv1.3 with HelloRetryRequest
+cookie validation.
+
+Create a DTLS listener using L<SSL_new_listener(3)> with an SSL_CTX configured
+for DTLS. The following flags control listener behavior:
+
+=over 4
+
+=item B<SSL_LISTENER_FLAG_NO_VALIDATE>
+
+Disables all address validation. The listener will not send HelloVerifyRequest
+(for DTLS 1.0/1.2) or HelloRetryRequest with cookie (for DTLSv1.3). This is
+faster but provides no protection against amplification attacks. Not recommended
+for use in untrusted network environments.
+
+=item B<SSL_LISTENER_FLAG_REQUIRE_HVR>
+
+Requires HelloVerifyRequest (HVR) cookie exchange for DTLS 1.0 and DTLS 1.2
+connections. This provides protection against amplification attacks for pre-1.3
+DTLS versions.
+
+=item B<SSL_LISTENER_FLAG_REQUIRE_HRR>
+
+Requires HelloRetryRequest (HRR) cookie exchange for DTLSv1.3 connections. This
+provides protection against amplification attacks for DTLSv1.3. Both
+B<SSL_LISTENER_FLAG_REQUIRE_HVR> and B<SSL_LISTENER_FLAG_REQUIRE_HRR> may be
+specified together to enable address validation for all supported DTLS versions.
+
+=item B<SSL_LISTENER_FLAG_SINGLE_THREAD>
+
+Specifies that the DTLS listener will operate in single-threaded mode. When this
+flag is set, the listener and all connections accepted from it should only be
+used from a single thread. This avoids the overhead of internal synchronization
+mechanisms.
+
+=back
+
+By default (when B<SSL_LISTENER_FLAG_SINGLE_THREAD> is not set), the listener
+initializes internal synchronization mechanisms that allow connections accepted
+from the listener to be safely used from multiple threads concurrently. This
+includes a notifier mechanism that enables efficient polling across threads.
+
+After attaching a UDP socket BIO with L<SSL_set0_rbio(3)> and L<SSL_set0_wbio(3)>
+and calling L<SSL_listen(3)>, use L<SSL_accept_connection(3)> to accept incoming
+connections. Each accepted connection has completed cookie validation (if required)
+but still requires L<SSL_do_handshake(3)> or L<SSL_accept(3)> to complete the TLS
+handshake before application data can be exchanged.
+
+The following tunables are available via L<SSL_get_value_uint(3)> /
+L<SSL_set_value_uint(3)>: B<SSL_VALUE_DTLS_LISTENER_MAX_PENDING_CONNS>
+(pending-connection cap, default 256), B<SSL_VALUE_DTLS_LISTENER_PENDING_TIMEOUT>
+(pending connection timeout in milliseconds, default 30000; pending connections
+that do not complete their handshake within this period are automatically cleaned
+up), and B<SSL_VALUE_DTLS_LISTENER_MAX_DGRAM_SIZE> (maximum received datagram size
+in bytes, default 2000).
+
+See L<SSL_new_listener(3)> for complete API documentation.
+
+The complete source code for an example DTLS listener server (and a matching
+client) is available in the B<demos/dtlslistenerecho> directory of the OpenSSL
+source distribution in the file B<main.c>. It is also available online at
+L<https://github.com/openssl/openssl/blob/master/demos/dtlslistenerecho/main.c>.
=head1 APPLICATION DATA
@@ -163,6 +234,39 @@ Use the -dtls option to specify that you want to use DTLS instead of TLS.
Use the min_protocol and max_protocol parameters and set them to DTLSv1.3.
+=head1 DEMOS
+
+OpenSSL is distributed with two DTLS demo applications that illustrate the
+concepts described on this page. They can be found in the B<demos> directory
+of the OpenSSL source distribution:
+
+=over 4
+
+=item B<demos/dtlsecho>
+
+A simple DTLSv1.3 echo client and server built on a single B<SSL> object per
+connection. The complete source code is available in the B<demos/dtlsecho>
+directory of the OpenSSL source distribution in the file B<main.c>. It is also
+available online at
+L<https://github.com/openssl/openssl/blob/master/demos/dtlsecho/main.c>.
+
+=item B<demos/dtlslistenerecho>
+
+A DTLS echo server that uses the DTLS listener API (L<SSL_new_listener(3)>,
+L<SSL_listen(3)> and L<SSL_accept_connection(3)>) to demultiplex and handle
+multiple client connections on a single UDP socket, together with a matching
+client. The complete source code is available in the B<demos/dtlslistenerecho>
+directory of the OpenSSL source distribution in the file B<main.c>. It is also
+available online at
+L<https://github.com/openssl/openssl/blob/master/demos/dtlslistenerecho/main.c>.
+
+=back
+
+=head1 SEE ALSO
+
+L<ossl-guide-introduction(7)>, L<ossl-guide-libraries-introduction(7)>,
+L<ossl-guide-libssl-introduction(7)>, L<ossl-guide-tls-introduction(7)>
+
=head1 COPYRIGHT
Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.