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.
This commit is contained in:
Vadim Zhestikov 2026-05-12 09:28:01 -07:00
parent 47d241f141
commit ce5e7b26bb

View file

@ -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;
}