1/***************************************************************************
2 * _ _ ____ _
3 * Project ___| | | | _ \| |
4 * / __| | | | |_) | |
5 * | (__| |_| | _ <| |___
6 * \___|\___/|_| \_\_____|
7 *
8 * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
9 *
10 * This software is licensed as described in the file COPYING, which
11 * you should have received as part of this distribution. The terms
12 * are also available at https://curl.se/docs/copyright.html.
13 *
14 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
15 * copies of the Software, and permit persons to whom the Software is
16 * furnished to do so, under the terms of the COPYING file.
17 *
18 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
19 * KIND, either express or implied.
20 *
21 * SPDX-License-Identifier: curl
22 *
23 ***************************************************************************/
24
25/* WIP, experimental: use recvmmsg() on linux
26 * we have no configure check, yet
27 * and also it is only available for _GNU_SOURCE, which
28 * we do not use otherwise.
29#define HAVE_SENDMMSG
30 */
31#if defined(HAVE_SENDMMSG)
32#define _GNU_SOURCE
33#include <sys/socket.h>
34#undef _GNU_SOURCE
35#endif
36
37#include "curl_setup.h"
38
39#ifdef HAVE_FCNTL_H
40#include <fcntl.h>
41#endif
42#include "urldata.h"
43#include "bufq.h"
44#include "dynbuf.h"
45#include "cfilters.h"
46#include "curl_trc.h"
47#include "curl_msh3.h"
48#include "curl_ngtcp2.h"
49#include "curl_quiche.h"
50#include "rand.h"
51#include "vquic.h"
52#include "vquic_int.h"
53#include "strerror.h"
54
55/* The last 3 #include files should be in this order */
56#include "curl_printf.h"
57#include "curl_memory.h"
58#include "memdebug.h"
59
60
61#ifdef ENABLE_QUIC
62
63#ifdef O_BINARY
64#define QLOGMODE O_WRONLY|O_CREAT|O_BINARY
65#else
66#define QLOGMODE O_WRONLY|O_CREAT
67#endif
68
69#define NW_CHUNK_SIZE (64 * 1024)
70#define NW_SEND_CHUNKS 2
71
72
73void Curl_quic_ver(char *p, size_t len)
74{
75#if defined(USE_NGTCP2) && defined(USE_NGHTTP3)
76 Curl_ngtcp2_ver(p, len);
77#elif defined(USE_QUICHE)
78 Curl_quiche_ver(p, len);
79#elif defined(USE_MSH3)
80 Curl_msh3_ver(p, len);
81#endif
82}
83
84CURLcode vquic_ctx_init(struct cf_quic_ctx *qctx)
85{
86 Curl_bufq_init2(&qctx->sendbuf, NW_CHUNK_SIZE, NW_SEND_CHUNKS,
87 BUFQ_OPT_SOFT_LIMIT);
88#if defined(__linux__) && defined(UDP_SEGMENT) && defined(HAVE_SENDMSG)
89 qctx->no_gso = FALSE;
90#else
91 qctx->no_gso = TRUE;
92#endif
93#ifdef DEBUGBUILD
94 {
95 char *p = getenv("CURL_DBG_QUIC_WBLOCK");
96 if(p) {
97 long l = strtol(p, NULL, 10);
98 if(l >= 0 && l <= 100)
99 qctx->wblock_percent = (int)l;
100 }
101 }
102#endif
103
104 return CURLE_OK;
105}
106
107void vquic_ctx_free(struct cf_quic_ctx *qctx)
108{
109 Curl_bufq_free(&qctx->sendbuf);
110}
111
112static CURLcode send_packet_no_gso(struct Curl_cfilter *cf,
113 struct Curl_easy *data,
114 struct cf_quic_ctx *qctx,
115 const uint8_t *pkt, size_t pktlen,
116 size_t gsolen, size_t *psent);
117
118static CURLcode do_sendmsg(struct Curl_cfilter *cf,
119 struct Curl_easy *data,
120 struct cf_quic_ctx *qctx,
121 const uint8_t *pkt, size_t pktlen, size_t gsolen,
122 size_t *psent)
123{
124#ifdef HAVE_SENDMSG
125 struct iovec msg_iov;
126 struct msghdr msg = {0};
127 ssize_t sent;
128#if defined(__linux__) && defined(UDP_SEGMENT)
129 uint8_t msg_ctrl[32];
130 struct cmsghdr *cm;
131#endif
132
133 *psent = 0;
134 msg_iov.iov_base = (uint8_t *)pkt;
135 msg_iov.iov_len = pktlen;
136 msg.msg_iov = &msg_iov;
137 msg.msg_iovlen = 1;
138
139#if defined(__linux__) && defined(UDP_SEGMENT)
140 if(pktlen > gsolen) {
141 /* Only set this, when we need it. macOS, for example,
142 * does not seem to like a msg_control of length 0. */
143 msg.msg_control = msg_ctrl;
144 assert(sizeof(msg_ctrl) >= CMSG_SPACE(sizeof(uint16_t)));
145 msg.msg_controllen = CMSG_SPACE(sizeof(uint16_t));
146 cm = CMSG_FIRSTHDR(&msg);
147 cm->cmsg_level = SOL_UDP;
148 cm->cmsg_type = UDP_SEGMENT;
149 cm->cmsg_len = CMSG_LEN(sizeof(uint16_t));
150 *(uint16_t *)(void *)CMSG_DATA(cm) = gsolen & 0xffff;
151 }
152#endif
153
154
155 while((sent = sendmsg(qctx->sockfd, &msg, 0)) == -1 && SOCKERRNO == EINTR)
156 ;
157
158 if(sent == -1) {
159 switch(SOCKERRNO) {
160 case EAGAIN:
161#if EAGAIN != EWOULDBLOCK
162 case EWOULDBLOCK:
163#endif
164 return CURLE_AGAIN;
165 case EMSGSIZE:
166 /* UDP datagram is too large; caused by PMTUD. Just let it be lost. */
167 break;
168 case EIO:
169 if(pktlen > gsolen) {
170 /* GSO failure */
171 failf(data, "sendmsg() returned %zd (errno %d); disable GSO", sent,
172 SOCKERRNO);
173 qctx->no_gso = TRUE;
174 return send_packet_no_gso(cf, data, qctx, pkt, pktlen, gsolen, psent);
175 }
176 /* FALLTHROUGH */
177 default:
178 failf(data, "sendmsg() returned %zd (errno %d)", sent, SOCKERRNO);
179 return CURLE_SEND_ERROR;
180 }
181 }
182 else {
183 assert(pktlen == (size_t)sent);
184 }
185#else
186 ssize_t sent;
187 (void)gsolen;
188
189 *psent = 0;
190
191 while((sent = send(qctx->sockfd,
192 (const char *)pkt, (SEND_TYPE_ARG3)pktlen, 0)) == -1 &&
193 SOCKERRNO == EINTR)
194 ;
195
196 if(sent == -1) {
197 if(SOCKERRNO == EAGAIN || SOCKERRNO == EWOULDBLOCK) {
198 return CURLE_AGAIN;
199 }
200 else {
201 failf(data, "send() returned %zd (errno %d)", sent, SOCKERRNO);
202 if(SOCKERRNO != EMSGSIZE) {
203 return CURLE_SEND_ERROR;
204 }
205 /* UDP datagram is too large; caused by PMTUD. Just let it be
206 lost. */
207 }
208 }
209#endif
210 (void)cf;
211 *psent = pktlen;
212
213 return CURLE_OK;
214}
215
216static CURLcode send_packet_no_gso(struct Curl_cfilter *cf,
217 struct Curl_easy *data,
218 struct cf_quic_ctx *qctx,
219 const uint8_t *pkt, size_t pktlen,
220 size_t gsolen, size_t *psent)
221{
222 const uint8_t *p, *end = pkt + pktlen;
223 size_t sent;
224
225 *psent = 0;
226
227 for(p = pkt; p < end; p += gsolen) {
228 size_t len = CURLMIN(gsolen, (size_t)(end - p));
229 CURLcode curlcode = do_sendmsg(cf, data, qctx, p, len, len, &sent);
230 if(curlcode != CURLE_OK) {
231 return curlcode;
232 }
233 *psent += sent;
234 }
235
236 return CURLE_OK;
237}
238
239static CURLcode vquic_send_packets(struct Curl_cfilter *cf,
240 struct Curl_easy *data,
241 struct cf_quic_ctx *qctx,
242 const uint8_t *pkt, size_t pktlen,
243 size_t gsolen, size_t *psent)
244{
245#ifdef DEBUGBUILD
246 /* simulate network blocking/partial writes */
247 if(qctx->wblock_percent > 0) {
248 unsigned char c;
249 Curl_rand(data, &c, 1);
250 if(c >= ((100-qctx->wblock_percent)*256/100)) {
251 CURL_TRC_CF(data, cf, "vquic_flush() simulate EWOULDBLOCK");
252 return CURLE_AGAIN;
253 }
254 }
255#endif
256 if(qctx->no_gso && pktlen > gsolen) {
257 return send_packet_no_gso(cf, data, qctx, pkt, pktlen, gsolen, psent);
258 }
259
260 return do_sendmsg(cf, data, qctx, pkt, pktlen, gsolen, psent);
261}
262
263CURLcode vquic_flush(struct Curl_cfilter *cf, struct Curl_easy *data,
264 struct cf_quic_ctx *qctx)
265{
266 const unsigned char *buf;
267 size_t blen, sent;
268 CURLcode result;
269 size_t gsolen;
270
271 while(Curl_bufq_peek(&qctx->sendbuf, &buf, &blen)) {
272 gsolen = qctx->gsolen;
273 if(qctx->split_len) {
274 gsolen = qctx->split_gsolen;
275 if(blen > qctx->split_len)
276 blen = qctx->split_len;
277 }
278
279 result = vquic_send_packets(cf, data, qctx, buf, blen, gsolen, &sent);
280 CURL_TRC_CF(data, cf, "vquic_send(len=%zu, gso=%zu) -> %d, sent=%zu",
281 blen, gsolen, result, sent);
282 if(result) {
283 if(result == CURLE_AGAIN) {
284 Curl_bufq_skip(&qctx->sendbuf, sent);
285 if(qctx->split_len)
286 qctx->split_len -= sent;
287 }
288 return result;
289 }
290 Curl_bufq_skip(&qctx->sendbuf, sent);
291 if(qctx->split_len)
292 qctx->split_len -= sent;
293 }
294 return CURLE_OK;
295}
296
297CURLcode vquic_send(struct Curl_cfilter *cf, struct Curl_easy *data,
298 struct cf_quic_ctx *qctx, size_t gsolen)
299{
300 qctx->gsolen = gsolen;
301 return vquic_flush(cf, data, qctx);
302}
303
304CURLcode vquic_send_tail_split(struct Curl_cfilter *cf, struct Curl_easy *data,
305 struct cf_quic_ctx *qctx, size_t gsolen,
306 size_t tail_len, size_t tail_gsolen)
307{
308 DEBUGASSERT(Curl_bufq_len(&qctx->sendbuf) > tail_len);
309 qctx->split_len = Curl_bufq_len(&qctx->sendbuf) - tail_len;
310 qctx->split_gsolen = gsolen;
311 qctx->gsolen = tail_gsolen;
312 CURL_TRC_CF(data, cf, "vquic_send_tail_split: [%zu gso=%zu][%zu gso=%zu]",
313 qctx->split_len, qctx->split_gsolen,
314 tail_len, qctx->gsolen);
315 return vquic_flush(cf, data, qctx);
316}
317
318#ifdef HAVE_SENDMMSG
319static CURLcode recvmmsg_packets(struct Curl_cfilter *cf,
320 struct Curl_easy *data,
321 struct cf_quic_ctx *qctx,
322 size_t max_pkts,
323 vquic_recv_pkt_cb *recv_cb, void *userp)
324{
325#define MMSG_NUM 64
326 struct iovec msg_iov[MMSG_NUM];
327 struct mmsghdr mmsg[MMSG_NUM];
328 uint8_t bufs[MMSG_NUM][2*1024];
329 struct sockaddr_storage remote_addr[MMSG_NUM];
330 size_t total_nread, pkts;
331 int mcount, i, n;
332 char errstr[STRERROR_LEN];
333 CURLcode result = CURLE_OK;
334
335 DEBUGASSERT(max_pkts > 0);
336 pkts = 0;
337 total_nread = 0;
338 while(pkts < max_pkts) {
339 n = (int)CURLMIN(MMSG_NUM, max_pkts);
340 memset(&mmsg, 0, sizeof(mmsg));
341 for(i = 0; i < n; ++i) {
342 msg_iov[i].iov_base = bufs[i];
343 msg_iov[i].iov_len = (int)sizeof(bufs[i]);
344 mmsg[i].msg_hdr.msg_iov = &msg_iov[i];
345 mmsg[i].msg_hdr.msg_iovlen = 1;
346 mmsg[i].msg_hdr.msg_name = &remote_addr[i];
347 mmsg[i].msg_hdr.msg_namelen = sizeof(remote_addr[i]);
348 }
349
350 while((mcount = recvmmsg(qctx->sockfd, mmsg, n, 0, NULL)) == -1 &&
351 SOCKERRNO == EINTR)
352 ;
353 if(mcount == -1) {
354 if(SOCKERRNO == EAGAIN || SOCKERRNO == EWOULDBLOCK) {
355 CURL_TRC_CF(data, cf, "ingress, recvmmsg -> EAGAIN");
356 goto out;
357 }
358 if(!cf->connected && SOCKERRNO == ECONNREFUSED) {
359 const char *r_ip = NULL;
360 int r_port = 0;
361 Curl_cf_socket_peek(cf->next, data, NULL, NULL,
362 &r_ip, &r_port, NULL, NULL);
363 failf(data, "QUIC: connection to %s port %u refused",
364 r_ip, r_port);
365 result = CURLE_COULDNT_CONNECT;
366 goto out;
367 }
368 Curl_strerror(SOCKERRNO, errstr, sizeof(errstr));
369 failf(data, "QUIC: recvmsg() unexpectedly returned %d (errno=%d; %s)",
370 mcount, SOCKERRNO, errstr);
371 result = CURLE_RECV_ERROR;
372 goto out;
373 }
374
375 CURL_TRC_CF(data, cf, "recvmmsg() -> %d packets", mcount);
376 pkts += mcount;
377 for(i = 0; i < mcount; ++i) {
378 total_nread += mmsg[i].msg_len;
379 result = recv_cb(bufs[i], mmsg[i].msg_len,
380 mmsg[i].msg_hdr.msg_name, mmsg[i].msg_hdr.msg_namelen,
381 0, userp);
382 if(result)
383 goto out;
384 }
385 }
386
387out:
388 if(total_nread || result)
389 CURL_TRC_CF(data, cf, "recvd %zu packets with %zu bytes -> %d",
390 pkts, total_nread, result);
391 return result;
392}
393
394#elif defined(HAVE_SENDMSG)
395static CURLcode recvmsg_packets(struct Curl_cfilter *cf,
396 struct Curl_easy *data,
397 struct cf_quic_ctx *qctx,
398 size_t max_pkts,
399 vquic_recv_pkt_cb *recv_cb, void *userp)
400{
401 struct iovec msg_iov;
402 struct msghdr msg;
403 uint8_t buf[64*1024];
404 struct sockaddr_storage remote_addr;
405 size_t total_nread, pkts;
406 ssize_t nread;
407 char errstr[STRERROR_LEN];
408 CURLcode result = CURLE_OK;
409
410 msg_iov.iov_base = buf;
411 msg_iov.iov_len = (int)sizeof(buf);
412
413 memset(&msg, 0, sizeof(msg));
414 msg.msg_iov = &msg_iov;
415 msg.msg_iovlen = 1;
416
417 DEBUGASSERT(max_pkts > 0);
418 for(pkts = 0, total_nread = 0; pkts < max_pkts;) {
419 msg.msg_name = &remote_addr;
420 msg.msg_namelen = sizeof(remote_addr);
421 while((nread = recvmsg(qctx->sockfd, &msg, 0)) == -1 &&
422 SOCKERRNO == EINTR)
423 ;
424 if(nread == -1) {
425 if(SOCKERRNO == EAGAIN || SOCKERRNO == EWOULDBLOCK) {
426 goto out;
427 }
428 if(!cf->connected && SOCKERRNO == ECONNREFUSED) {
429 const char *r_ip = NULL;
430 int r_port = 0;
431 Curl_cf_socket_peek(cf->next, data, NULL, NULL,
432 &r_ip, &r_port, NULL, NULL);
433 failf(data, "QUIC: connection to %s port %u refused",
434 r_ip, r_port);
435 result = CURLE_COULDNT_CONNECT;
436 goto out;
437 }
438 Curl_strerror(SOCKERRNO, errstr, sizeof(errstr));
439 failf(data, "QUIC: recvmsg() unexpectedly returned %zd (errno=%d; %s)",
440 nread, SOCKERRNO, errstr);
441 result = CURLE_RECV_ERROR;
442 goto out;
443 }
444
445 ++pkts;
446 total_nread += (size_t)nread;
447 result = recv_cb(buf, (size_t)nread, msg.msg_name, msg.msg_namelen,
448 0, userp);
449 if(result)
450 goto out;
451 }
452
453out:
454 if(total_nread || result)
455 CURL_TRC_CF(data, cf, "recvd %zu packets with %zu bytes -> %d",
456 pkts, total_nread, result);
457 return result;
458}
459
460#else /* HAVE_SENDMMSG || HAVE_SENDMSG */
461static CURLcode recvfrom_packets(struct Curl_cfilter *cf,
462 struct Curl_easy *data,
463 struct cf_quic_ctx *qctx,
464 size_t max_pkts,
465 vquic_recv_pkt_cb *recv_cb, void *userp)
466{
467 uint8_t buf[64*1024];
468 int bufsize = (int)sizeof(buf);
469 struct sockaddr_storage remote_addr;
470 socklen_t remote_addrlen = sizeof(remote_addr);
471 size_t total_nread, pkts;
472 ssize_t nread;
473 char errstr[STRERROR_LEN];
474 CURLcode result = CURLE_OK;
475
476 DEBUGASSERT(max_pkts > 0);
477 for(pkts = 0, total_nread = 0; pkts < max_pkts;) {
478 while((nread = recvfrom(qctx->sockfd, (char *)buf, bufsize, 0,
479 (struct sockaddr *)&remote_addr,
480 &remote_addrlen)) == -1 &&
481 SOCKERRNO == EINTR)
482 ;
483 if(nread == -1) {
484 if(SOCKERRNO == EAGAIN || SOCKERRNO == EWOULDBLOCK) {
485 CURL_TRC_CF(data, cf, "ingress, recvfrom -> EAGAIN");
486 goto out;
487 }
488 if(!cf->connected && SOCKERRNO == ECONNREFUSED) {
489 const char *r_ip = NULL;
490 int r_port = 0;
491 Curl_cf_socket_peek(cf->next, data, NULL, NULL,
492 &r_ip, &r_port, NULL, NULL);
493 failf(data, "QUIC: connection to %s port %u refused",
494 r_ip, r_port);
495 result = CURLE_COULDNT_CONNECT;
496 goto out;
497 }
498 Curl_strerror(SOCKERRNO, errstr, sizeof(errstr));
499 failf(data, "QUIC: recvfrom() unexpectedly returned %zd (errno=%d; %s)",
500 nread, SOCKERRNO, errstr);
501 result = CURLE_RECV_ERROR;
502 goto out;
503 }
504
505 ++pkts;
506 total_nread += (size_t)nread;
507 result = recv_cb(buf, (size_t)nread, &remote_addr, remote_addrlen,
508 0, userp);
509 if(result)
510 goto out;
511 }
512
513out:
514 if(total_nread || result)
515 CURL_TRC_CF(data, cf, "recvd %zu packets with %zu bytes -> %d",
516 pkts, total_nread, result);
517 return result;
518}
519#endif /* !HAVE_SENDMMSG && !HAVE_SENDMSG */
520
521CURLcode vquic_recv_packets(struct Curl_cfilter *cf,
522 struct Curl_easy *data,
523 struct cf_quic_ctx *qctx,
524 size_t max_pkts,
525 vquic_recv_pkt_cb *recv_cb, void *userp)
526{
527#if defined(HAVE_SENDMMSG)
528 return recvmmsg_packets(cf, data, qctx, max_pkts, recv_cb, userp);
529#elif defined(HAVE_SENDMSG)
530 return recvmsg_packets(cf, data, qctx, max_pkts, recv_cb, userp);
531#else
532 return recvfrom_packets(cf, data, qctx, max_pkts, recv_cb, userp);
533#endif
534}
535
536/*
537 * If the QLOGDIR environment variable is set, open and return a file
538 * descriptor to write the log to.
539 *
540 * This function returns error if something failed outside of failing to
541 * create the file. Open file success is deemed by seeing if the returned fd
542 * is != -1.
543 */
544CURLcode Curl_qlogdir(struct Curl_easy *data,
545 unsigned char *scid,
546 size_t scidlen,
547 int *qlogfdp)
548{
549 const char *qlog_dir = getenv("QLOGDIR");
550 *qlogfdp = -1;
551 if(qlog_dir) {
552 struct dynbuf fname;
553 CURLcode result;
554 unsigned int i;
555 Curl_dyn_init(&fname, DYN_QLOG_NAME);
556 result = Curl_dyn_add(&fname, qlog_dir);
557 if(!result)
558 result = Curl_dyn_add(&fname, "/");
559 for(i = 0; (i < scidlen) && !result; i++) {
560 char hex[3];
561 msnprintf(hex, 3, "%02x", scid[i]);
562 result = Curl_dyn_add(&fname, hex);
563 }
564 if(!result)
565 result = Curl_dyn_add(&fname, ".sqlog");
566
567 if(!result) {
568 int qlogfd = open(Curl_dyn_ptr(&fname), QLOGMODE,
569 data->set.new_file_perms);
570 if(qlogfd != -1)
571 *qlogfdp = qlogfd;
572 }
573 Curl_dyn_free(&fname);
574 if(result)
575 return result;
576 }
577
578 return CURLE_OK;
579}
580
581CURLcode Curl_cf_quic_create(struct Curl_cfilter **pcf,
582 struct Curl_easy *data,
583 struct connectdata *conn,
584 const struct Curl_addrinfo *ai,
585 int transport)
586{
587 (void)transport;
588 DEBUGASSERT(transport == TRNSPRT_QUIC);
589#if defined(USE_NGTCP2) && defined(USE_NGHTTP3)
590 return Curl_cf_ngtcp2_create(pcf, data, conn, ai);
591#elif defined(USE_QUICHE)
592 return Curl_cf_quiche_create(pcf, data, conn, ai);
593#elif defined(USE_MSH3)
594 return Curl_cf_msh3_create(pcf, data, conn, ai);
595#else
596 *pcf = NULL;
597 (void)data;
598 (void)conn;
599 (void)ai;
600 return CURLE_NOT_BUILT_IN;
601#endif
602}
603
604bool Curl_conn_is_http3(const struct Curl_easy *data,
605 const struct connectdata *conn,
606 int sockindex)
607{
608#if defined(USE_NGTCP2) && defined(USE_NGHTTP3)
609 return Curl_conn_is_ngtcp2(data, conn, sockindex);
610#elif defined(USE_QUICHE)
611 return Curl_conn_is_quiche(data, conn, sockindex);
612#elif defined(USE_MSH3)
613 return Curl_conn_is_msh3(data, conn, sockindex);
614#else
615 return ((conn->handler->protocol & PROTO_FAMILY_HTTP) &&
616 (conn->httpversion == 30));
617#endif
618}
619
620CURLcode Curl_conn_may_http3(struct Curl_easy *data,
621 const struct connectdata *conn)
622{
623 if(conn->transport == TRNSPRT_UNIX) {
624 /* cannot do QUIC over a unix domain socket */
625 return CURLE_QUIC_CONNECT_ERROR;
626 }
627 if(!(conn->handler->flags & PROTOPT_SSL)) {
628 failf(data, "HTTP/3 requested for non-HTTPS URL");
629 return CURLE_URL_MALFORMAT;
630 }
631#ifndef CURL_DISABLE_PROXY
632 if(conn->bits.socksproxy) {
633 failf(data, "HTTP/3 is not supported over a SOCKS proxy");
634 return CURLE_URL_MALFORMAT;
635 }
636 if(conn->bits.httpproxy && conn->bits.tunnel_proxy) {
637 failf(data, "HTTP/3 is not supported over a HTTP proxy");
638 return CURLE_URL_MALFORMAT;
639 }
640#endif
641
642 return CURLE_OK;
643}
644
645#else /* ENABLE_QUIC */
646
647CURLcode Curl_conn_may_http3(struct Curl_easy *data,
648 const struct connectdata *conn)
649{
650 (void)conn;
651 (void)data;
652 DEBUGF(infof(data, "QUIC is not supported in this build"));
653 return CURLE_NOT_BUILT_IN;
654}
655
656#endif /* !ENABLE_QUIC */
657