]> git.kaiwu.me - haproxy.git/commitdiff
WIP: BUG/MINOR: connection: fix the type of the PROXY protocol TLV fetches
authorFrederic Lecaille <flecaille@haproxy.com>
Mon, 14 Sep 2026 08:25:43 +0000 (10:25 +0200)
committerFrederic Lecaille <flecaille@haproxy.com>
Wed, 16 Sep 2026 07:18:10 +0000 (09:18 +0200)
A string sample cannot contain a \0 byte, but fc_pp_tlv() returned the
raw TLV value as a string, so a sender could put a \0 in it. Such a
value was then matched by "-m str" on its prefix: "public\0x" matched
"public". This also affected fc_pp_authority and fc_pp_unique_id which
rely on it.

A TLV value is a raw byte sequence, so fc_pp_tlv now returns a binary
sample: its bytes are kept, \0 included, for hex, bytes or relaying
with set-proxy-v2-tlv-fmt, and the usual binary to string cast applies
for text consumers. fc_pp_authority and fc_pp_unique_id are text by
nature and still return a string, but fail when the value holds a \0.

Note that an ACL on fc_pp_tlv now needs an explicit matching method
("-m str") since a binary sample has no default one. Such a rule
prevents haproxy from starting:

   http-request deny if { fc_pp_tlv(ALPN) h2 }

must now be written

   http-request deny if { fc_pp_tlv(ALPN) -m str h2 }

The documentation of these three fetches is updated accordingly, and a
reg-test is added: it checks the three fetches with a raw PROXY v2
header holding "public\0x" values and a binary SSL TLV full of \0
bytes. A second reg-test, only run when OpenSSL is available, checks
that the SSL TLV sent by send-proxy-v2-ssl is returned as is.

This must be backported to all stable branches.

doc/configuration.txt
reg-tests/connection/proxy_protocol_ssl_tlv.vtc [new file with mode: 0644]
reg-tests/connection/proxy_protocol_tlv_embedded_nul.vtc [new file with mode: 0644]
src/connection.c

index b5615ba49ca640a22c6a2c0e71f92174e69be1d0..bc6361bffb646ce6da9621f4eb1821cedfcb5be0 100644 (file)
@@ -19906,7 +19906,7 @@ set-proxy-v2-tlv-fmt(<id>) <fmt>
   Example:
   server srv1 192.168.1.1:80 send-proxy-v2 set-proxy-v2-tlv-fmt(0x20) %[fc_pp_tlv(0x20)]
 
-  In this case, we fetch the TLV with the type 0x20 as a string and set as the value
+  In this case, we fetch the raw TLV with the type 0x20 and set it as the value
   of a newly created TLV that also has the type 0x20.
 
 proxy-v2-options <option>[,<option>]*
@@ -24789,7 +24789,7 @@ fc_http_major                                      integer
 fc_lost                                            integer
 fc_nb_streams                                      integer
 fc_pp_authority                                    string
-fc_pp_tlv(<id>)                                    string
+fc_pp_tlv(<id>)                                    binary
 fc_pp_unique_id                                    string
 fc_rcvd_proxy                                      boolean
 fc_reordering                                      integer
@@ -25367,10 +25367,13 @@ fc_nb_streams : integer
 
 fc_pp_authority : string
   Returns the first authority TLV sent by the client in the PROXY protocol
-  header, if any.
+  header, if any. If the value contains a NUL byte, it is considered malformed
+  and no data is returned.
 
-fc_pp_tlv(<id>) : string
-  Returns the TLV value for the given TLV ID. The ID must either be a numeric
+fc_pp_tlv(<id>) : binary
+  Returns the raw TLV value for the given TLV ID, which may contain NUL
+  bytes. Use "-m str" or a converter to match it as a string, in which case
+  the value ends at the first NUL byte. The ID must either be a numeric
   value between 0 and 255 or one of the following supported symbolic names
   that correspond to the TLV constant suffixes in the PPv2 spec:
   "ALPN": PP2_TYPE_ALPN, "AUTHORITY": PP2_TYPE_AUTHORITY,
@@ -25394,7 +25397,8 @@ fc_pp_tlv(<id>) : string
 
 fc_pp_unique_id : string
   Returns the first unique ID TLV sent by the client in the PROXY protocol
-  header, if any.
+  header, if any. If the value contains a NUL byte, it is considered malformed
+  and no data is returned.
 
 fc_rcvd_proxy : boolean
   Returns true if the client initiated the connection with a PROXY protocol
