Stream: DTLS support.

The change adds support for DTLS encryption to udp connections.
TLS and DTLS connections may coexist in one server.

DTLS termination example:

    listen 9000 udp ssl;

DTLS proxy example:

    listen 9000 udp;
    proxy_ssl on;
    proxy_pass 127.0.0.1:9000;

Based on previous work by Vladimir Homutov.
This commit is contained in:
Roman Arutyunyan 2024-11-21 08:43:27 +04:00
parent 37728de184
commit fa3d4ad088
8 changed files with 411 additions and 18 deletions

View File

@ -11,10 +11,16 @@
#define NGX_SSL_PASSWORD_BUFFER_SIZE 4096
#define NGX_SSL_COOKIE_SECRET_LENGTH 32
typedef struct {
ngx_uint_t engine; /* unsigned engine:1; */
ngx_uint_t engine; /* unsigned engine:1; */
#if (NGX_SSL_DTLS)
u_char cookie_secret[NGX_SSL_COOKIE_SECRET_LENGTH];
BIO_METHOD *method;
#endif
} ngx_openssl_conf_t;
@ -82,6 +88,17 @@ static time_t ngx_ssl_parse_time(
#endif
ASN1_TIME *asn1time, ngx_log_t *log);
#if (NGX_SSL_DTLS)
static ngx_int_t ngx_ssl_try_listen(ngx_connection_t *c);
static int ngx_ssl_shared_send(BIO *b, const char *in, int inl);
static int ngx_ssl_shared_recv(BIO *b, char *out, int outl);
static long ngx_ssl_shared_ctrl(BIO *b, int cmd, long num, void *ptr);
static int ngx_ssl_generate_cookie(SSL *ssl, unsigned char *cookie,
unsigned int *cookie_len);
static int ngx_ssl_verify_cookie(SSL *ssl, const unsigned char *cookie,
unsigned int cookie_len);
#endif
static void *ngx_openssl_create_conf(ngx_cycle_t *cycle);
static char *ngx_openssl_engine(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static void ngx_openssl_exit(ngx_cycle_t *cycle);
@ -276,7 +293,18 @@ ngx_ssl_init(ngx_log_t *log)
ngx_int_t
ngx_ssl_create(ngx_ssl_t *ssl, ngx_uint_t protocols, void *data)
{
ssl->ctx = SSL_CTX_new(SSLv23_method());
const SSL_METHOD *method;
#if (NGX_SSL_DTLS)
if (ssl->udp) {
method = DTLS_method();
} else
#endif
{
method = SSLv23_method();
}
ssl->ctx = SSL_CTX_new(method);
if (ssl->ctx == NULL) {
ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, "SSL_CTX_new() failed");
@ -410,6 +438,11 @@ ngx_ssl_create(ngx_ssl_t *ssl, ngx_uint_t protocols, void *data)
SSL_CTX_set_info_callback(ssl->ctx, ngx_ssl_info_callback);
#if (NGX_SSL_DTLS)
SSL_CTX_set_cookie_generate_cb(ssl->ctx, ngx_ssl_generate_cookie);
SSL_CTX_set_cookie_verify_cb(ssl->ctx, ngx_ssl_verify_cookie);
#endif
return NGX_OK;
}
@ -1615,6 +1648,28 @@ ngx_ssl_create_connection(ngx_ssl_t *ssl, ngx_connection_t *c, ngx_uint_t flags)
return NGX_ERROR;
}
#if (NGX_SSL_DTLS)
if (c->shared) {
BIO *bio;
ngx_openssl_conf_t *oscf;
sc->try_listen = 1;
oscf = (ngx_openssl_conf_t *) ngx_get_conf(ngx_cycle->conf_ctx,
ngx_openssl_module);
bio = BIO_new(oscf->method);
if (bio == NULL) {
return NGX_ERROR;
}
BIO_set_app_data(bio, c);
BIO_set_init(bio, 1);
SSL_set_bio(sc->connection, bio, bio);
} else
#endif
if (SSL_set_fd(sc->connection, c->fd) == 0) {
ngx_ssl_error(NGX_LOG_ALERT, c->log, 0, "SSL_set_fd() failed");
return NGX_ERROR;
@ -1688,6 +1743,12 @@ ngx_ssl_handshake(ngx_connection_t *c)
ngx_err_t err;
ngx_int_t rc;
#if (NGX_SSL_DTLS)
if (c->ssl->try_listen) {
return ngx_ssl_try_listen(c);
}
#endif
#ifdef SSL_READ_EARLY_DATA_SUCCESS
if (c->ssl->try_early_data) {
return ngx_ssl_try_early_data(c);
@ -5868,6 +5929,227 @@ ngx_ssl_parse_time(
}
#if (NGX_SSL_DTLS)
static ngx_int_t
ngx_ssl_try_listen(ngx_connection_t *c)
{
int n, sslerr;
BIO *rbio;
ngx_err_t err;
#ifdef LIBRESSL_VERSION_NUMBER
static ngx_sockaddr_t speer;
static struct sockaddr *peer;
#else
static BIO_ADDR *peer;
#endif
if (peer == NULL) {
#ifdef LIBRESSL_VERSION_NUMBER
peer = &speer.sockaddr;
#else
peer = BIO_ADDR_new();
if (peer == NULL) {
ngx_ssl_error(NGX_LOG_ALERT, c->log, 0, "BIO_ADDR_new() failed");
return NGX_ERROR;
}
#endif
}
n = DTLSv1_listen(c->ssl->connection, peer);
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "DTLSv1_listen: %d", n);
if (n > 0) {
c->ssl->try_listen = 0;
return ngx_ssl_handshake(c);
}
if (n == 0) {
return NGX_ERROR;
}
/* n < 0 */
rbio = SSL_get_rbio(c->ssl->connection);
if (rbio && BIO_should_retry(rbio)) {
return NGX_ERROR;
}
sslerr = SSL_get_error(c->ssl->connection, n);
err = (sslerr == SSL_ERROR_SYSCALL) ? ngx_errno : 0;
c->read->error = 1;
ngx_ssl_connection_error(c, sslerr, err, "DTLSv1_listen() failed");
return NGX_ERROR;
}
static int
ngx_ssl_shared_send(BIO *b, const char *in, int inl)
{
ssize_t n;
ngx_connection_t *c;
c = BIO_get_app_data(b);
BIO_clear_retry_flags(b);
n = ngx_udp_send(c, (u_char *) in, inl);
if (n == NGX_ERROR) {
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
"ssl shared send failed");
return -1;
}
if (n == NGX_AGAIN) {
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
"ssl shared send not ready");
BIO_set_retry_write(b);
return -1;
}
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "ssl shared send n:%z", n);
return n;
}
static int
ngx_ssl_shared_recv(BIO *b, char *out, int outl)
{
ssize_t n;
ngx_buf_t *bb;
ngx_connection_t *c;
c = BIO_get_app_data(b);
n = 0;
if (out) {
BIO_clear_retry_flags(b);
bb = c->buffer;
if (bb && bb->pos != bb->last) {
/* TODO move to ngx_udp_shared_recv() */
n = ngx_min(outl, bb->last - bb->pos);
ngx_memcpy(out, bb->pos, n);
bb->pos = bb->last;
goto done;
}
n = ngx_udp_shared_recv(c, (u_char *) out, outl);
if (n == NGX_ERROR) {
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
"ssl shared recv failed");
return -1;
}
if (n == NGX_AGAIN) {
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
"ssl shared recv not ready");
BIO_set_retry_read(b);
return -1;
}
}
done:
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, "ssl shared recv n:%z", n);
return n;
}
static long
ngx_ssl_shared_ctrl(BIO *b, int cmd, long num, void *ptr)
{
long ret;
#if (NGX_DEBUG)
ngx_connection_t *c;
c = BIO_get_app_data(b);
ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0,
"ssl shared ctrl c:%d n:%l", cmd, num);
#endif
switch (cmd) {
case BIO_CTRL_DGRAM_QUERY_MTU:
case BIO_CTRL_DGRAM_GET_MTU:
case BIO_CTRL_DGRAM_GET_FALLBACK_MTU:
/* TODO MTU discovery */
ret = 1200;
break;
case BIO_CTRL_FLUSH:
ret = 1;
break;
default:
ret = 0;
break;
}
return ret;
}
static int
ngx_ssl_generate_cookie(SSL *ssl, unsigned char *cookie,
unsigned int *cookie_len)
{
ngx_connection_t *c;
ngx_openssl_conf_t *oscf;
c = ngx_ssl_get_connection(ssl);
oscf = (ngx_openssl_conf_t *) ngx_get_conf(ngx_cycle->conf_ctx,
ngx_openssl_module);
/* SHA256_DIGEST_LENGTH <= DTLS1_COOKIE_LENGTH */
if (HMAC(EVP_sha256(), oscf->cookie_secret, NGX_SSL_COOKIE_SECRET_LENGTH,
(u_char *) c->sockaddr, c->socklen, cookie, cookie_len)
== NULL)
{
return 0;
}
return 1;
}
static int
ngx_ssl_verify_cookie(SSL *ssl, const unsigned char *cookie,
unsigned int cookie_len)
{
unsigned int ocookie_len;
u_char ocookie[DTLS1_COOKIE_LENGTH];
if (ngx_ssl_generate_cookie(ssl, ocookie, &ocookie_len) == 0) {
return 0;
}
if (cookie_len != ocookie_len
|| ngx_memcmp(cookie, ocookie, cookie_len) != 0)
{
return 0;
}
return 1;
}
#endif
static void *
ngx_openssl_create_conf(ngx_cycle_t *cycle)
{
@ -5884,6 +6166,23 @@ ngx_openssl_create_conf(ngx_cycle_t *cycle)
* oscf->engine = 0;
*/
#if (NGX_SSL_DTLS)
if (!RAND_bytes(oscf->cookie_secret, NGX_SSL_COOKIE_SECRET_LENGTH)) {
return NULL;
}
oscf->method = BIO_meth_new(BIO_TYPE_DGRAM, "nginx dgram wrapper");
if (oscf->method == NULL) {
return NULL;
}
BIO_meth_set_write(oscf->method, ngx_ssl_shared_send);
BIO_meth_set_read(oscf->method, ngx_ssl_shared_recv);
BIO_meth_set_ctrl(oscf->method, ngx_ssl_shared_ctrl);
#endif
return oscf;
}

