MEDIUM: quic: send version negotiation packet on unknown version

If the client announced a QUIC version not supported by haproxy, emit a
Version Negotiation Packet, according to RFC9000 6. Version Negotiation.

This is required to be able to use the framework for QUIC interop
testing from https://github.com/marten-seemann/quic-interop-runner. The
simulator checks that the server is available by sending packets to
force the emission of a Version Negotiation Packet.
diff --git a/src/xprt_quic.c b/src/xprt_quic.c
index 35afeaa..ca1e44f 100644
--- a/src/xprt_quic.c
+++ b/src/xprt_quic.c
@@ -52,6 +52,14 @@
 #include <haproxy/trace.h>
 #include <haproxy/xprt_quic.h>
 
+/* list of supported QUIC versions by this implementation */
+static int quic_supported_version[] = {
+	0x00000001,
+
+	/* placeholder, do not add entry after this */
+	0x0
+};
+
 /* This is the values of some QUIC transport parameters when absent.
  * Should be used to initialize any transport parameters (local or remote)
  * before updating them with customized values.
@@ -3136,8 +3144,6 @@
 	if (!quic_read_uint32(&pkt->version, (const unsigned char **)buf, end))
 		return 0;
 
-	if (!pkt->version) { /* XXX TO DO XXX Version negotiation packet */ };
-
 	/* Destination Connection ID Length */
 	dcid_len = *(*buf)++;
 	/* We want to be sure we can read <dcid_len> bytes and one more for <scid_len> value */
@@ -3313,6 +3319,24 @@
 	}
 }
 
+/*
+ * Check if the QUIC version in packet <pkt> is supported. Returns a boolean.
+ */
+static inline int qc_pkt_is_supported_version(struct quic_rx_packet *pkt)
+{
+	int j = 0, version;
+
+	do {
+		version = quic_supported_version[j];
+		if (version == pkt->version)
+			return 1;
+
+		version = quic_supported_version[++j];
+	} while(version);
+
+	return 0;
+}
+
 __attribute__((unused))
 static ssize_t qc_srv_pkt_rcv(unsigned char **buf, const unsigned char *end,
                               struct quic_rx_packet *pkt,
@@ -3350,6 +3374,12 @@
 
 		if (!quic_packet_read_long_header(buf, end, pkt))
 			goto err;
+
+		/* unsupported QUIC version */
+		if (!qc_pkt_is_supported_version(pkt)) {
+			TRACE_PROTO("Null QUIC version, packet dropped", QUIC_EV_CONN_LPKT);
+			goto err;
+		}
 
 		/* For Initial packets, and for servers (QUIC clients connections),
 		 * there is no Initial connection IDs storage.
@@ -3467,6 +3497,60 @@
 	return -1;
 }
 
+/*
+ * Send a Version Negotiation packet on response to <pkt> on socket <fd> to
+ * address <addr>.
+ * Implementation of RFC9000 6. Version Negotiation
+ *
+ * TODO implement a rate-limiting sending of Version Negotiation packets
+ *
+ * Returns 0 on success else non-zero
+ */
+static int qc_send_version_negotiation(int fd, struct sockaddr_storage *addr,
+                                       struct quic_rx_packet *pkt)
+{
+	char buf[256];
+	int i = 0, j, version;
+	const socklen_t addrlen = get_addr_len(addr);
+
+	/*
+	 * header form
+	 * long header, fixed bit to 0 for Version Negotiation
+	 */
+	buf[i++] = '\x80';
+
+	/* null version for Version Negotiation */
+	buf[i++] = '\x00';
+	buf[i++] = '\x00';
+	buf[i++] = '\x00';
+	buf[i++] = '\x00';
+
+	/* source connection id */
+	buf[i++] = pkt->scid.len;
+	memcpy(buf, pkt->scid.data, pkt->scid.len);
+	i += pkt->scid.len;
+
+	/* destination connection id */
+	buf[i++] = pkt->dcid.len;
+	memcpy(buf, pkt->dcid.data, pkt->dcid.len);
+	i += pkt->dcid.len;
+
+	/* supported version */
+	j = 0;
+	do {
+		version = htonl(quic_supported_version[j]);
+		memcpy(&buf[i], &version, sizeof(version));
+		i += sizeof(version);
+
+		version = quic_supported_version[++j];
+	} while (version);
+
+	if (sendto(fd, buf, i, 0, (struct sockaddr *)addr, addrlen) < 0)
+		return 1;
+
+	return 0;
+}
+
 static ssize_t qc_lstnr_pkt_rcv(unsigned char **buf, const unsigned char *end,
                                 struct quic_rx_packet *pkt,
                                 struct quic_dgram_ctx *dgram_ctx,
@@ -3508,6 +3592,26 @@
 			goto err;
 		}
 
+		/* RFC9000 6. Version Negotiation */
+		if (!qc_pkt_is_supported_version(pkt)) {
+			/* do not send Version Negotiation in response to a
+			 * Version Negotiation packet.
+			 */
+			if (!pkt->version) {
+				TRACE_PROTO("Null QUIC version, packet dropped", QUIC_EV_CONN_LPKT);
+				goto err;
+			}
+
+			 /* unsupported version, send Negotiation packet */
+			if (qc_send_version_negotiation(l->rx.fd, saddr, pkt)) {
+				TRACE_PROTO("Error on Version Negotiation sending", QUIC_EV_CONN_LPKT);
+				goto err;
+			}
+
+			TRACE_PROTO("Unsupported QUIC version, send Version Negotiation packet", QUIC_EV_CONN_LPKT);
+			goto out;
+		}
+
 		dcid_len = pkt->dcid.len;
 		/* For Initial packets, and for servers (QUIC clients connections),
 		 * there is no Initial connection IDs storage.
@@ -3721,6 +3825,7 @@
 	if (conn_ctx && HA_ATOMIC_LOAD(&conn_ctx->conn->ctx))
 		tasklet_wakeup(conn_ctx->wait_event.tasklet);
 
+ out:
 	TRACE_LEAVE(QUIC_EV_CONN_LPKT, qc->conn, pkt);
 
 	return pkt->len;