diff --git a/reg-tests/connection/proxy_protocol_ssl_tlv.vtc b/reg-tests/connection/proxy_protocol_ssl_tlv.vtc
new file mode 100644 (file)
index 0000000..99df8b0
--- /dev/null
@@ -0,0 +1,38 @@
+varnishtest "fc_pp_tlv on the SSL TLV sent by send-proxy-v2-ssl"
+feature cmd "$HAPROXY_PROGRAM -cc 'feature(OPENSSL)'"
+feature ignore_unknown_macro
+
+# The SSL TLV (0x20) sent by send-proxy-v2-ssl is binary: its <client> and
+# <verify> fields hold \0 bytes when the client connection is not TLS.
+# fc_pp_tlv(0x20) must return all its bytes, so that it can be converted
+# with hex or relayed.
+
+haproxy h1 -conf {
+    global
+    .if feature(THREAD)
+        thread-groups 1
+    .endif
+
+    defaults
+        mode http
+        timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
+        timeout client  "${HAPROXY_TEST_TIMEOUT-5s}"
+        timeout server  "${HAPROXY_TEST_TIMEOUT-5s}"
+
+    listen sender
+        bind "fd@${feS}"
+        server r ${h1_feR_addr}:${h1_feR_port} send-proxy-v2-ssl set-proxy-v2-tlv-fmt(0xE1) %[str("text")]
+
+    listen receiver
+        bind "fd@${feR}" accept-proxy
+        http-request return status 200 hdr ssl-tlv "%[fc_pp_tlv(0x20),hex]" hdr custom-tlv "%[fc_pp_tlv(0xE1)]"
+} -start
+
+client c1 -connect ${h1_feS_sock} {
+    txreq -url "/"
+    rxresp
+    expect resp.status == 200
+    expect resp.http.custom-tlv == "text"
+    # <client> = 0x00 (no TLS), <verify> = 0x00000000
+    expect resp.http.ssl-tlv == "0000000000"
+} -run
diff --git a/reg-tests/connection/proxy_protocol_tlv_embedded_nul.vtc b/reg-tests/connection/proxy_protocol_tlv_embedded_nul.vtc
new file mode 100644 (file)
index 0000000..7c16f0d
--- /dev/null
@@ -0,0 +1,64 @@
+varnishtest "PROXY v2 TLV fetches and values holding a \0 byte"
+feature ignore_unknown_macro
+
+# A string sample cannot contain a \0 byte. TLV values are copied as is
+# from the PROXY v2 header, so the sender can put a \0 in them. The text
+# fetches fc_pp_authority and fc_pp_unique_id must return nothing for such
+# a value instead of being matched by "-m str" on its prefix. fc_pp_tlv
+# returns the raw bytes and must keep the \0: the SSL TLV (0x20) sent by
+# send-proxy-v2-ssl holds \0 bytes in its <client> and <verify> fields, and
+# must still be readable.
+
+haproxy h1 -conf {
+    global
+    .if feature(THREAD)
+        thread-groups 1
+    .endif
+
+    defaults
+        mode http
+        timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
+        timeout client  "${HAPROXY_TEST_TIMEOUT-5s}"
+        timeout server  "${HAPROXY_TEST_TIMEOUT-5s}"
+
+    frontend fe1
+        bind "fd@${fe1}" accept-proxy
+        http-request set-var-fmt(txn.auth) match if { fc_pp_authority -m str public }
+        http-request set-var-fmt(txn.uid) match  if { fc_pp_unique_id -m str public }
+        http-request return status 200 hdr authority %[var(txn.auth,nomatch)] hdr unique-id %[var(txn.uid,nomatch)] hdr tlv %[fc_pp_tlv(0xE1),hex] hdr ssl-tlv %[fc_pp_tlv(0x20),hex]
+} -start
+
+# Header layout:
+#   0d 0a 0d 0a 00 0d 0a 51 55 49 54 0a    PROXY v2 signature
+#   21 11                                  version 2, PROXY, TCP4
+#   00 LL                                  length
+#   7f000001 7f000001 002a 0539            127.0.0.1:42 -> 127.0.0.1:1337
+#   02 00 LL <value>                       PP2_TYPE_AUTHORITY
+#   05 00 LL <value>                       PP2_TYPE_UNIQUE_ID
+#   e1 00 LL <value>                       custom TLV
+#   20 00 05 00 00000000                   PP2_TYPE_SSL: <client> = 0, <verify> = 0
+
+# all values "public", plus the SSL TLV
+client c1 -connect ${h1_fe1_sock} {
+    sendhex "0d0a0d0a000d0a515549540a 2111 002f 7f0000017f000001002a0539 020006 7075626c6963 050006 7075626c6963 e10006 7075626c6963 200005 0000000000"
+    txreq -url "/"
+    rxresp
+    expect resp.status == 200
+    expect resp.http.authority == "match"
+    expect resp.http.unique-id == "match"
+    expect resp.http.tlv == "7075626C6963"
+    # a binary TLV keeps its \0 bytes
+    expect resp.http.ssl-tlv == "0000000000"
+} -run
+
+# all values "public\0x"
+client c2 -connect ${h1_fe1_sock} {
+    sendhex "0d0a0d0a000d0a515549540a 2111 002d 7f0000017f000001002a0539 020008 7075626c69630078 050008 7075626c69630078 e10008 7075626c69630078"
+    txreq -url "/"
+    rxresp
+    expect resp.status == 200
+    expect resp.http.authority == "nomatch"
+    expect resp.http.unique-id == "nomatch"
+    # a raw TLV keeps its \0 byte
+    expect resp.http.tlv == "7075626C69630078"
+} -run
index 4b4340724fbd8b2a0cdba438aaab1cbf4e2f9b7a..05040bfeb941222e61ced2ff1981de6bec114fe9 100644 (file)
@@ -2741,7 +2741,8 @@ int smp_fetch_fc_pp_tlv(const struct arg *args, struct sample *smp, const char *
        list_for_each_entry_from(conn_tlv, &conn->tlv_list, list) {
                if (conn_tlv->type == idx) {
                        smp->flags |= SMP_F_NOT_LAST;
-                       smp->data.type = SMP_T_STR;
+                       /* a TLV value is a raw byte sequence */
+                       smp->data.type = SMP_T_BIN;
                        smp->data.u.str.area = conn_tlv->value;
                        smp->data.u.str.data = conn_tlv->len;
                        smp->ctx.p = conn_tlv;
@@ -2755,28 +2756,37 @@ int smp_fetch_fc_pp_tlv(const struct arg *args, struct sample *smp, const char *
        return 0;
 }
 
-/* fetch the authority TLV from a PROXY protocol header */
-int smp_fetch_fc_pp_authority(const struct arg *args, struct sample *smp, const char *kw, void *private)
+/* Returns the first TLV of type <type> as a string. Fails if the value
+ * holds a NUL byte, since a string sample cannot contain one.
+ */
+static int smp_fetch_fc_pp_tlv_str(int type, struct sample *smp, const char *kw, void *private)
 {
        struct arg tlv_arg;
        int ret;
 
-       set_tlv_arg(PP2_TYPE_AUTHORITY, &tlv_arg);
+       set_tlv_arg(type, &tlv_arg);
        ret = smp_fetch_fc_pp_tlv(&tlv_arg, smp, kw, private);
-       smp->flags &= ~SMP_F_NOT_LAST; // return only the first authority
-       return ret;
+       smp->flags &= ~SMP_F_NOT_LAST; // return only the first one
+       if (!ret)
+               return 0;
+
+       if (memchr(smp->data.u.str.area, 0, smp->data.u.str.data))
+               return 0;
+
+       smp->data.type = SMP_T_STR;
+       return 1;
+}
+
+/* fetch the authority TLV from a PROXY protocol header */
+int smp_fetch_fc_pp_authority(const struct arg *args, struct sample *smp, const char *kw, void *private)
+{
+       return smp_fetch_fc_pp_tlv_str(PP2_TYPE_AUTHORITY, smp, kw, private);
 }
 
 /* fetch the unique ID TLV from a PROXY protocol header */
 int smp_fetch_fc_pp_unique_id(const struct arg *args, struct sample *smp, const char *kw, void *private)
 {
-       struct arg tlv_arg;
-       int ret;
-
-       set_tlv_arg(PP2_TYPE_UNIQUE_ID, &tlv_arg);
-       ret = smp_fetch_fc_pp_tlv(&tlv_arg, smp, kw, private);
-       smp->flags &= ~SMP_F_NOT_LAST; // return only the first unique ID
-       return ret;
+       return smp_fetch_fc_pp_tlv_str(PP2_TYPE_UNIQUE_ID, smp, kw, private);
 }
 
 /* fetch the error code of a connection */
@@ -2914,7 +2924,7 @@ static struct sample_fetch_kw_list sample_fetch_keywords = {ILH, {
        { "fc_nb_streams", smp_fetch_fc_nb_streams, 0, NULL, SMP_T_SINT, SMP_USE_L4CLI },
        { "fc_pp_authority", smp_fetch_fc_pp_authority, 0, NULL, SMP_T_STR, SMP_USE_L4CLI },
        { "fc_pp_unique_id", smp_fetch_fc_pp_unique_id, 0, NULL, SMP_T_STR, SMP_USE_L4CLI },
-       { "fc_pp_tlv", smp_fetch_fc_pp_tlv, ARG1(1, STR), smp_check_tlv_type, SMP_T_STR, SMP_USE_L5CLI },
+       { "fc_pp_tlv", smp_fetch_fc_pp_tlv, ARG1(1, STR), smp_check_tlv_type, SMP_T_BIN, SMP_USE_L5CLI },
        { "fc_settings_streams_limit", smp_fetch_fc_streams_limit, 0, NULL, SMP_T_SINT, SMP_USE_L5CLI },
        { /* END */ },
 }};