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