View File

@ -63,6 +63,10 @@
#endif
#if (!defined OPENSSL_IS_BORINGSSL && !defined OPENSSL_NO_DTLS \
&& !NGX_WIN32 && OPENSSL_VERSION_NUMBER >= 0x10100000L)
#define NGX_SSL_DTLS 1
#endif
#define ngx_ssl_session_t SSL_SESSION
#define ngx_ssl_conn_t SSL
@ -95,6 +99,10 @@ struct ngx_ssl_s {
ngx_rbtree_t staple_rbtree;
ngx_rbtree_node_t staple_sentinel;
#if (NGX_SSL_DTLS)
ngx_uint_t udp; /* unsigned udp:1; */
#endif
};
@ -128,6 +136,7 @@ struct ngx_ssl_connection_s {
unsigned shutdown_without_free:1;
unsigned handshake_buffer_set:1;
unsigned session_timeout_set:1;
unsigned try_listen:1;
unsigned try_early_data:1;
unsigned in_early:1;
unsigned in_ocsp:1;

View File

@ -13,8 +13,6 @@
#if !(NGX_WIN32)
static void ngx_close_accepted_udp_connection(ngx_connection_t *c);
static ssize_t ngx_udp_shared_recv(ngx_connection_t *c, u_char *buf,
size_t size);
static ngx_int_t ngx_insert_udp_connection(ngx_connection_t *c);
static ngx_connection_t *ngx_lookup_udp_connection(ngx_listening_t *ls,
struct sockaddr *sockaddr, socklen_t socklen,
@ -365,7 +363,7 @@ ngx_close_accepted_udp_connection(ngx_connection_t *c)
}
static ssize_t
ssize_t
ngx_udp_shared_recv(ngx_connection_t *c, u_char *buf, size_t size)
{
ssize_t n;

View File

@ -56,6 +56,7 @@ ngx_int_t ngx_get_srcaddr_cmsg(struct cmsghdr *cmsg,
void ngx_event_recvmsg(ngx_event_t *ev);
ssize_t ngx_sendmsg(ngx_connection_t *c, struct msghdr *msg, int flags);
ssize_t ngx_udp_shared_recv(ngx_connection_t *c, u_char *buf, size_t size);
void ngx_udp_rbtree_insert_value(ngx_rbtree_node_t *temp,
ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel);
#endif

View File

@ -1237,7 +1237,7 @@ ngx_stream_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
}
#endif
#if (NGX_STREAM_SSL)
#if (NGX_STREAM_SSL && !NGX_SSL_DTLS)
if (lsopt.ssl) {
return "\"ssl\" parameter is incompatible with \"udp\"";
}

View File

@ -53,6 +53,9 @@ typedef struct {
ngx_array_t *ssl_conf_commands;
ngx_ssl_t *ssl;
#if (NGX_SSL_DTLS)
ngx_ssl_t *ssl_udp;
#endif
#endif
ngx_stream_upstream_srv_conf_t *upstream;
@ -822,9 +825,9 @@ ngx_stream_proxy_init_upstream(ngx_stream_session_t *s)
#if (NGX_STREAM_SSL)
if (pc->type == SOCK_STREAM && pscf->ssl_enable) {
if (pscf->ssl_enable) {
if (u->proxy_protocol) {
if (pc->type == SOCK_STREAM && u->proxy_protocol) {
if (ngx_stream_proxy_send_proxy_protocol(s) != NGX_OK) {
return;
}
@ -1068,6 +1071,7 @@ static void
ngx_stream_proxy_ssl_init_connection(ngx_stream_session_t *s)
{
ngx_int_t rc;
ngx_ssl_t *ssl;
ngx_connection_t *pc;
ngx_stream_upstream_t *u;
ngx_stream_proxy_srv_conf_t *pscf;
@ -1078,7 +1082,16 @@ ngx_stream_proxy_ssl_init_connection(ngx_stream_session_t *s)
pscf = ngx_stream_get_module_srv_conf(s, ngx_stream_proxy_module);
if (ngx_ssl_create_connection(pscf->ssl, pc, NGX_SSL_BUFFER|NGX_SSL_CLIENT)
#if (NGX_SSL_DTLS)
if (pc->type == SOCK_DGRAM) {
ssl = pscf->ssl_udp;
} else
#endif
{
ssl = pscf->ssl;
}
if (ngx_ssl_create_connection(ssl, pc, NGX_SSL_BUFFER|NGX_SSL_CLIENT)
!= NGX_OK)
{
ngx_stream_proxy_finalize(s, NGX_STREAM_INTERNAL_SERVER_ERROR);
@ -2092,6 +2105,7 @@ ngx_stream_proxy_create_srv_conf(ngx_conf_t *cf)
* conf->ssl_crl = { 0, NULL };
*
* conf->ssl = NULL;
* conf->ssl_udp = NULL;
* conf->upstream = NULL;
* conf->upstream_value = NULL;
*/
@ -2215,6 +2229,13 @@ ngx_stream_proxy_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
if (ngx_stream_proxy_set_ssl(cf, conf, conf->ssl) != NGX_OK) {
return NGX_CONF_ERROR;
}
#if (NGX_SSL_DTLS)
conf->ssl_udp->udp = 1;
if (ngx_stream_proxy_set_ssl(cf, conf, conf->ssl_udp) != NGX_OK) {
return NGX_CONF_ERROR;
}
#endif
}
#endif
@ -2245,6 +2266,9 @@ ngx_stream_proxy_merge_ssl(ngx_conf_t *cf, ngx_stream_proxy_srv_conf_t *conf,
{
if (prev->ssl) {
conf->ssl = prev->ssl;
#if (NGX_SSL_DTLS)
conf->ssl_udp = prev->ssl_udp;
#endif
return NGX_OK;
}
@ -2261,6 +2285,15 @@ ngx_stream_proxy_merge_ssl(ngx_conf_t *cf, ngx_stream_proxy_srv_conf_t *conf,
conf->ssl->log = cf->log;
#if (NGX_SSL_DTLS)
conf->ssl_udp = ngx_pcalloc(cf->pool, sizeof(ngx_ssl_t));
if (conf->ssl_udp == NULL) {
return NGX_ERROR;
}
conf->ssl_udp->log = cf->log;
#endif
/*
* special handling to preserve conf->ssl
* in the "stream" section to inherit it to all servers
@ -2268,6 +2301,9 @@ ngx_stream_proxy_merge_ssl(ngx_conf_t *cf, ngx_stream_proxy_srv_conf_t *conf,
if (preserve) {
prev->ssl = conf->ssl;
#if (NGX_SSL_DTLS)
prev->ssl_udp = conf->ssl_udp;
#endif
}
return NGX_OK;

View File

@ -403,6 +403,7 @@ ngx_stream_ssl_handler(ngx_stream_session_t *s)
{
long rc;
X509 *cert;
ngx_ssl_t *ssl;
ngx_int_t rv;
ngx_connection_t *c;
ngx_stream_ssl_srv_conf_t *sscf;
@ -418,7 +419,16 @@ ngx_stream_ssl_handler(ngx_stream_session_t *s)
if (c->ssl == NULL) {
c->log->action = "SSL handshaking";
rv = ngx_stream_ssl_init_connection(&sscf->ssl, c);
#if (NGX_SSL_DTLS)
if (c->type == SOCK_DGRAM) {
ssl = &sscf->ssl_udp;
} else
#endif
{
ssl = &sscf->ssl;
}
rv = ngx_stream_ssl_init_connection(ssl, c);
if (rv != NGX_OK) {
return rv;
@ -472,7 +482,9 @@ ngx_stream_ssl_init_connection(ngx_ssl_t *ssl, ngx_connection_t *c)
cscf = ngx_stream_get_module_srv_conf(s, ngx_stream_core_module);
if (cscf->tcp_nodelay && ngx_tcp_nodelay(c) != NGX_OK) {
if (c->type == SOCK_STREAM
&& cscf->tcp_nodelay && ngx_tcp_nodelay(c) != NGX_OK)
{
return NGX_ERROR;
}
@ -529,6 +541,7 @@ ngx_stream_ssl_servername(ngx_ssl_conn_t *ssl_conn, int *ad, void *arg)
{
ngx_int_t rc;
ngx_str_t host;
ngx_ssl_t *ssl;
const char *servername;
ngx_connection_t *c;
ngx_stream_session_t *s;
@ -589,8 +602,17 @@ ngx_stream_ssl_servername(ngx_ssl_conn_t *ssl_conn, int *ad, void *arg)
sscf = ngx_stream_get_module_srv_conf(s, ngx_stream_ssl_module);
if (sscf->ssl.ctx) {
if (SSL_set_SSL_CTX(ssl_conn, sscf->ssl.ctx) == NULL) {
#if (NGX_SSL_DTLS)
if (c->type == SOCK_DGRAM) {
ssl = &sscf->ssl_udp;
} else
#endif
{
ssl = &sscf->ssl;
}
if (ssl->ctx) {
if (SSL_set_SSL_CTX(ssl_conn, ssl->ctx) == NULL) {
goto error;
}
@ -599,18 +621,18 @@ ngx_stream_ssl_servername(ngx_ssl_conn_t *ssl_conn, int *ad, void *arg)
* adjust other things we care about
*/
SSL_set_verify(ssl_conn, SSL_CTX_get_verify_mode(sscf->ssl.ctx),
SSL_CTX_get_verify_callback(sscf->ssl.ctx));
SSL_set_verify(ssl_conn, SSL_CTX_get_verify_mode(ssl->ctx),
SSL_CTX_get_verify_callback(ssl->ctx));
SSL_set_verify_depth(ssl_conn, SSL_CTX_get_verify_depth(sscf->ssl.ctx));
SSL_set_verify_depth(ssl_conn, SSL_CTX_get_verify_depth(ssl->ctx));
#if OPENSSL_VERSION_NUMBER >= 0x009080dfL
/* only in 0.9.8m+ */
SSL_clear_options(ssl_conn, SSL_get_options(ssl_conn) &
~SSL_CTX_get_options(sscf->ssl.ctx));
~SSL_CTX_get_options(ssl->ctx));
#endif
SSL_set_options(ssl_conn, SSL_CTX_get_options(sscf->ssl.ctx));
SSL_set_options(ssl_conn, SSL_CTX_get_options(ssl->ctx));
#ifdef SSL_OP_NO_RENEGOTIATION
SSL_set_options(ssl_conn, SSL_OP_NO_RENEGOTIATION);
@ -943,6 +965,13 @@ ngx_stream_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
return NGX_CONF_ERROR;
}
#if (NGX_SSL_DTLS)
conf->ssl_udp.udp = 1;
if (ngx_stream_ssl_set_ssl(cf, conf, prev, &conf->ssl_udp) != NGX_OK) {
return NGX_CONF_ERROR;
}
#endif
return NGX_CONF_OK;
}
@ -1527,6 +1556,15 @@ ngx_stream_ssl_init(ngx_conf_t *cf)
{
return NGX_ERROR;
}
#if (NGX_SSL_DTLS)
if (ngx_ssl_stapling_resolver(cf, &sscf->ssl_udp, cscf->resolver,
cscf->resolver_timeout)
!= NGX_OK)
{
return NGX_ERROR;
}
#endif
}
if (sscf->ocsp) {
@ -1536,6 +1574,15 @@ ngx_stream_ssl_init(ngx_conf_t *cf)
{
return NGX_ERROR;
}
#if (NGX_SSL_DTLS)
if (ngx_ssl_ocsp_resolver(cf, &sscf->ssl_udp, cscf->resolver,
cscf->resolver_timeout)
!= NGX_OK)
{
return NGX_ERROR;
}
#endif
}
}

View File

@ -21,6 +21,9 @@ typedef struct {
ngx_flag_t reject_handshake;
ngx_ssl_t ssl;
#if (NGX_SSL_DTLS)
ngx_ssl_t ssl_udp;
#endif
ngx_uint_t protocols;