From: William Dauchy <[email protected]>

On backend connections, quic_rx_pkt_parse() accepts long header packets
with an unsupported version and Version Negotiation packets, as these
checks only apply to listeners. Such packets keep the
QUIC_PACKET_TYPE_UNKNOWN type, which maps to no encryption level, and hit
BUG_ON(!qel || !pktns) in qc_enc_level_alloc() from qc_try_rm_hp(), or a
NULL dereference without DEBUG_STRICT.

No malicious peer is required: a server which does not support the
version selected by haproxy responds with a Version Negotiation packet
(RFC 9000 6.1). A malicious server or an on-path attacker may also send a
packet with an unsupported version.

Drop these packets on backend connections, as required by RFC 9000 5.2.1.
Version Negotiation itself is still not handled (RFC 9000 6.2): the
connection attempt fails later, as with an unresponsive server.

This should be backported up to 3.3, where it was introduced by 43d88a44f
("MINOR: quic-be: Datagrams and packet parsing support").

Signed-off-by: William Dauchy <[email protected]>
---
 reg-tests/quic/unknown_version_backend.py  | 52 ++++++++++++++++++++++
 reg-tests/quic/unknown_version_backend.vtc | 49 ++++++++++++++++++++
 src/quic_rx.c                              |  6 +++
 3 files changed, 107 insertions(+)
 create mode 100644 reg-tests/quic/unknown_version_backend.py
 create mode 100644 reg-tests/quic/unknown_version_backend.vtc

diff --git a/reg-tests/quic/unknown_version_backend.py 
b/reg-tests/quic/unknown_version_backend.py
new file mode 100644
index 000000000..8b5970f91
--- /dev/null
+++ b/reg-tests/quic/unknown_version_backend.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+"""Fake QUIC backend answering haproxy's Initial with a long header packet
+using an unsupported version. A single datagram is enough to reach
+quic_rx_pkt_parse() on the backend side of the connection: it must be
+dropped instead of being parsed with a QUIC_PACKET_TYPE_UNKNOWN type, which
+resolves to a NULL encryption level.
+"""
+import argparse
+import socket
+import sys
+
+
+def main():
+    ap = argparse.ArgumentParser()
+    ap.add_argument("--port", type=int, required=True)
+    ap.add_argument("--timeout", type=float, default=10.0)
+    args = ap.parse_args()
+
+    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    s.settimeout(args.timeout)
+    s.bind(("127.0.0.1", args.port))
+    print("READY", flush=True)
+
+    try:
+        data, peer = s.recvfrom(65535)
+    except socket.timeout:
+        print("no Initial packet received", flush=True)
+        return 1
+
+    if not data[0] & 0x80:
+        print("not a long header packet", flush=True)
+        return 1
+
+    dcid_len = data[5]
+    scid_off = 6 + dcid_len
+    scid_len = data[scid_off]
+    hap_scid = data[scid_off + 1:scid_off + 1 + scid_len]
+
+    payload = b"\x00" * 40
+    pkt = (bytes([0xC0]) + (0x1A2A3A4A).to_bytes(4, "big") +
+           bytes([len(hap_scid)]) + hap_scid + b"\x00" +
+           bytes([0x40 | (len(payload) >> 8), len(payload) & 0xFF]) + payload)
+    s.sendto(pkt, peer)
+    s.close()
+    print("sent %d bytes long header packet with unsupported version" % 
len(pkt), flush=True)
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/reg-tests/quic/unknown_version_backend.vtc 
b/reg-tests/quic/unknown_version_backend.vtc
new file mode 100644
index 000000000..697322c22
--- /dev/null
+++ b/reg-tests/quic/unknown_version_backend.vtc
@@ -0,0 +1,49 @@
+varnishtest "QUIC backend must drop packets with an unsupported version"
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+#REGTEST_TYPE=bug
+
+feature ignore_unknown_macro
+feature cmd "python3 --version"
+
+# QUIC backend connections are not supported with USE_QUIC_OPENSSL_COMPAT.
+feature cmd "$HAPROXY_PROGRAM -cc 'feature(QUIC) && 
!feature(QUIC_OPENSSL_COMPAT) && !feature(OPENSSL_WOLFSSL)'"
+
+# Reserve the UDP port used by the fake QUIC backend.
+server s_backend {} -start
+server s_backend -break
+
+process p1 "python3 ${testdir}/unknown_version_backend.py --port 
${s_backend_port}" -start
+process p1 -expect-text 0 0 "READY"
+
+haproxy h1 -conf {
+       global
+               expose-experimental-directives
+
+       defaults
+               mode http
+               timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
+               timeout client  "${HAPROXY_TEST_TIMEOUT-5s}"
+               timeout server  "${HAPROXY_TEST_TIMEOUT-5s}"
+
+       frontend fe
+               bind "fd@${fe}"
+               default_backend be
+
+       backend be
+               # Fail on the first attempt: do not depend on ICMP port 
unreachable
+               # to make connection retries fail fast once the fake backend 
exited.
+               retries 0
+               server qsrv [email protected]:${s_backend_port} ssl verify none 
alpn h3
+} -start
+
+# The fake backend answers the Initial with a long header packet using an
+# unsupported version: the request must fail, but haproxy used to abort on
+# BUG_ON(!qel || !pktns) in qc_enc_level_alloc() while parsing it.
+client c1 -connect ${h1_fe_sock} {
+       txreq
+       rxresp
+       expect resp.status == 503
+} -run
+
+process p1 -wait
diff --git a/src/quic_rx.c b/src/quic_rx.c
index 74e3f79a5..fb54d7867 100644
--- a/src/quic_rx.c
+++ b/src/quic_rx.c
@@ -2074,6 +2074,12 @@ static int quic_rx_pkt_parse(struct quic_conn *qc, 
struct quic_rx_packet *pkt,
                        goto drop_silent;
                }
 
+               if (!l && (!pkt->version || !pkt->version->num)) {
+                       TRACE_PROTO("RX unsupported version or VN packet on 
backend",
+                                   QUIC_EV_CONN_LPKT, NULL, NULL, NULL, 
pkt->version);
+                       goto drop;
+               }
+
                if (pkt->type == QUIC_PACKET_TYPE_INITIAL) {
                        /* For Initial packets, and for servers (QUIC clients 
connections),
                         * there is no Initial connection IDs storage.
-- 
2.56.0



Reply via email to