curl-fuzzer C++ reference
Loading...
Searching...
No Matches
http3_mock_server.cc
Go to the documentation of this file.
1/*
2 * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al.
3 *
4 * SPDX-License-Identifier: curl
5 */
6
9
11
12#include <arpa/inet.h>
13#include <fcntl.h>
14#include <nghttp3/nghttp3.h>
15#include <ngtcp2/ngtcp2.h>
16#include <ngtcp2/ngtcp2_crypto.h>
17#include <ngtcp2/ngtcp2_crypto_ossl.h>
18#include <openssl/err.h>
19#include <openssl/pem.h>
20#include <openssl/ssl.h>
21#include <sys/socket.h>
22#include <unistd.h>
23
24#include <algorithm>
25#include <array>
26#include <cerrno>
27#include <chrono>
28#include <climits>
29#include <cstddef>
30#include <cstdint>
31#include <cstring>
32#include <memory>
33#include <string>
34#include <utility>
35#include <vector>
36
39#include "proto_fuzzer/tls_test_credentials.h"
40
41namespace proto_fuzzer {
42
43namespace {
44
45constexpr std::int64_t kNoStreamId = -1;
46constexpr std::size_t kMaxDatagramsPerTurn = 64;
47constexpr std::size_t kServerConnectionIdBytes = 18;
48constexpr std::size_t kReceiveBufferBytes = 64 * 1024;
49// One request plus the six HTTP/3 critical streams leave room for every
50// bounded action to create a fresh raw unidirectional stream.
51constexpr std::size_t kMaxStreams = scenario_limits::kMaxHttp3Actions + 8;
52constexpr std::size_t kMaxWritesPerTurn = 64;
53constexpr std::size_t kMaxPendingPlaintextBytes = 64 * 1024;
54constexpr int kHttp3IdleIterations = 32;
55// QUIC's first two bits select one of these wire widths. The remaining bits
56// encode the integer itself.
57constexpr std::array<std::size_t, 4> kQuicVarintByteWidths = {1, 2, 4, 8};
58constexpr unsigned int kQuicVarintTagBits = 2;
59constexpr unsigned int kQuicVarintTagShift = CHAR_BIT - kQuicVarintTagBits;
60static_assert(CHAR_BIT == 8, "QUIC varints require 8-bit bytes");
61
62enum StreamRoleIndex : std::size_t {
63 kResponseStream = 0,
64 kControlStream = 1,
65 kQpackEncoderStream = 2,
66 kQpackDecoderStream = 3,
67 kStreamRoleCount = 4,
68};
69
72class OpenSslErrorQueueGuard {
73 public:
74 OpenSslErrorQueueGuard() { ERR_clear_error(); }
75 ~OpenSslErrorQueueGuard() { ERR_clear_error(); }
76
77 OpenSslErrorQueueGuard(const OpenSslErrorQueueGuard&) = delete;
78 OpenSslErrorQueueGuard& operator=(const OpenSslErrorQueueGuard&) = delete;
79};
80
83bool ConfigureSocket(int fd) {
84 if (fd < 0) {
85 return false;
86 }
87 const int descriptor_flags = ::fcntl(fd, F_GETFD, 0);
88 if (descriptor_flags < 0 || ::fcntl(fd, F_SETFD, descriptor_flags | FD_CLOEXEC) < 0) {
89 return false;
90 }
91 const int status_flags = ::fcntl(fd, F_GETFL, 0);
92 return status_flags >= 0 && ::fcntl(fd, F_SETFL, status_flags | O_NONBLOCK) == 0;
93}
94
96int OpenLoopbackSocket(struct sockaddr_in* bound_address) {
97 if (bound_address == nullptr) {
98 return -1;
99 }
100
101 const int fd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
102 if (fd < 0 || !ConfigureSocket(fd)) {
103 if (fd >= 0) {
104 (void)::close(fd);
105 }
106 return -1;
107 }
108
109 struct sockaddr_in requested = {};
110 requested.sin_family = AF_INET;
111 requested.sin_port = htons(0);
112 requested.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
113 if (::bind(fd, reinterpret_cast<const struct sockaddr*>(&requested), sizeof(requested)) != 0) {
114 (void)::close(fd);
115 return -1;
116 }
117
118 socklen_t length = sizeof(*bound_address);
119 std::memset(bound_address, 0, sizeof(*bound_address));
120 if (::getsockname(fd, reinterpret_cast<struct sockaddr*>(bound_address), &length) != 0 ||
121 length != sizeof(*bound_address) || bound_address->sin_family != AF_INET) {
122 (void)::close(fd);
123 return -1;
124 }
125 return fd;
126}
127
129bool RewriteDestination(struct curl_sockaddr* address, const struct sockaddr_in& destination) {
130 if (address == nullptr || sizeof(destination) > sizeof(address->addr)) {
131 return false;
132 }
133 address->family = AF_INET;
134 address->socktype = SOCK_DGRAM;
135 address->protocol = IPPROTO_UDP;
136 address->addrlen = sizeof(destination);
137 std::memset(&address->addr, 0, sizeof(address->addr));
138 std::memcpy(&address->addr, &destination, sizeof(destination));
139 return true;
140}
141
143int SelectHttp3Alpn(SSL* /*ssl*/, const unsigned char** selected, unsigned char* selected_length,
144 const unsigned char* client_protocols, unsigned int client_protocols_length, void* /*userdata*/) {
145 static constexpr unsigned char server_protocols[] = {2, 'h', '3'};
146 unsigned char* match = nullptr;
147 unsigned char match_length = 0;
148 const int result = SSL_select_next_proto(&match, &match_length, server_protocols, sizeof(server_protocols),
149 client_protocols, client_protocols_length);
150 if (result != OPENSSL_NPN_NEGOTIATED) {
151 return SSL_TLSEXT_ERR_ALERT_FATAL;
152 }
153 *selected = match;
154 *selected_length = match_length;
155 return SSL_TLSEXT_ERR_OK;
156}
157
160bool AddExtraChainCertificate(SSL_CTX* context, const char* certificate_pem) {
161 BIO* certificate_bio = BIO_new_mem_buf(certificate_pem, -1);
162 if (certificate_bio == nullptr) {
163 return false;
164 }
165 X509* certificate = PEM_read_bio_X509(certificate_bio, nullptr, nullptr, nullptr);
166 BIO_free(certificate_bio);
167 if (certificate == nullptr) {
168 return false;
169 }
170 if (SSL_CTX_add_extra_chain_cert(context, certificate) != 1) {
171 X509_free(certificate);
172 return false;
173 }
174 return true;
175}
176
178bool LoadCredentials(SSL_CTX* context, curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain) {
179 BIO* certificate_bio = BIO_new_mem_buf(tls_test_credentials::kCertificatePem, -1);
180 BIO* key_bio = BIO_new_mem_buf(tls_test_credentials::kPrivateKeyPem, -1);
181 if (certificate_bio == nullptr || key_bio == nullptr) {
182 BIO_free(certificate_bio);
183 BIO_free(key_bio);
184 return false;
185 }
186
187 X509* certificate = PEM_read_bio_X509(certificate_bio, nullptr, nullptr, nullptr);
188 EVP_PKEY* key = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, nullptr);
189 BIO_free(certificate_bio);
190 BIO_free(key_bio);
191 if (certificate == nullptr || key == nullptr) {
192 X509_free(certificate);
193 EVP_PKEY_free(key);
194 return false;
195 }
196
197 const bool loaded = SSL_CTX_use_certificate(context, certificate) == 1 && SSL_CTX_use_PrivateKey(context, key) == 1 &&
198 SSL_CTX_check_private_key(context) == 1;
199 X509_free(certificate);
200 EVP_PKEY_free(key);
201 if (!loaded) {
202 return false;
203 }
204
205 if (certificate_chain == curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_ALL_KEY_TYPES) {
206 return AddExtraChainCertificate(context, tls_test_credentials::kRsaCertificatePem) &&
207 AddExtraChainCertificate(context, tls_test_credentials::kDsaCertificatePem) &&
208 AddExtraChainCertificate(context, tls_test_credentials::kDhCertificatePem);
209 }
210 return true;
211}
212
214SSL_CTX* CreateTlsContext(curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain) {
215 OpenSslErrorQueueGuard error_guard;
216 SSL_CTX* context = SSL_CTX_new(TLS_server_method());
217 if (context == nullptr || !LoadCredentials(context, certificate_chain)) {
218 SSL_CTX_free(context);
219 return nullptr;
220 }
221
222 if (SSL_CTX_set_min_proto_version(context, TLS1_3_VERSION) != 1 ||
223 SSL_CTX_set_max_proto_version(context, TLS1_3_VERSION) != 1) {
224 SSL_CTX_free(context);
225 return nullptr;
226 }
227 (void)SSL_CTX_set_options(context, SSL_OP_NO_COMPRESSION);
228 SSL_CTX_set_alpn_select_cb(context, &SelectHttp3Alpn, nullptr);
229 return context;
230}
231
233void AppendQuicVarint(std::string* output, std::uint64_t value) {
235 for (std::size_t encoding = 0; encoding < kQuicVarintByteWidths.size(); ++encoding) {
236 const std::size_t byte_width = kQuicVarintByteWidths[encoding];
237 const unsigned int value_bits = static_cast<unsigned int>(byte_width * CHAR_BIT - kQuicVarintTagBits);
238 if (value >= (std::uint64_t{1} << value_bits)) {
239 continue;
240 }
241
242 for (std::size_t byte = byte_width; byte != 0; --byte) {
243 const unsigned int shift = static_cast<unsigned int>((byte - 1) * CHAR_BIT);
244 unsigned char encoded_byte = static_cast<unsigned char>(value >> shift);
245 if (byte == byte_width) {
246 encoded_byte |= static_cast<unsigned char>(encoding << kQuicVarintTagShift);
247 }
248 output->push_back(static_cast<char>(encoded_byte));
249 }
250 return;
251 }
252}
253
257std::string NormalizeHeaderName(const std::string& source) {
258 std::string result = source.substr(0, scenario_limits::kMaxHttp3HeaderNameBytes);
259 if (result.empty()) {
260 return "x-fuzz";
261 }
262 for (char& byte : result) {
263 const unsigned char value = static_cast<unsigned char>(byte);
264 const bool lower = value >= 'a' && value <= 'z';
265 const bool upper = value >= 'A' && value <= 'Z';
266 const bool digit = value >= '0' && value <= '9';
267 const bool punctuation = value == '!' || value == '#' || value == '$' || value == '%' || value == '&' ||
268 value == '\'' || value == '*' || value == '+' || value == '-' || value == '.' ||
269 value == '^' || value == '_' || value == '`' || value == '|' || value == '~';
270 if (upper) {
271 byte = static_cast<char>(value - 'A' + 'a');
272 } else if (!lower && !digit && !punctuation) {
273 byte = '-';
274 }
275 }
276 return result;
277}
278
280std::string NormalizeHeaderValue(const std::string& source) {
281 std::string result = source.substr(0, scenario_limits::kMaxHttp3HeaderValueBytes);
282 for (char& byte : result) {
283 const unsigned char value = static_cast<unsigned char>(byte);
284 if ((value < 0x20U && value != '\t') || value == 0x7fU) {
285 byte = ' ';
286 }
287 }
288 return result;
289}
290
292ngtcp2_tstamp QuicNow() {
293 const auto now = std::chrono::steady_clock::now().time_since_epoch();
294 return static_cast<ngtcp2_tstamp>(std::chrono::duration_cast<std::chrono::nanoseconds>(now).count());
295}
296
297} // namespace
298
302class Http3MockServerImpl {
303 public:
304 explicit Http3MockServerImpl(curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain)
305 : context_(CreateTlsContext(certificate_chain)), crypto_connection_ref_{&GetQuicConnection, this} {
306 role_stream_ids_.fill(kNoStreamId);
307 streams_.reserve(kMaxStreams);
308 retained_writes_.reserve(kMaxWritesPerTurn);
309 }
310
311 ~Http3MockServerImpl() {
312 ResetPeer();
313 OpenSslErrorQueueGuard error_guard;
314 SSL_CTX_free(context_);
315 }
316
317 Http3MockServerImpl(const Http3MockServerImpl&) = delete;
318 Http3MockServerImpl& operator=(const Http3MockServerImpl&) = delete;
319
320 curl_socket_t OpenSocket(curlsocktype purpose, struct curl_sockaddr* address) {
321 if (purpose != CURLSOCKTYPE_IPCXN || address == nullptr || socket_opened_ || context_ == nullptr) {
322 return CURL_SOCKET_BAD;
323 }
324 socket_opened_ = true;
325
326 struct sockaddr_in listener_address = {};
327 server_fd_ = OpenLoopbackSocket(&listener_address);
328 if (server_fd_ < 0 || !RewriteDestination(address, listener_address)) {
329 ResetPeer();
330 return CURL_SOCKET_BAD;
331 }
332 server_port_ = ntohs(listener_address.sin_port);
333 local_address_ = listener_address;
334
335 const int client_fd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
336 if (client_fd < 0 || !ConfigureSocket(client_fd)) {
337 if (client_fd >= 0) {
338 (void)::close(client_fd);
339 }
340 ResetPeer();
341 return CURL_SOCKET_BAD;
342 }
343 return static_cast<curl_socket_t>(client_fd);
344 }
345
346 void BeginScenario(const curl::fuzzer::proto::Scenario& scenario) {
347 ResetPeer();
348 scenario_ = &scenario;
349 default_response_.Clear();
350 default_response_.set_status_code(200);
351 default_response_.set_finish_stream(true);
352 }
353
354 std::size_t DrivePeerTurn() {
355 if (server_fd_ < 0 || transport_failed_) {
356 return 0;
357 }
358
359 OpenSslErrorQueueGuard error_guard;
360 std::size_t progress = HandleExpiry();
361 if (transport_failed_) {
362 return progress;
363 }
364 progress += ReadDatagrams();
365 if (quic_connection_ == nullptr || transport_failed_) {
366 return progress;
367 }
368
369 if (handshake_complete_ && !close_requested_ && !close_sent_) {
370 progress += CreateServerStreams();
371 progress += DriveApplicationOutput();
372 }
373 progress += FlushTransportPackets();
374 return progress;
375 }
376
377 bool handshake_complete() const { return handshake_complete_; }
378
379 bool request_headers_received() const { return request_headers_received_; }
380
381 std::size_t executed_action_count() const { return next_action_; }
382
383 std::uint16_t server_port() const { return server_port_; }
384
385 private:
386 // ngtcp2 acknowledges QUIC writes by stream offset, while nghttp3 accepts
387 // acknowledgement only for its own serialized bytes. Track those regions
388 // separately from deliberately raw script writes.
389 struct StreamState {
390 struct ManagedWriteRange {
391 std::uint64_t begin = 0;
392 std::uint64_t end = 0;
393 };
394
395 std::int64_t id = kNoStreamId;
396 std::uint64_t write_offset = 0;
397 std::vector<ManagedWriteRange> managed_write_ranges;
398 std::vector<std::uint64_t> managed_fin_offsets;
399 std::size_t next_managed_ack_range = 0;
400 std::size_t next_managed_fin_offset = 0;
401 };
402
403 // Queue at most one application write at a time. The byte string is kept in
404 // retained_writes_ because ngtcp2 may submit only a prefix on each turn.
405 struct PendingWrite {
406 std::int64_t stream_id = kNoStreamId;
407 const std::string* bytes = nullptr;
408 std::size_t offset = 0;
409 bool finish_stream = false;
410 bool nghttp3_managed = false;
411 bool completes_action = false;
412
413 bool active() const { return stream_id != kNoStreamId; }
414
415 void Clear() {
416 stream_id = kNoStreamId;
417 bytes = nullptr;
418 offset = 0;
419 finish_stream = false;
420 nghttp3_managed = false;
421 completes_action = false;
422 }
423 };
424
425 // nghttp3 pulls response bytes through a callback. This state lets that
426 // callback preserve the scenario's chunk boundaries and final-stream intent.
427 struct ResponseBodyState {
428 std::vector<std::string> chunks;
429 std::size_t next_chunk = 0;
430 bool finish_stream = false;
431 bool has_trailers = false;
432 };
433
434 enum class WriteResult {
435 kComplete,
436 kBlocked,
437 kFailed,
438 };
439
440 // Tear down in protocol-layer order: HTTP/3 retains references into QUIC,
441 // and OpenSSL's app data points back to this object until it is cleared.
442 void ResetPeer() {
443 OpenSslErrorQueueGuard error_guard;
444 pending_write_.Clear();
445 nghttp3_conn_del(http3_connection_);
446 http3_connection_ = nullptr;
447 ngtcp2_conn_del(quic_connection_);
448 quic_connection_ = nullptr;
449 if (tls_connection_ != nullptr) {
450 SSL_set_app_data(tls_connection_, nullptr);
451 SSL_free(tls_connection_);
452 tls_connection_ = nullptr;
453 }
454 ngtcp2_crypto_ossl_ctx_del(crypto_context_);
455 crypto_context_ = nullptr;
456 streams_.clear();
457 retained_writes_.clear();
458 if (server_fd_ >= 0) {
459 (void)::close(server_fd_);
460 server_fd_ = -1;
461 }
462
463 scenario_ = nullptr;
464 role_stream_ids_.fill(kNoStreamId);
465 std::memset(&local_address_, 0, sizeof(local_address_));
466 std::memset(&remote_address_, 0, sizeof(remote_address_));
467 ngtcp2_path_storage_zero(&path_storage_);
468 response_body_.chunks.clear();
469 response_body_.next_chunk = 0;
470 response_body_.finish_stream = false;
471 response_body_.has_trailers = false;
472 next_action_ = 0;
473 server_port_ = 0;
474 socket_opened_ = false;
475 handshake_complete_ = false;
476 request_headers_received_ = false;
477 server_streams_bound_ = false;
478 bootstrap_drained_ = false;
479 waiting_for_h3_drain_ = false;
480 waiting_drain_completes_action_ = false;
481 final_response_submitted_ = false;
482 default_response_started_ = false;
483 http3_failed_ = false;
484 transport_failed_ = false;
485 close_requested_ = false;
486 close_sent_ = false;
487 close_error_code_ = 0;
488 random_counter_ = 0;
489 }
490
491 // ngtcp2 calls into this group while it owns QUIC parsing and encryption.
492 // Each callback bridges a transport event into the separately-owned nghttp3
493 // connection or records enough state for a later application turn.
494 static ngtcp2_conn* GetQuicConnection(ngtcp2_crypto_conn_ref* connection_ref) {
495 auto* self = static_cast<Http3MockServerImpl*>(connection_ref->user_data);
496 return self == nullptr ? nullptr : self->quic_connection_;
497 }
498
499 static int HandshakeCompleted(ngtcp2_conn* /*connection*/, void* user_data) {
500 auto* self = static_cast<Http3MockServerImpl*>(user_data);
501 if (self == nullptr) {
502 return NGTCP2_ERR_CALLBACK_FAILURE;
503 }
504 self->handshake_complete_ = true;
505 return 0;
506 }
507
508 static int ReceiveTransmitKey(ngtcp2_conn* /*connection*/, ngtcp2_encryption_level level, void* user_data) {
509 auto* self = static_cast<Http3MockServerImpl*>(user_data);
510 if (self == nullptr) {
511 return NGTCP2_ERR_CALLBACK_FAILURE;
512 }
513 if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT || self->server_streams_bound_) {
514 return 0;
515 }
516 // HTTP/3 application streams become valid only after 1-RTT keys exist.
517 // Bind its critical streams before allowing script output onto the peer.
518 if (!self->CreateHttp3Connection()) {
519 return NGTCP2_ERR_CALLBACK_FAILURE;
520 }
521 (void)self->CreateServerStreams();
522 return self->server_streams_bound_ ? 0 : NGTCP2_ERR_CALLBACK_FAILURE;
523 }
524
525 static void GenerateRandomBytes(std::uint8_t* destination, std::size_t length,
526 const ngtcp2_rand_ctx* random_context) {
527 if (random_context == nullptr) {
528 std::memset(destination, 0, length);
529 return;
530 }
531 auto* self = static_cast<Http3MockServerImpl*>(random_context->native_handle);
532 if (self == nullptr) {
533 std::memset(destination, 0, length);
534 return;
535 }
536 self->FillRandom(destination, length);
537 }
538
539 static int GetNewConnectionId(ngtcp2_conn* /*connection*/, ngtcp2_cid* connection_id,
540 ngtcp2_stateless_reset_token* reset_token, std::size_t connection_id_length,
541 void* user_data) {
542 auto* self = static_cast<Http3MockServerImpl*>(user_data);
543 if (self == nullptr || connection_id == nullptr || reset_token == nullptr ||
544 connection_id_length > sizeof(connection_id->data)) {
545 return NGTCP2_ERR_CALLBACK_FAILURE;
546 }
547 connection_id->datalen = connection_id_length;
548 self->FillRandom(connection_id->data, connection_id_length);
549 self->FillRandom(reset_token->data, sizeof(reset_token->data));
550 return 0;
551 }
552
553 static int OpenRemoteStream(ngtcp2_conn* /*connection*/, std::int64_t stream_id, void* user_data) {
554 auto* self = static_cast<Http3MockServerImpl*>(user_data);
555 if (self == nullptr || self->streams_.size() >= kMaxStreams) {
556 return NGTCP2_ERR_CALLBACK_FAILURE;
557 }
558 StreamState state;
559 state.id = stream_id;
560 self->streams_.push_back(state);
561 if (self->role_stream_ids_[kResponseStream] == kNoStreamId && ngtcp2_is_bidi_stream(stream_id)) {
562 self->role_stream_ids_[kResponseStream] = stream_id;
563 }
564 return 0;
565 }
566
567 // ngtcp2 presents decrypted stream bytes; nghttp3 parses the HTTP/3 layer.
568 // Extending QUIC's flow-control windows by the consumed amount keeps only
569 // parsed bytes eligible for further client transmission.
570 static int ReceiveQuicStreamData(ngtcp2_conn* connection, std::uint32_t flags, std::int64_t stream_id,
571 std::uint64_t /*offset*/, const std::uint8_t* data, std::size_t length,
572 void* user_data, void* /*stream_user_data*/) {
573 auto* self = static_cast<Http3MockServerImpl*>(user_data);
574 if (self == nullptr || self->http3_connection_ == nullptr || self->http3_failed_) {
575 return NGTCP2_ERR_CALLBACK_FAILURE;
576 }
577 const nghttp3_ssize consumed =
578 nghttp3_conn_read_stream2(self->http3_connection_, stream_id, data, length, flags & NGTCP2_STREAM_DATA_FLAG_FIN,
579 ngtcp2_conn_get_timestamp(connection));
580 if (consumed < 0) {
581 self->http3_failed_ = true;
582 return NGTCP2_ERR_CALLBACK_FAILURE;
583 }
584 ngtcp2_conn_extend_max_stream_offset(connection, stream_id, static_cast<std::uint64_t>(consumed));
585 ngtcp2_conn_extend_max_offset(connection, static_cast<std::uint64_t>(consumed));
586 return 0;
587 }
588
589 // A transport acknowledgement covers both raw fuzz bytes and nghttp3 output.
590 // Report only intersecting nghttp3-managed ranges back to nghttp3; otherwise
591 // its write offsets would advance for bytes it never generated.
592 static int AckedStreamDataOffset(ngtcp2_conn* /*connection*/, std::int64_t stream_id, std::uint64_t offset,
593 std::uint64_t length, void* user_data, void* /*stream_user_data*/) {
594 auto* self = static_cast<Http3MockServerImpl*>(user_data);
595 if (self == nullptr || self->http3_connection_ == nullptr || self->http3_failed_) {
596 return 0;
597 }
598
599 StreamState* stream = self->FindStream(stream_id);
600 if (stream == nullptr || offset > UINT64_MAX - length) {
601 return NGTCP2_ERR_CALLBACK_FAILURE;
602 }
603
604 if (length == 0) {
605 if (stream->next_managed_fin_offset < stream->managed_fin_offsets.size() &&
606 stream->managed_fin_offsets[stream->next_managed_fin_offset] == offset) {
607 ++stream->next_managed_fin_offset;
608 if (nghttp3_conn_add_ack_offset(self->http3_connection_, stream_id, 0) != 0) {
609 self->http3_failed_ = true;
610 return NGTCP2_ERR_CALLBACK_FAILURE;
611 }
612 }
613 return 0;
614 }
615
616 const std::uint64_t end = offset + length;
617 std::uint64_t managed_length = 0;
618 for (std::size_t index = stream->next_managed_ack_range; index < stream->managed_write_ranges.size(); ++index) {
619 const StreamState::ManagedWriteRange& range = stream->managed_write_ranges[index];
620 if (range.end <= offset) {
621 stream->next_managed_ack_range = index + 1;
622 continue;
623 }
624 if (range.begin >= end) {
625 break;
626 }
627 managed_length += std::min(range.end, end) - std::max(range.begin, offset);
628 if (range.end <= end) {
629 stream->next_managed_ack_range = index + 1;
630 }
631 }
632
633 if (managed_length != 0 && nghttp3_conn_add_ack_offset(self->http3_connection_, stream_id, managed_length) != 0) {
634 self->http3_failed_ = true;
635 return NGTCP2_ERR_CALLBACK_FAILURE;
636 }
637 return 0;
638 }
639
640 // Keep nghttp3's stream lifecycle synchronized with QUIC resets, STOP_SENDING
641 // events, and close error-code flags.
642 static int CloseQuicStream(ngtcp2_conn* /*connection*/, std::uint32_t flags, std::int64_t stream_id,
643 std::uint64_t receive_error_code, std::uint64_t transmit_error_code, void* user_data,
644 void* /*stream_user_data*/) {
645 auto* self = static_cast<Http3MockServerImpl*>(user_data);
646 if (self == nullptr || self->http3_connection_ == nullptr || self->http3_failed_) {
647 return 0;
648 }
649 std::uint32_t http3_flags = NGHTTP3_STREAM_CLOSE_FLAG_NONE;
650 if ((flags & NGTCP2_STREAM_CLOSE2_FLAG_RX_APP_ERROR_CODE_SET) != 0) {
651 http3_flags |= NGHTTP3_STREAM_CLOSE_FLAG_RX_APP_ERROR_CODE_SET;
652 }
653 if ((flags & NGTCP2_STREAM_CLOSE2_FLAG_TX_APP_ERROR_CODE_SET) != 0) {
654 http3_flags |= NGHTTP3_STREAM_CLOSE_FLAG_TX_APP_ERROR_CODE_SET;
655 }
656 const int result = nghttp3_conn_close_stream2(self->http3_connection_, http3_flags, stream_id, receive_error_code,
657 transmit_error_code);
658 if (result != 0 && result != NGHTTP3_ERR_STREAM_NOT_FOUND) {
659 self->http3_failed_ = true;
660 return NGTCP2_ERR_CALLBACK_FAILURE;
661 }
662 return 0;
663 }
664
665 static int ResetQuicStream(ngtcp2_conn* /*connection*/, std::int64_t stream_id, std::uint64_t /*final_size*/,
666 std::uint64_t /*application_error_code*/, void* user_data, void* /*stream_user_data*/) {
667 return ShutdownHttp3StreamRead(stream_id, user_data);
668 }
669
670 static int StopSendingQuicStream(ngtcp2_conn* /*connection*/, std::int64_t stream_id,
671 std::uint64_t /*application_error_code*/, void* user_data,
672 void* /*stream_user_data*/) {
673 return ShutdownHttp3StreamRead(stream_id, user_data);
674 }
675
676 static int ShutdownHttp3StreamRead(std::int64_t stream_id, void* user_data) {
677 auto* self = static_cast<Http3MockServerImpl*>(user_data);
678 if (self == nullptr || self->http3_connection_ == nullptr || self->http3_failed_) {
679 return 0;
680 }
681 const int result = nghttp3_conn_shutdown_stream_read(self->http3_connection_, stream_id);
682 if (result != 0 && result != NGHTTP3_ERR_STREAM_NOT_FOUND) {
683 self->http3_failed_ = true;
684 return NGTCP2_ERR_CALLBACK_FAILURE;
685 }
686 return 0;
687 }
688
689 // Create HTTP/3 once the QUIC handshake can carry application traffic.
690 // Dynamic QPACK is intentionally disabled so scenarios cannot retain table
691 // state beyond the bounded peer lifetime.
692 bool CreateHttp3Connection() {
693 if (http3_connection_ != nullptr) {
694 return true;
695 }
696 nghttp3_callbacks callbacks = {};
697 callbacks.recv_data = &ReceiveRequestData;
698 callbacks.deferred_consume = &DeferredConsumeRequestData;
699 callbacks.end_headers = &EndRequestHeaders;
700 callbacks.end_stream = &EndRequestStream;
701 nghttp3_settings settings;
702 nghttp3_settings_default(&settings);
703 settings.qpack_max_dtable_capacity = 0;
704 settings.qpack_encoder_max_dtable_capacity = 0;
705 settings.qpack_blocked_streams = 0;
706 if (nghttp3_conn_server_new(&http3_connection_, &callbacks, &settings, nullptr, this) != 0) {
707 http3_failed_ = true;
708 return false;
709 }
710 nghttp3_conn_set_max_concurrent_streams(http3_connection_, kMaxStreams);
711 return true;
712 }
713
714 // QUIC needs unpredictable-looking connection IDs and reset tokens, but
715 // fuzzing needs deterministic reruns. This local generator is not used for
716 // cryptographic secrecy.
717 void FillRandom(std::uint8_t* destination, std::size_t length) {
718 std::uint64_t state = ++random_counter_ * UINT64_C(0x9e3779b97f4a7c15);
719 for (std::size_t index = 0; index < length; ++index) {
720 state ^= state >> 12U;
721 state ^= state << 25U;
722 state ^= state >> 27U;
723 destination[index] = static_cast<std::uint8_t>((state * UINT64_C(0x2545f4914f6cdd1d)) >> 56U);
724 }
725 }
726
727 // The first valid Initial establishes the sole remote endpoint. Construct
728 // all three protocol layers here because ngtcp2's crypto callbacks need a
729 // live server-side SSL object while processing this packet.
730 bool InitializeConnection(const ngtcp2_pkt_hd& header, const struct sockaddr_in& remote_address) {
731 if (quic_connection_ != nullptr || header.type != NGTCP2_PKT_INITIAL) {
732 return false;
733 }
734
735 remote_address_ = remote_address;
736 ngtcp2_path_storage_init(&path_storage_, reinterpret_cast<const ngtcp2_sockaddr*>(&local_address_),
737 sizeof(local_address_), reinterpret_cast<const ngtcp2_sockaddr*>(&remote_address_),
738 sizeof(remote_address_), nullptr);
739
740 ngtcp2_callbacks callbacks = {};
741 callbacks.recv_client_initial = ngtcp2_crypto_recv_client_initial_cb;
742 callbacks.recv_crypto_data = ngtcp2_crypto_recv_crypto_data_cb;
743 callbacks.handshake_completed = &HandshakeCompleted;
744 callbacks.encrypt = ngtcp2_crypto_encrypt_cb;
745 callbacks.decrypt = ngtcp2_crypto_decrypt_cb;
746 callbacks.hp_mask = ngtcp2_crypto_hp_mask_cb;
747 callbacks.recv_stream_data = &ReceiveQuicStreamData;
748 callbacks.acked_stream_data_offset = &AckedStreamDataOffset;
749 callbacks.stream_open = &OpenRemoteStream;
750 callbacks.stream_reset = &ResetQuicStream;
751 callbacks.rand = &GenerateRandomBytes;
752 callbacks.update_key = ngtcp2_crypto_update_key_cb;
753 callbacks.delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb;
754 callbacks.delete_crypto_cipher_ctx = ngtcp2_crypto_delete_crypto_cipher_ctx_cb;
755 callbacks.stream_stop_sending = &StopSendingQuicStream;
756 callbacks.version_negotiation = ngtcp2_crypto_version_negotiation_cb;
757 callbacks.recv_tx_key = &ReceiveTransmitKey;
758 callbacks.get_new_connection_id2 = &GetNewConnectionId;
759 callbacks.get_path_challenge_data2 = ngtcp2_crypto_get_path_challenge_data2_cb;
760 callbacks.stream_close2 = &CloseQuicStream;
761
762 ngtcp2_settings settings;
763 ngtcp2_settings_default(&settings);
764 settings.initial_ts = QuicNow();
765 settings.rand_ctx.native_handle = this;
766 settings.no_pmtud = 1;
767 settings.max_tx_udp_payload_size = packet_buffer_.size();
768
769 ngtcp2_transport_params parameters;
770 ngtcp2_transport_params_default(&parameters);
771 parameters.initial_max_stream_data_bidi_local = kMaxPendingPlaintextBytes;
772 parameters.initial_max_stream_data_bidi_remote = kMaxPendingPlaintextBytes;
773 parameters.initial_max_stream_data_uni = kMaxPendingPlaintextBytes;
774 parameters.initial_max_data = kMaxPendingPlaintextBytes * 4U;
775 parameters.initial_max_streams_bidi = kMaxStreams;
776 parameters.initial_max_streams_uni = kMaxStreams;
777 parameters.max_idle_timeout = 5U * NGTCP2_SECONDS;
778 parameters.active_connection_id_limit = 2;
779 parameters.original_dcid = header.dcid;
780 parameters.original_dcid_present = 1;
781
782 ngtcp2_cid server_cid = {};
783 server_cid.datalen = kServerConnectionIdBytes;
784 FillRandom(server_cid.data, server_cid.datalen);
785 if (ngtcp2_conn_server_new(&quic_connection_, &header.scid, &server_cid, &path_storage_.path, header.version,
786 &callbacks, &settings, &parameters, nullptr, this) != 0) {
787 return false;
788 }
789
790 if (ngtcp2_crypto_ossl_ctx_new(&crypto_context_, nullptr) != 0) {
791 return false;
792 }
793 tls_connection_ = SSL_new(context_);
794 if (tls_connection_ == nullptr) {
795 return false;
796 }
797 ngtcp2_crypto_ossl_ctx_set_ssl(crypto_context_, tls_connection_);
798 if (ngtcp2_crypto_ossl_configure_server_session(tls_connection_) != 0) {
799 return false;
800 }
801 SSL_set_app_data(tls_connection_, &crypto_connection_ref_);
802 SSL_set_accept_state(tls_connection_);
803 ngtcp2_conn_set_tls_native_handle(quic_connection_, crypto_context_);
804 return true;
805 }
806
807 // Drain a bounded datagram batch. Before connection setup, accept only a
808 // valid QUIC Initial; afterwards, ignore packets from other UDP endpoints.
809 std::size_t ReadDatagrams() {
810 std::size_t progress = 0;
811 for (std::size_t packet = 0; packet < kMaxDatagramsPerTurn; ++packet) {
812 struct sockaddr_in remote_address = {};
813 socklen_t remote_length = sizeof(remote_address);
814 const ssize_t received = ::recvfrom(server_fd_, receive_buffer_.data(), receive_buffer_.size(), 0,
815 reinterpret_cast<struct sockaddr*>(&remote_address), &remote_length);
816 if (received < 0) {
817 if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
818 transport_failed_ = true;
819 }
820 break;
821 }
822 if (received == 0 || remote_length != sizeof(remote_address) || remote_address.sin_family != AF_INET) {
823 continue;
824 }
825 ++progress;
826
827 if (quic_connection_ == nullptr) {
828 ngtcp2_pkt_hd header = {};
829 if (ngtcp2_accept(&header, receive_buffer_.data(), static_cast<std::size_t>(received)) != 0) {
830 continue;
831 }
832 if (!InitializeConnection(header, remote_address)) {
833 transport_failed_ = true;
834 return progress;
835 }
836 } else if (remote_address.sin_port != remote_address_.sin_port ||
837 remote_address.sin_addr.s_addr != remote_address_.sin_addr.s_addr) {
838 continue;
839 }
840
841 ngtcp2_pkt_info packet_info = {};
842 const int result = ngtcp2_conn_read_pkt(quic_connection_, &path_storage_.path, &packet_info,
843 receive_buffer_.data(), static_cast<std::size_t>(received), QuicNow());
844 if (result != 0) {
845 transport_failed_ = true;
846 return progress;
847 }
848 }
849 return progress;
850 }
851
852 // ngtcp2 timers can make handshake, loss-recovery, or close packets ready
853 // even when curl has not sent a new UDP datagram this turn.
854 std::size_t HandleExpiry() {
855 if (quic_connection_ == nullptr) {
856 return 0;
857 }
858 const ngtcp2_tstamp now = QuicNow();
859 if (ngtcp2_conn_get_expiry2(quic_connection_) > now) {
860 return 0;
861 }
862 const int result = ngtcp2_conn_handle_expiry(quic_connection_, now);
863 if (result != 0) {
864 transport_failed_ = true;
865 }
866 return 1;
867 }
868
869 // A nonblocking socket can defer a packet without making the peer invalid.
870 // Hard send errors are terminal because no later turn can repair the path.
871 bool SendPacket(const ngtcp2_path& path, const std::uint8_t* data, std::size_t length) {
872 const ngtcp2_addr* destination = &path.remote;
873 if (destination->addr == nullptr || destination->addrlen == 0) {
874 destination = &path_storage_.path.remote;
875 }
876 const ssize_t sent = ::sendto(server_fd_, data, length, 0,
877 reinterpret_cast<const struct sockaddr*>(destination->addr), destination->addrlen);
878 if (sent == static_cast<ssize_t>(length)) {
879 return true;
880 }
881 if (sent < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)) {
882 return false;
883 }
884 transport_failed_ = true;
885 return false;
886 }
887
888 // Serialize all QUIC control traffic after application writes. A requested
889 // CONNECTION_CLOSE takes priority and is emitted at most once.
890 std::size_t FlushTransportPackets() {
891 if (quic_connection_ == nullptr || transport_failed_ || close_sent_) {
892 return 0;
893 }
894
895 if (close_requested_) {
896 ngtcp2_ccerr error;
897 ngtcp2_ccerr_default(&error);
898 ngtcp2_ccerr_set_application_error(&error, close_error_code_, nullptr, 0);
899 ngtcp2_path_storage output_path;
900 ngtcp2_path_storage_zero(&output_path);
901 ngtcp2_pkt_info packet_info = {};
902 const ngtcp2_tstamp now = QuicNow();
903 const ngtcp2_ssize written = ngtcp2_conn_write_connection_close(
904 quic_connection_, &output_path.path, &packet_info, packet_buffer_.data(), packet_buffer_.size(), &error, now);
905 if (written < 0) {
906 transport_failed_ = true;
907 return 0;
908 }
909 close_sent_ = true;
910 if (written == 0) {
911 return 1;
912 }
913 ngtcp2_conn_update_pkt_tx_time(quic_connection_, now);
914 (void)SendPacket(output_path.path, packet_buffer_.data(), static_cast<std::size_t>(written));
915 return 1;
916 }
917
918 std::size_t progress = 0;
919 for (std::size_t packet = 0; packet < kMaxWritesPerTurn; ++packet) {
920 ngtcp2_path_storage output_path;
921 ngtcp2_path_storage_zero(&output_path);
922 ngtcp2_pkt_info packet_info = {};
923 const ngtcp2_tstamp now = QuicNow();
924 const ngtcp2_ssize written = ngtcp2_conn_write_pkt(quic_connection_, &output_path.path, &packet_info,
925 packet_buffer_.data(), packet_buffer_.size(), now);
926 if (written < 0) {
927 transport_failed_ = true;
928 break;
929 }
930 if (written == 0) {
931 break;
932 }
933 ngtcp2_conn_update_pkt_tx_time(quic_connection_, now);
934 if (!SendPacket(output_path.path, packet_buffer_.data(), static_cast<std::size_t>(written))) {
935 break;
936 }
937 ++progress;
938 }
939 return progress;
940 }
941
942 // HTTP/3 requires one control stream and two QPACK streams before it can
943 // send response metadata. Their stream IDs are retained by role so script
944 // actions can target them without exposing transport-chosen IDs.
945 std::size_t CreateServerStreams() {
946 if (server_streams_bound_ || quic_connection_ == nullptr || http3_connection_ == nullptr || http3_failed_) {
947 return 0;
948 }
949
950 std::size_t progress = 0;
951 const std::array<StreamRoleIndex, 3> roles = {kControlStream, kQpackEncoderStream, kQpackDecoderStream};
952 for (const StreamRoleIndex role : roles) {
953 if (role_stream_ids_[role] != kNoStreamId) {
954 continue;
955 }
956 const std::int64_t stream_id = OpenUnidirectionalStream();
957 if (stream_id == kNoStreamId) {
958 return progress;
959 }
960 role_stream_ids_[role] = stream_id;
961 ++progress;
962 }
963
964 if (role_stream_ids_[kControlStream] == kNoStreamId || role_stream_ids_[kQpackEncoderStream] == kNoStreamId ||
965 role_stream_ids_[kQpackDecoderStream] == kNoStreamId) {
966 return progress;
967 }
968 if (nghttp3_conn_bind_qpack_streams(http3_connection_, role_stream_ids_[kQpackEncoderStream],
969 role_stream_ids_[kQpackDecoderStream]) != 0 ||
970 nghttp3_conn_bind_control_stream(http3_connection_, role_stream_ids_[kControlStream]) != 0) {
971 http3_failed_ = true;
972 return progress;
973 }
974 server_streams_bound_ = true;
975 return progress + 1;
976 }
977
978 std::int64_t OpenUnidirectionalStream() {
979 if (quic_connection_ == nullptr || streams_.size() >= kMaxStreams) {
980 return kNoStreamId;
981 }
982 std::int64_t stream_id = kNoStreamId;
983 if (ngtcp2_conn_open_uni_stream(quic_connection_, &stream_id, nullptr) != 0) {
984 return kNoStreamId;
985 }
986
987 StreamState state;
988 state.id = stream_id;
989 streams_.push_back(state);
990 return stream_id;
991 }
992
993 // Advance protocol-generated output before one scenario action. This keeps
994 // QPACK/control bytes causally ahead of raw writes and makes each turn's
995 // work bounded even when the client is blocked.
996 std::size_t DriveApplicationOutput() {
997 std::size_t progress = 0;
998
999 if (pending_write_.active()) {
1000 const bool completes_action = pending_write_.completes_action;
1001 const WriteResult result = FlushPendingWrite(&progress);
1002 if (result == WriteResult::kBlocked) {
1003 return progress;
1004 }
1005 if (result == WriteResult::kFailed && transport_failed_) {
1006 return progress;
1007 }
1008 if (completes_action) {
1009 ++next_action_;
1010 return progress + 1;
1011 }
1012 if (result == WriteResult::kFailed && waiting_for_h3_drain_) {
1013 waiting_for_h3_drain_ = false;
1014 if (waiting_drain_completes_action_) {
1015 ++next_action_;
1016 }
1017 return progress + 1;
1018 }
1019 }
1020
1021 if (!server_streams_bound_) {
1022 return progress;
1023 }
1024 if (!bootstrap_drained_) {
1025 if (http3_failed_ || PumpNghttp3(&progress)) {
1026 bootstrap_drained_ = true;
1027 ++progress;
1028 }
1029 return progress;
1030 }
1031 if (waiting_for_h3_drain_) {
1032 if (http3_failed_ || PumpNghttp3(&progress)) {
1033 waiting_for_h3_drain_ = false;
1034 if (waiting_drain_completes_action_) {
1035 ++next_action_;
1036 }
1037 waiting_drain_completes_action_ = false;
1038 ++progress;
1039 }
1040 return progress;
1041 }
1042
1043 // Reading a request can produce QPACK decoder instructions independently
1044 // of a response action. Keep those ahead of script-controlled raw writes.
1045 if (!http3_failed_ && !PumpNghttp3(&progress)) {
1046 return progress;
1047 }
1048 if (transport_failed_) {
1049 return progress;
1050 }
1051
1052 const std::size_t action_count =
1053 scenario_ == nullptr
1054 ? 0
1055 : std::min<std::size_t>(scenario_limits::kMaxHttp3Actions, scenario_->http3_plan().actions_size());
1056 if (next_action_ >= action_count) {
1057 // An empty H3 plan still needs one successful response after a request.
1058 if (action_count == 0 && !default_response_started_ && request_headers_received_) {
1059 default_response_started_ = true;
1060 if (SubmitStructuredResponse(default_response_)) {
1061 waiting_for_h3_drain_ = true;
1062 waiting_drain_completes_action_ = false;
1063 ++progress;
1064 }
1065 }
1066 return progress;
1067 }
1068
1069 const auto& action = scenario_->http3_plan().actions(static_cast<int>(next_action_));
1070 if (action.has_structured_response()) {
1071 if (!request_headers_received_) {
1072 return progress;
1073 }
1074 if (SubmitStructuredResponse(action.structured_response())) {
1075 waiting_for_h3_drain_ = true;
1076 waiting_drain_completes_action_ = true;
1077 } else {
1078 ++next_action_;
1079 }
1080 return progress + 1;
1081 }
1082
1083 if (action.has_stream_write()) {
1084 const auto& write = action.stream_write();
1085 const std::int64_t stream_id = StreamForRole(write.role());
1086 if (stream_id == kNoStreamId) {
1087 return progress;
1088 }
1089 const std::size_t length = std::min<std::size_t>(scenario_limits::kMaxHttp3RawWriteBytes, write.data().size());
1090 QueueWrite(stream_id, write.data().substr(0, length), write.finish_stream(), false, true);
1091 return progress + 1;
1092 }
1093
1094 if (action.has_open_unidirectional_stream()) {
1095 const auto& open = action.open_unidirectional_stream();
1096 const std::int64_t stream_id = OpenUnidirectionalStream();
1097 if (stream_id == kNoStreamId) {
1098 ++next_action_;
1099 return progress + 1;
1100 }
1101 const std::size_t length = std::min<std::size_t>(scenario_limits::kMaxHttp3RawWriteBytes, open.data().size());
1102 QueueWrite(stream_id, open.data().substr(0, length), open.finish_stream(), false, true);
1103 return progress + 1;
1104 }
1105
1106 if (action.has_stream_reset()) {
1107 const auto& reset = action.stream_reset();
1108 const std::int64_t stream_id = StreamForRole(reset.role());
1109 if (stream_id == kNoStreamId) {
1110 return progress;
1111 }
1112 (void)ngtcp2_conn_shutdown_stream_write(quic_connection_, 0, stream_id,
1113 reset.application_error_code() & scenario_limits::kMaxQuicVarint);
1114 ++next_action_;
1115 return progress + 1;
1116 }
1117
1118 if (action.has_goaway()) {
1119 const std::int64_t control_stream_id = StreamForRole(curl::fuzzer::proto::HTTP3_STREAM_CONTROL);
1120 if (control_stream_id == kNoStreamId) {
1121 return progress;
1122 }
1123 // HTTP/3 GOAWAY is a raw control-stream frame so its identifier remains
1124 // directly mutation-controlled instead of being selected by nghttp3.
1125 std::string payload;
1126 AppendQuicVarint(&payload, action.goaway().id());
1127 std::string frame;
1128 AppendQuicVarint(&frame, 0x07U);
1129 AppendQuicVarint(&frame, payload.size());
1130 frame.append(payload);
1131 QueueWrite(control_stream_id, std::move(frame), false, false, true);
1132 return progress + 1;
1133 }
1134
1135 if (action.has_connection_close()) {
1136 close_error_code_ = action.connection_close().application_error_code() & scenario_limits::kMaxQuicVarint;
1137 close_requested_ = true;
1138 ++next_action_;
1139 return progress + 1;
1140 }
1141
1142 ++next_action_;
1143 return progress + 1;
1144 }
1145
1146 // Retain script and nghttp3 bytes until ngtcp2 accepts their full prefix.
1147 // Only one pending write exists, preserving ordering between action types.
1148 void QueueWrite(std::int64_t stream_id, std::string bytes, bool finish_stream, bool nghttp3_managed,
1149 bool completes_action) {
1150 retained_writes_.push_back(std::make_unique<std::string>(std::move(bytes)));
1151 pending_write_.stream_id = stream_id;
1152 pending_write_.bytes = retained_writes_.back().get();
1153 pending_write_.offset = 0;
1154 pending_write_.finish_stream = finish_stream;
1155 pending_write_.nghttp3_managed = nghttp3_managed;
1156 pending_write_.completes_action = completes_action;
1157 }
1158
1159 // Packetize one prefix through ngtcp2. For nghttp3-managed output, record
1160 // the exact submitted offsets so its acknowledgement accounting can exclude
1161 // interleaved raw writes on the same QUIC stream.
1162 WriteResult FlushPendingWrite(std::size_t* progress) {
1163 if (!pending_write_.active()) {
1164 return WriteResult::kComplete;
1165 }
1166
1167 if (pending_write_.bytes == nullptr || pending_write_.offset > pending_write_.bytes->size()) {
1168 pending_write_.Clear();
1169 return WriteResult::kFailed;
1170 }
1171
1172 const std::size_t remaining = pending_write_.bytes->size() - pending_write_.offset;
1173 if (remaining == 0 && !pending_write_.finish_stream) {
1174 pending_write_.Clear();
1175 ++*progress;
1176 return WriteResult::kComplete;
1177 }
1178
1179 ngtcp2_vec vector = {};
1180 vector.base =
1181 reinterpret_cast<std::uint8_t*>(const_cast<char*>(pending_write_.bytes->data() + pending_write_.offset));
1182 vector.len = remaining;
1183 const std::size_t vector_count = remaining == 0 ? 0 : 1;
1184 const std::uint32_t flags =
1185 pending_write_.finish_stream ? NGTCP2_WRITE_STREAM_FLAG_FIN : NGTCP2_WRITE_STREAM_FLAG_NONE;
1186 ngtcp2_path_storage output_path;
1187 ngtcp2_path_storage_zero(&output_path);
1188 ngtcp2_pkt_info packet_info = {};
1189 ngtcp2_ssize submitted = -1;
1190 const ngtcp2_tstamp now = QuicNow();
1191 const ngtcp2_ssize packet_length = ngtcp2_conn_writev_stream(
1192 quic_connection_, &output_path.path, &packet_info, packet_buffer_.data(), packet_buffer_.size(), &submitted,
1193 flags, pending_write_.stream_id, vector_count == 0 ? nullptr : &vector, vector_count, now);
1194 if (packet_length == NGTCP2_ERR_STREAM_DATA_BLOCKED) {
1195 return WriteResult::kBlocked;
1196 }
1197 if (packet_length == NGTCP2_ERR_STREAM_SHUT_WR || packet_length == NGTCP2_ERR_STREAM_NOT_FOUND) {
1198 pending_write_.Clear();
1199 return WriteResult::kFailed;
1200 }
1201 if (packet_length < 0) {
1202 transport_failed_ = true;
1203 pending_write_.Clear();
1204 return WriteResult::kFailed;
1205 }
1206 if (packet_length == 0) {
1207 return WriteResult::kBlocked;
1208 }
1209 ngtcp2_conn_update_pkt_tx_time(quic_connection_, now);
1210 (void)SendPacket(output_path.path, packet_buffer_.data(), static_cast<std::size_t>(packet_length));
1211 ++*progress;
1212 if (transport_failed_) {
1213 pending_write_.Clear();
1214 return WriteResult::kFailed;
1215 }
1216
1217 if (submitted >= 0) {
1218 StreamState* stream = FindStream(pending_write_.stream_id);
1219 if (stream == nullptr || static_cast<std::uint64_t>(submitted) > UINT64_MAX - stream->write_offset) {
1220 transport_failed_ = true;
1221 pending_write_.Clear();
1222 return WriteResult::kFailed;
1223 }
1224 const std::uint64_t write_begin = stream->write_offset;
1225 stream->write_offset += static_cast<std::uint64_t>(submitted);
1226 if (pending_write_.nghttp3_managed) {
1227 if (nghttp3_conn_add_write_offset(http3_connection_, pending_write_.stream_id,
1228 static_cast<std::uint64_t>(submitted)) != 0) {
1229 http3_failed_ = true;
1230 pending_write_.Clear();
1231 return WriteResult::kFailed;
1232 }
1233 if (submitted != 0) {
1234 stream->managed_write_ranges.push_back({write_begin, stream->write_offset});
1235 } else if (pending_write_.finish_stream && pending_write_.offset == pending_write_.bytes->size()) {
1236 stream->managed_fin_offsets.push_back(write_begin);
1237 }
1238 }
1239 pending_write_.offset += static_cast<std::size_t>(submitted);
1240 }
1241 if (submitted < 0 || pending_write_.offset != pending_write_.bytes->size()) {
1242 return WriteResult::kBlocked;
1243 }
1244 pending_write_.Clear();
1245 return WriteResult::kComplete;
1246 }
1247
1248 // Let nghttp3 serialize response/control bytes, then feed each generated
1249 // vector through the same QUIC write path as raw scenario data. Stop when a
1250 // raw action is pending so it cannot be reordered behind generated output.
1251 bool PumpNghttp3(std::size_t* progress) {
1252 if (http3_connection_ == nullptr || http3_failed_) {
1253 return true;
1254 }
1255
1256 for (std::size_t operation = 0; operation < kMaxWritesPerTurn; ++operation) {
1257 if (pending_write_.active()) {
1258 if (!pending_write_.nghttp3_managed) {
1259 return false;
1260 }
1261 const WriteResult result = FlushPendingWrite(progress);
1262 if (result == WriteResult::kBlocked) {
1263 return false;
1264 }
1265 if (result == WriteResult::kFailed) {
1266 http3_failed_ = true;
1267 return true;
1268 }
1269 }
1270
1271 std::array<nghttp3_vec, 16> vectors;
1272 std::int64_t stream_id = -1;
1273 int finish_stream = 0;
1274 const nghttp3_ssize vector_count =
1275 nghttp3_conn_writev_stream(http3_connection_, &stream_id, &finish_stream, vectors.data(), vectors.size());
1276 if (vector_count < 0) {
1277 http3_failed_ = true;
1278 return true;
1279 }
1280 if (vector_count == 0 && stream_id == -1) {
1281 return true;
1282 }
1283
1284 StreamState* stream = FindStream(stream_id);
1285 if (stream == nullptr) {
1286 http3_failed_ = true;
1287 return true;
1288 }
1289
1290 std::string bytes;
1291 if (vector_count > 0) {
1292 std::size_t total = 0;
1293 for (nghttp3_ssize index = 0; index < vector_count; ++index) {
1294 if (vectors[static_cast<std::size_t>(index)].len > kMaxPendingPlaintextBytes - total) {
1295 http3_failed_ = true;
1296 return true;
1297 }
1298 total += vectors[static_cast<std::size_t>(index)].len;
1299 }
1300 bytes.reserve(total);
1301 for (nghttp3_ssize index = 0; index < vector_count; ++index) {
1302 const nghttp3_vec& vector = vectors[static_cast<std::size_t>(index)];
1303 bytes.append(reinterpret_cast<const char*>(vector.base), vector.len);
1304 }
1305 }
1306
1307 QueueWrite(stream->id, std::move(bytes), finish_stream != 0, true, false);
1308 }
1309 return false;
1310 }
1311
1312 // Convert a bounded schema response into nghttp3-owned fields and pull-based
1313 // body state. Interim responses do not consume the single final response.
1314 bool SubmitStructuredResponse(const curl::fuzzer::proto::Http3Response& response) {
1315 if (http3_connection_ == nullptr || http3_failed_ || role_stream_ids_[kResponseStream] == kNoStreamId) {
1316 return false;
1317 }
1318
1319 std::uint32_t status_code = response.status_code();
1320 if (status_code < 100U || status_code > 599U) {
1321 status_code = 100U + status_code % 500U;
1322 }
1323 std::vector<std::pair<std::string, std::string>> fields;
1324 fields.reserve(1 + std::min<std::size_t>(scenario_limits::kMaxHttp3Headers, response.response_headers_size()));
1325 fields.emplace_back(":status", std::to_string(status_code));
1326 AppendHeaders(&fields, response.response_headers(), scenario_limits::kMaxHttp3Headers);
1327 const std::vector<nghttp3_nv> name_values = MakeNameValues(fields);
1328 const std::int64_t response_stream_id = static_cast<std::int64_t>(role_stream_ids_[kResponseStream]);
1329
1330 if (status_code < 200U) {
1331 return nghttp3_conn_submit_info(http3_connection_, response_stream_id, name_values.data(), name_values.size()) ==
1332 0;
1333 }
1334 if (final_response_submitted_) {
1335 return false;
1336 }
1337
1338 response_body_.chunks.clear();
1339 response_body_.next_chunk = 0;
1340 response_body_.finish_stream = response.finish_stream();
1341 response_body_.has_trailers = response.response_trailers_size() != 0;
1342 std::size_t body_bytes = 0;
1343 const std::size_t body_chunks =
1344 std::min<std::size_t>(scenario_limits::kMaxHttp3BodyChunks, response.body_chunks_size());
1345 response_body_.chunks.reserve(body_chunks);
1346 for (std::size_t index = 0; index < body_chunks && body_bytes < scenario_limits::kMaxHttp3BodyBytes; ++index) {
1347 const std::string& source = response.body_chunks(static_cast<int>(index));
1348 const std::size_t length = std::min(source.size(), scenario_limits::kMaxHttp3BodyBytes - body_bytes);
1349 response_body_.chunks.emplace_back(source.data(), length);
1350 body_bytes += length;
1351 }
1352
1353 nghttp3_data_reader reader = {};
1354 reader.read_data = &ReadResponseBody;
1355 if (nghttp3_conn_submit_response(http3_connection_, response_stream_id, name_values.data(), name_values.size(),
1356 &reader) != 0) {
1357 return false;
1358 }
1359 final_response_submitted_ = true;
1360
1361 if (response_body_.has_trailers) {
1362 std::vector<std::pair<std::string, std::string>> trailers;
1363 trailers.reserve(std::min<std::size_t>(scenario_limits::kMaxHttp3Trailers, response.response_trailers_size()));
1364 AppendHeaders(&trailers, response.response_trailers(), scenario_limits::kMaxHttp3Trailers);
1365 const std::vector<nghttp3_nv> trailer_values = MakeNameValues(trailers);
1366 if (nghttp3_conn_submit_trailers(http3_connection_, response_stream_id, trailer_values.data(),
1367 trailer_values.size()) != 0) {
1368 http3_failed_ = true;
1369 }
1370 }
1371 return true;
1372 }
1373
1374 template <typename RepeatedHeaders>
1375 static void AppendHeaders(std::vector<std::pair<std::string, std::string>>* destination,
1376 const RepeatedHeaders& source, std::size_t count_limit) {
1377 std::size_t remaining = scenario_limits::kMaxHttp3HeaderBytes;
1378 const std::size_t count = std::min<std::size_t>(count_limit, source.size());
1379 for (std::size_t index = 0; index < count && remaining != 0; ++index) {
1380 std::string name = NormalizeHeaderName(source.Get(static_cast<int>(index)).name());
1381 std::string value = NormalizeHeaderValue(source.Get(static_cast<int>(index)).value());
1382 if (name.size() > remaining) {
1383 name.resize(remaining);
1384 value.clear();
1385 } else if (value.size() > remaining - name.size()) {
1386 value.resize(remaining - name.size());
1387 }
1388 remaining -= name.size() + value.size();
1389 destination->emplace_back(std::move(name), std::move(value));
1390 }
1391 }
1392
1393 static std::vector<nghttp3_nv> MakeNameValues(const std::vector<std::pair<std::string, std::string>>& fields) {
1394 std::vector<nghttp3_nv> name_values;
1395 name_values.reserve(fields.size());
1396 for (const auto& field : fields) {
1397 nghttp3_nv name_value = {};
1398 name_value.name = reinterpret_cast<std::uint8_t*>(const_cast<char*>(field.first.data()));
1399 name_value.value = reinterpret_cast<std::uint8_t*>(const_cast<char*>(field.second.data()));
1400 name_value.namelen = field.first.size();
1401 name_value.valuelen = field.second.size();
1402 name_value.flags = NGHTTP3_NV_FLAG_NONE;
1403 name_values.push_back(name_value);
1404 }
1405 return name_values;
1406 }
1407
1408 StreamState* FindStream(std::int64_t stream_id) {
1409 for (StreamState& stream : streams_) {
1410 if (stream.id == stream_id) {
1411 return &stream;
1412 }
1413 }
1414 return nullptr;
1415 }
1416
1417 std::int64_t StreamForRole(curl::fuzzer::proto::Http3StreamRole role) {
1418 const int role_value = static_cast<int>(role);
1419 if (role_value < 0 || role_value >= static_cast<int>(kStreamRoleCount)) {
1420 return kNoStreamId;
1421 }
1422 return role_stream_ids_[static_cast<std::size_t>(role_value)];
1423 }
1424
1425 // nghttp3 callbacks replenish QUIC flow-control credit only as application
1426 // bytes are consumed, then discover the first request stream for responses.
1427 static int ReceiveRequestData(nghttp3_conn* /*connection*/, std::int64_t stream_id, const std::uint8_t* /*data*/,
1428 std::size_t data_length, void* user_data, void* /*stream_user_data*/) {
1429 auto* self = static_cast<Http3MockServerImpl*>(user_data);
1430 if (self != nullptr && self->quic_connection_ != nullptr) {
1431 ngtcp2_conn_extend_max_stream_offset(self->quic_connection_, stream_id, data_length);
1432 ngtcp2_conn_extend_max_offset(self->quic_connection_, data_length);
1433 }
1434 return 0;
1435 }
1436
1437 static int DeferredConsumeRequestData(nghttp3_conn* /*connection*/, std::int64_t stream_id, std::size_t consumed,
1438 void* user_data, void* /*stream_user_data*/) {
1439 auto* self = static_cast<Http3MockServerImpl*>(user_data);
1440 if (self != nullptr && self->quic_connection_ != nullptr) {
1441 ngtcp2_conn_extend_max_stream_offset(self->quic_connection_, stream_id, consumed);
1442 ngtcp2_conn_extend_max_offset(self->quic_connection_, consumed);
1443 }
1444 return 0;
1445 }
1446
1447 static int EndRequestHeaders(nghttp3_conn* /*connection*/, std::int64_t stream_id, int /*finish_stream*/,
1448 void* user_data, void* /*stream_user_data*/) {
1449 auto* self = static_cast<Http3MockServerImpl*>(user_data);
1450 if (self != nullptr) {
1451 if (self->role_stream_ids_[kResponseStream] == kNoStreamId) {
1452 self->role_stream_ids_[kResponseStream] = stream_id;
1453 if (self->FindStream(stream_id) == nullptr && self->streams_.size() < kMaxStreams) {
1454 StreamState state;
1455 state.id = stream_id;
1456 self->streams_.push_back(state);
1457 }
1458 }
1459 if (self->role_stream_ids_[kResponseStream] == stream_id) {
1460 self->request_headers_received_ = true;
1461 }
1462 }
1463 return 0;
1464 }
1465
1466 static int EndRequestStream(nghttp3_conn* /*connection*/, std::int64_t /*stream_id*/, void* /*user_data*/,
1467 void* /*stream_user_data*/) {
1468 return 0;
1469 }
1470
1471 // nghttp3 pulls one schema body chunk per call. EOF may keep the stream open
1472 // for trailers or a deliberate no-FIN response.
1473 static nghttp3_ssize ReadResponseBody(nghttp3_conn* /*connection*/, std::int64_t /*stream_id*/, nghttp3_vec* vectors,
1474 std::size_t vector_count, std::uint32_t* flags, void* user_data,
1475 void* /*stream_user_data*/) {
1476 auto* self = static_cast<Http3MockServerImpl*>(user_data);
1477 if (self == nullptr || vectors == nullptr || vector_count == 0 || flags == nullptr) {
1478 return NGHTTP3_ERR_CALLBACK_FAILURE;
1479 }
1480
1481 while (self->response_body_.next_chunk < self->response_body_.chunks.size() &&
1482 self->response_body_.chunks[self->response_body_.next_chunk].empty()) {
1483 ++self->response_body_.next_chunk;
1484 }
1485 if (self->response_body_.next_chunk == self->response_body_.chunks.size()) {
1486 *flags = NGHTTP3_DATA_FLAG_EOF;
1487 if (!self->response_body_.finish_stream && !self->response_body_.has_trailers) {
1488 *flags |= NGHTTP3_DATA_FLAG_NO_END_STREAM;
1489 }
1490 return 0;
1491 }
1492
1493 const std::string& chunk = self->response_body_.chunks[self->response_body_.next_chunk++];
1494 vectors[0].base = reinterpret_cast<std::uint8_t*>(const_cast<char*>(chunk.data()));
1495 vectors[0].len = chunk.size();
1496 *flags = NGHTTP3_DATA_FLAG_NONE;
1497 if (self->response_body_.next_chunk == self->response_body_.chunks.size()) {
1498 *flags |= NGHTTP3_DATA_FLAG_EOF;
1499 if (!self->response_body_.finish_stream && !self->response_body_.has_trailers) {
1500 *flags |= NGHTTP3_DATA_FLAG_NO_END_STREAM;
1501 }
1502 }
1503 return 1;
1504 }
1505
1506 // Layer-owned transport state. The destruction order in ResetPeer mirrors
1507 // these dependencies: HTTP/3, QUIC, TLS adapter, then TLS context/socket.
1508 SSL_CTX* context_ = nullptr;
1509 ngtcp2_conn* quic_connection_ = nullptr;
1510 SSL* tls_connection_ = nullptr;
1511 ngtcp2_crypto_ossl_ctx* crypto_context_ = nullptr;
1512 ngtcp2_crypto_conn_ref crypto_connection_ref_;
1513 nghttp3_conn* http3_connection_ = nullptr;
1514 const curl::fuzzer::proto::Scenario* scenario_ = nullptr;
1515 std::vector<StreamState> streams_;
1516 std::vector<std::unique_ptr<std::string>> retained_writes_;
1517 std::array<std::int64_t, kStreamRoleCount> role_stream_ids_;
1518 PendingWrite pending_write_;
1519 ResponseBodyState response_body_;
1520 curl::fuzzer::proto::Http3Response default_response_;
1521 ngtcp2_path_storage path_storage_ = {};
1522 struct sockaddr_in local_address_ = {};
1523 struct sockaddr_in remote_address_ = {};
1524 std::array<std::uint8_t, kReceiveBufferBytes> receive_buffer_ = {};
1525 std::array<std::uint8_t, NGTCP2_MAX_UDP_PAYLOAD_SIZE> packet_buffer_ = {};
1526 std::uint64_t random_counter_ = 0;
1527 std::uint64_t close_error_code_ = 0;
1528 std::size_t next_action_ = 0;
1529 int server_fd_ = -1;
1530 std::uint16_t server_port_ = 0;
1531 bool socket_opened_ = false;
1532 bool handshake_complete_ = false;
1533 bool request_headers_received_ = false;
1534 bool server_streams_bound_ = false;
1535 bool bootstrap_drained_ = false;
1536 bool waiting_for_h3_drain_ = false;
1537 bool waiting_drain_completes_action_ = false;
1538 bool final_response_submitted_ = false;
1539 bool default_response_started_ = false;
1540 bool http3_failed_ = false;
1541 bool transport_failed_ = false;
1542 bool close_requested_ = false;
1543 bool close_sent_ = false;
1544};
1545
1547Http3MockServer::Http3MockServer() : Http3MockServer(curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_DEFAULT_EC) {}
1548
1551Http3MockServer::Http3MockServer(curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain)
1552 : MockServerBase(MultiDrivePolicy::kSocketAction),
1553 impl_(std::make_unique<Http3MockServerImpl>(certificate_chain)) {}
1554
1555Http3MockServer::~Http3MockServer() = default;
1556
1561 // Curl trusts the checked-in server certificate directly and must use HTTP/3
1562 // rather than silently falling back to an HTTP/1 or HTTP/2 code path.
1564 struct curl_blob trust_anchor = {const_cast<char*>(tls_test_credentials::kCertificatePem),
1565 sizeof(tls_test_credentials::kCertificatePem) - 1, CURL_BLOB_NOCOPY};
1566 (void)curl_easy_setopt(easy, CURLOPT_CAINFO_BLOB, &trust_anchor);
1567 (void)curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L);
1568 (void)curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L);
1569 (void)curl_easy_setopt(easy, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_3ONLY);
1570}
1571
1573bool Http3MockServer::handshake_complete() const { return impl_ != nullptr && impl_->handshake_complete(); }
1574
1576bool Http3MockServer::request_headers_received() const { return impl_ != nullptr && impl_->request_headers_received(); }
1577
1580 return impl_ == nullptr ? 0 : impl_->executed_action_count();
1581}
1582
1584std::uint16_t Http3MockServer::server_port() const { return impl_ == nullptr ? 0 : impl_->server_port(); }
1585
1591curl_socket_t Http3MockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* address) {
1592 return impl_ == nullptr ? CURL_SOCKET_BAD : impl_->OpenSocket(purpose, address);
1593}
1594
1601 curlsocktype /*purpose*/) const {
1602 return SocketSetupDisposition::kNeedsSetup;
1603}
1604
1610void Http3MockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
1611 (void)easy;
1612 if (impl_ == nullptr) {
1613 return;
1614 }
1615 impl_->BeginScenario(scenario);
1616
1617 int still_running = 1;
1618 int idle_iterations = 0;
1619 if (MultiSocketDriver* driver = multi_socket_driver(); driver != nullptr) {
1620 // Alternate curl readiness processing with one bounded peer turn. Neither
1621 // side blocks, so an idle limit prevents a malformed case spinning forever.
1622 CURLMcode result = driver->Start(&still_running);
1623 for (int iteration = 0; result == CURLM_OK && iteration < kMaxDriveIterations; ++iteration) {
1624 std::size_t progress = impl_->DrivePeerTurn();
1625 // Curl can queue the final ACK or reset while completing the transfer.
1626 // Give the peer its next bounded turn before stopping, without driving
1627 // curl again or extending the existing operation budget.
1628 if (still_running == 0) {
1629 break;
1630 }
1631 const MultiSocketDriver::DriveResult drive_result = driver->DriveReady(&still_running);
1632 result = drive_result.code;
1633 progress += drive_result.made_progress ? 1U : 0U;
1634 if (progress != 0) {
1635 idle_iterations = 0;
1636 } else if (++idle_iterations >= kHttp3IdleIterations) {
1637 break;
1638 }
1639 }
1640 return;
1641 }
1642
1643 // The fallback exercises curl's public multi-perform API while preserving
1644 // the same peer-first/idle-limit behavior as the socket-action driver.
1645 for (int iteration = 0; iteration < kMaxDriveIterations && still_running != 0; ++iteration) {
1646 const int running_before = still_running;
1647 const CURLMcode result = curl_multi_perform(multi, &still_running);
1648 if (result != CURLM_OK) {
1649 break;
1650 }
1651
1652 std::size_t progress = still_running == running_before ? 0U : 1U;
1653 progress += impl_->DrivePeerTurn();
1654 if (progress != 0) {
1655 idle_iterations = 0;
1656 } else if (++idle_iterations >= kHttp3IdleIterations) {
1657 break;
1658 }
1659 }
1660}
1661
1664#if defined(PROTO_FUZZER_OBSERVE_STATE)
1666 const volatile std::size_t http3_observation[] = {handshake_complete(), request_headers_received(),
1668 (void)http3_observation;
1669#endif
1670}
1671
1672} // namespace proto_fuzzer
Http3MockServer()
Construct a peer with the default checked-in EC certificate.
void RunLoop(CURLM *multi, CURL *easy, const curl::fuzzer::proto::Scenario &scenario) override
Alternate nonblocking curl and QUIC server turns under fixed operation and idle budgets.
void ObserveState() override
Retain protocol outcomes in coverage and introspector builds.
void Install(CURL *easy) override
Install UDP socket callbacks, the local trust anchor, and HTTP/3-only negotiation on an easy handle.
std::size_t executed_action_count() const
curl_socket_t HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr *address) override
Create curl's real UDP descriptor and rewrite its destination to the private loopback QUIC listener.
SocketSetupDisposition GetSocketSetupDisposition(curl_socket_t curlfd, curlsocktype purpose) const override
HTTP/3 returns an unconnected UDP descriptor, so curl must perform its normal socket setup before usi...
virtual void Install(CURL *easy)
Install the common OPENSOCKETFUNCTION / OPENSOCKETDATA / SOCKOPTFUNCTION callbacks on 'easy'.
virtual void ObserveState()
Default no-op for peers without protocol-specific observations.
static constexpr int kMaxDriveIterations
Hard operation budget for one scenario.
MultiSocketDriver * multi_socket_driver()
Expose the callback state installed for the current socket-action drive.
Owns the socket and timer callback state required by libcurl's event-driven multi API.
Bounded ngtcp2/OpenSSL QUIC peer for HTTP/3 scenarios.
SocketSetupDisposition
Tell curl whether an application-provided socket still needs its normal connect/options setup or alre...
MultiDrivePolicy
Fixed policy for interpreting a scenario's multi-drive fields.
@ kSocketAction
Use the callback-driven curl_multi_socket_action loop.
Bounded event-loop state for driving curl_multi_socket_action.
Shared complexity limits for structured scenarios.
constexpr std::uint64_t kMaxQuicVarint
QUIC variable-length integers reserve their two high bits for encoding.
constexpr std::size_t kMaxHttp3Actions
Ordered H3 work needs enough slots to interleave response fragments with stream and connection state ...
constexpr std::size_t kMaxHttp3HeaderValueBytes
Values use the shared metadata ceiling used by other header-bearing APIs.
constexpr std::size_t kMaxHttp3HeaderNameBytes
Header names are tokens rather than general metadata.
Result from one non-blocking event-loop turn.
bool made_progress
True when an action ran or callback/handle state changed.
CURLMcode code
libcurl's result for the last socket action in the turn.