From ce5e7b26bbf67a3eb36f520345e046813f528eb1 Mon Sep 17 00:00:00 2001 From: Vadim Zhestikov Date: Tue, 12 May 2026 09:28:01 -0700 Subject: [PATCH] Stream: proxy_protocol_tlv directive for PROXY protocol v2. Adds a proxy_protocol_tlv directive to the stream proxy module, allowing arbitrary TLV extensions to be appended to the PROXY protocol v2 upstream header: proxy_protocol_tlv 0x05 "custom-value"; proxy_protocol_tlv 0xe7 $variable; Values are compiled as complex values, so they may contain nginx variables evaluated per request. The directive is rejected at configuration time when the reserved CRC32c checksum TLV type (0x03) is specified; that type will be covered by the upcoming proxy_protocol_crc32c directive. --- src/stream/ngx_stream_proxy_module.c | 181 +++++++++++++++++++++++++-- 1 file changed, 174 insertions(+), 7 deletions(-) diff --git a/src/stream/ngx_stream_proxy_module.c b/src/stream/ngx_stream_proxy_module.c index 0c58854b1..5b3ebdd1a 100644 --- a/src/stream/ngx_stream_proxy_module.c +++ b/src/stream/ngx_stream_proxy_module.c @@ -19,6 +19,12 @@ typedef struct { } ngx_stream_upstream_local_t; +typedef struct { + ngx_uint_t type; + ngx_stream_complex_value_t *value; +} ngx_stream_proxy_protocol_tlv_t; + + typedef struct { ngx_msec_t connect_timeout; ngx_msec_t timeout; @@ -32,6 +38,7 @@ typedef struct { ngx_flag_t next_upstream; ngx_flag_t proxy_protocol; ngx_uint_t proxy_protocol_version; + ngx_array_t *proxy_protocol_tlvs; ngx_flag_t half_close; ngx_stream_upstream_local_t *local; ngx_flag_t socket_keepalive; @@ -93,6 +100,8 @@ static char *ngx_stream_proxy_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static char *ngx_stream_proxy_bind(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); +static char *ngx_stream_proxy_protocol_tlv(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); #if (NGX_STREAM_SSL) @@ -272,6 +281,13 @@ static ngx_command_t ngx_stream_proxy_commands[] = { offsetof(ngx_stream_proxy_srv_conf_t, proxy_protocol_version), &ngx_stream_proxy_protocol_versions }, + { ngx_string("proxy_protocol_tlv"), + NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE2, + ngx_stream_proxy_protocol_tlv, + NGX_STREAM_SRV_CONF_OFFSET, + 0, + NULL }, + { ngx_string("proxy_half_close"), NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_FLAG, ngx_conf_set_flag_slot, @@ -834,11 +850,52 @@ ngx_stream_proxy_connect(ngx_stream_session_t *s) } +static ngx_array_t * +ngx_stream_proxy_build_tlvs(ngx_stream_session_t *s, ngx_array_t *conf_tlvs, + size_t *sizep) +{ + ngx_uint_t i; + ngx_str_t val; + ngx_array_t *tlvs; + ngx_stream_proxy_protocol_tlv_t *ctlv; + ngx_proxy_protocol_write_tlv_t *tlv; + + tlvs = ngx_array_create(s->connection->pool, conf_tlvs->nelts, + sizeof(ngx_proxy_protocol_write_tlv_t)); + if (tlvs == NULL) { + return NULL; + } + + *sizep = 0; + ctlv = conf_tlvs->elts; + + for (i = 0; i < conf_tlvs->nelts; i++) { + if (ngx_stream_complex_value(s, ctlv[i].value, &val) != NGX_OK) { + return NULL; + } + + tlv = ngx_array_push(tlvs); + if (tlv == NULL) { + return NULL; + } + + tlv->type = ctlv[i].type; + tlv->value = val; + + *sizep += 3 + val.len; /* TLV wire format: type(1) + len(2) + value */ + } + + return tlvs; +} + + static void ngx_stream_proxy_init_upstream(ngx_stream_session_t *s) { u_char *p; + size_t buf_size, tlv_size; ngx_chain_t *cl; + ngx_array_t *tlvs; ngx_connection_t *c, *pc; ngx_log_handler_pt handler; ngx_stream_upstream_t *u; @@ -953,7 +1010,23 @@ ngx_stream_proxy_init_upstream(ngx_stream_session_t *s) return; } - p = ngx_pnalloc(c->pool, NGX_PROXY_PROTOCOL_V1_MAX_HEADER); + buf_size = NGX_PROXY_PROTOCOL_V2_MAX_HEADER; + tlvs = NULL; + + if (pscf->proxy_protocol_version == 2 + && pscf->proxy_protocol_tlvs != NULL) + { + tlvs = ngx_stream_proxy_build_tlvs(s, pscf->proxy_protocol_tlvs, + &tlv_size); + if (tlvs == NULL) { + ngx_stream_proxy_finalize(s, NGX_STREAM_INTERNAL_SERVER_ERROR); + return; + } + + buf_size += tlv_size; + } + + p = ngx_pnalloc(c->pool, buf_size); if (p == NULL) { ngx_stream_proxy_finalize(s, NGX_STREAM_INTERNAL_SERVER_ERROR); return; @@ -962,8 +1035,7 @@ ngx_stream_proxy_init_upstream(ngx_stream_session_t *s) cl->buf->pos = p; if (pscf->proxy_protocol_version == 2) { - p = ngx_proxy_protocol_v2_write(c, p, - p + NGX_PROXY_PROTOCOL_V2_MAX_HEADER); + p = ngx_proxy_protocol_v2_write_tlvs(c, p, p + buf_size, tlvs); } else { p = ngx_proxy_protocol_write(c, p, p + NGX_PROXY_PROTOCOL_V1_MAX_HEADER); @@ -1007,12 +1079,14 @@ ngx_stream_proxy_init_upstream(ngx_stream_session_t *s) static ngx_int_t ngx_stream_proxy_send_proxy_protocol(ngx_stream_session_t *s) { - u_char *p; + u_char *p, *buf; + size_t buf_size, tlv_size; ssize_t n, size; + ngx_array_t *tlvs; ngx_connection_t *c, *pc; ngx_stream_upstream_t *u; ngx_stream_proxy_srv_conf_t *pscf; - u_char buf[NGX_PROXY_PROTOCOL_V1_MAX_HEADER]; + u_char stack_buf[NGX_PROXY_PROTOCOL_V2_MAX_HEADER]; c = s->connection; @@ -1021,9 +1095,30 @@ ngx_stream_proxy_send_proxy_protocol(ngx_stream_session_t *s) ngx_log_debug0(NGX_LOG_DEBUG_STREAM, c->log, 0, "stream proxy send PROXY protocol header"); + buf = stack_buf; + buf_size = NGX_PROXY_PROTOCOL_V2_MAX_HEADER; + tlvs = NULL; + + if (pscf->proxy_protocol_version == 2 + && pscf->proxy_protocol_tlvs != NULL) + { + tlvs = ngx_stream_proxy_build_tlvs(s, pscf->proxy_protocol_tlvs, + &tlv_size); + if (tlvs == NULL) { + ngx_stream_proxy_finalize(s, NGX_STREAM_INTERNAL_SERVER_ERROR); + return NGX_ERROR; + } + + buf_size += tlv_size; + buf = ngx_pnalloc(c->pool, buf_size); + if (buf == NULL) { + ngx_stream_proxy_finalize(s, NGX_STREAM_INTERNAL_SERVER_ERROR); + return NGX_ERROR; + } + } + if (pscf->proxy_protocol_version == 2) { - p = ngx_proxy_protocol_v2_write(c, buf, - buf + NGX_PROXY_PROTOCOL_V2_MAX_HEADER); + p = ngx_proxy_protocol_v2_write_tlvs(c, buf, buf + buf_size, tlvs); } else { p = ngx_proxy_protocol_write(c, buf, buf + NGX_PROXY_PROTOCOL_V1_MAX_HEADER); @@ -2393,6 +2488,7 @@ ngx_stream_proxy_create_srv_conf(ngx_conf_t *cf) conf->next_upstream = NGX_CONF_UNSET; conf->proxy_protocol = NGX_CONF_UNSET; conf->proxy_protocol_version = NGX_CONF_UNSET_UINT; + conf->proxy_protocol_tlvs = NGX_CONF_UNSET_PTR; conf->local = NGX_CONF_UNSET_PTR; conf->socket_keepalive = NGX_CONF_UNSET; conf->half_close = NGX_CONF_UNSET; @@ -2454,6 +2550,9 @@ ngx_stream_proxy_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_uint_value(conf->proxy_protocol_version, prev->proxy_protocol_version, 1); + ngx_conf_merge_ptr_value(conf->proxy_protocol_tlvs, + prev->proxy_protocol_tlvs, NULL); + ngx_conf_merge_ptr_value(conf->local, prev->local, NULL); ngx_conf_merge_value(conf->socket_keepalive, @@ -2864,3 +2963,71 @@ ngx_stream_proxy_bind(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) return NGX_CONF_OK; } + + +static char * +ngx_stream_proxy_protocol_tlv(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_stream_proxy_srv_conf_t *pscf = conf; + + ngx_int_t type; + ngx_str_t *value; + ngx_stream_compile_complex_value_t ccv; + ngx_stream_proxy_protocol_tlv_t *tlv; + + value = cf->args->elts; + + if (value[1].len > 2 + && value[1].data[0] == '0' + && (value[1].data[1] == 'x' || value[1].data[1] == 'X')) + { + type = ngx_hextoi(value[1].data + 2, value[1].len - 2); + } else { + type = ngx_atoi(value[1].data, value[1].len); + } + + if (type == NGX_ERROR || type < 0 || type > 255) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid PROXY protocol TLV type \"%V\"", + &value[1]); + return NGX_CONF_ERROR; + } + + if (type == 0x03) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "TLV type 0x03 is reserved for CRC32c checksum, " + "use the \"proxy_protocol_crc32c\" directive"); + return NGX_CONF_ERROR; + } + + if (pscf->proxy_protocol_tlvs == NGX_CONF_UNSET_PTR) { + pscf->proxy_protocol_tlvs = ngx_array_create(cf->pool, 4, + sizeof(ngx_stream_proxy_protocol_tlv_t)); + if (pscf->proxy_protocol_tlvs == NULL) { + return NGX_CONF_ERROR; + } + } + + tlv = ngx_array_push(pscf->proxy_protocol_tlvs); + if (tlv == NULL) { + return NGX_CONF_ERROR; + } + + tlv->type = (ngx_uint_t) type; + + tlv->value = ngx_palloc(cf->pool, sizeof(ngx_stream_complex_value_t)); + if (tlv->value == NULL) { + return NGX_CONF_ERROR; + } + + ngx_memzero(&ccv, sizeof(ngx_stream_compile_complex_value_t)); + ccv.cf = cf; + ccv.value = &value[2]; + ccv.complex_value = tlv->value; + + if (ngx_stream_compile_complex_value(&ccv) != NGX_OK) { + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; +}