/src/curl_fuzzer/proto_fuzzer/http3_mock_server.cc
Line | Count | Source |
1 | | /* |
2 | | * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al. |
3 | | * |
4 | | * SPDX-License-Identifier: curl |
5 | | */ |
6 | | |
7 | | /// @file |
8 | | /// @brief ngtcp2 QUIC transport and nghttp3 peer implementation. |
9 | | |
10 | | #include "proto_fuzzer/http3_mock_server.h" |
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 | | |
37 | | #include "proto_fuzzer/multi_socket_driver.h" |
38 | | #include "proto_fuzzer/scenario_limits.h" |
39 | | #include "proto_fuzzer/tls_test_credentials.h" |
40 | | |
41 | | namespace proto_fuzzer { |
42 | | |
43 | | namespace { |
44 | | |
45 | | constexpr std::int64_t kNoStreamId = -1; |
46 | | constexpr std::size_t kMaxDatagramsPerTurn = 64; |
47 | | constexpr std::size_t kServerConnectionIdBytes = 18; |
48 | | constexpr 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. |
51 | | constexpr std::size_t kMaxStreams = scenario_limits::kMaxHttp3Actions + 8; |
52 | | constexpr std::size_t kMaxWritesPerTurn = 64; |
53 | | constexpr std::size_t kMaxPendingPlaintextBytes = 64 * 1024; |
54 | | constexpr int kHttp3IdleIterations = 32; |
55 | | // QUIC's first two bits select one of these wire widths. The remaining bits |
56 | | // encode the integer itself. |
57 | | constexpr std::array<std::size_t, 4> kQuicVarintByteWidths = {1, 2, 4, 8}; |
58 | | constexpr unsigned int kQuicVarintTagBits = 2; |
59 | | constexpr unsigned int kQuicVarintTagShift = CHAR_BIT - kQuicVarintTagBits; |
60 | | static_assert(CHAR_BIT == 8, "QUIC varints require 8-bit bytes"); |
61 | | |
62 | | enum StreamRoleIndex : std::size_t { |
63 | | kResponseStream = 0, |
64 | | kControlStream = 1, |
65 | | kQpackEncoderStream = 2, |
66 | | kQpackDecoderStream = 3, |
67 | | kStreamRoleCount = 4, |
68 | | }; |
69 | | |
70 | | /// Keep server-side OpenSSL failures from changing error classification in |
71 | | /// curl's OpenSSL client, which executes on the same thread. |
72 | | class OpenSslErrorQueueGuard { |
73 | | public: |
74 | 164k | OpenSslErrorQueueGuard() { ERR_clear_error(); } |
75 | 164k | ~OpenSslErrorQueueGuard() { ERR_clear_error(); } |
76 | | |
77 | | OpenSslErrorQueueGuard(const OpenSslErrorQueueGuard&) = delete; |
78 | | OpenSslErrorQueueGuard& operator=(const OpenSslErrorQueueGuard&) = delete; |
79 | | }; |
80 | | |
81 | | /// Configure callback-created descriptors explicitly: curl's requested |
82 | | /// SOCK_NONBLOCK and SOCK_CLOEXEC flags do not constrain an application fd. |
83 | 13.6k | bool ConfigureSocket(int fd) { |
84 | 13.6k | if (fd < 0) { |
85 | 0 | return false; |
86 | 0 | } |
87 | 13.6k | const int descriptor_flags = ::fcntl(fd, F_GETFD, 0); |
88 | 13.6k | if (descriptor_flags < 0 || ::fcntl(fd, F_SETFD, descriptor_flags | FD_CLOEXEC) < 0) { |
89 | 0 | return false; |
90 | 0 | } |
91 | 13.6k | const int status_flags = ::fcntl(fd, F_GETFL, 0); |
92 | 13.6k | return status_flags >= 0 && ::fcntl(fd, F_SETFL, status_flags | O_NONBLOCK) == 0; |
93 | 13.6k | } |
94 | | |
95 | | /// Bind one private IPv4 UDP endpoint to an ephemeral loopback port. |
96 | 6.80k | int OpenLoopbackSocket(struct sockaddr_in* bound_address) { |
97 | 6.80k | if (bound_address == nullptr) { |
98 | 0 | return -1; |
99 | 0 | } |
100 | | |
101 | 6.80k | const int fd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); |
102 | 6.80k | if (fd < 0 || !ConfigureSocket(fd)) { |
103 | 0 | if (fd >= 0) { |
104 | 0 | (void)::close(fd); |
105 | 0 | } |
106 | 0 | return -1; |
107 | 0 | } |
108 | | |
109 | 6.80k | struct sockaddr_in requested = {}; |
110 | 6.80k | requested.sin_family = AF_INET; |
111 | 6.80k | requested.sin_port = htons(0); |
112 | 6.80k | requested.sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
113 | 6.80k | if (::bind(fd, reinterpret_cast<const struct sockaddr*>(&requested), sizeof(requested)) != 0) { |
114 | 0 | (void)::close(fd); |
115 | 0 | return -1; |
116 | 0 | } |
117 | | |
118 | 6.80k | socklen_t length = sizeof(*bound_address); |
119 | 6.80k | std::memset(bound_address, 0, sizeof(*bound_address)); |
120 | 6.80k | if (::getsockname(fd, reinterpret_cast<struct sockaddr*>(bound_address), &length) != 0 || |
121 | 6.80k | length != sizeof(*bound_address) || bound_address->sin_family != AF_INET) { |
122 | 0 | (void)::close(fd); |
123 | 0 | return -1; |
124 | 0 | } |
125 | 6.80k | return fd; |
126 | 6.80k | } |
127 | | |
128 | | /// Replace all destination metadata curl can retain with the private listener. |
129 | 6.80k | bool RewriteDestination(struct curl_sockaddr* address, const struct sockaddr_in& destination) { |
130 | 6.80k | if (address == nullptr || sizeof(destination) > sizeof(address->addr)) { |
131 | 0 | return false; |
132 | 0 | } |
133 | 6.80k | address->family = AF_INET; |
134 | 6.80k | address->socktype = SOCK_DGRAM; |
135 | 6.80k | address->protocol = IPPROTO_UDP; |
136 | 6.80k | address->addrlen = sizeof(destination); |
137 | 6.80k | std::memset(&address->addr, 0, sizeof(address->addr)); |
138 | 6.80k | std::memcpy(&address->addr, &destination, sizeof(destination)); |
139 | 6.80k | return true; |
140 | 6.80k | } |
141 | | |
142 | | /// Select only the final RFC HTTP/3 ALPN identifier. |
143 | | int SelectHttp3Alpn(SSL* /*ssl*/, const unsigned char** selected, unsigned char* selected_length, |
144 | 6.80k | const unsigned char* client_protocols, unsigned int client_protocols_length, void* /*userdata*/) { |
145 | 6.80k | static constexpr unsigned char server_protocols[] = {2, 'h', '3'}; |
146 | 6.80k | unsigned char* match = nullptr; |
147 | 6.80k | unsigned char match_length = 0; |
148 | 6.80k | const int result = SSL_select_next_proto(&match, &match_length, server_protocols, sizeof(server_protocols), |
149 | 6.80k | client_protocols, client_protocols_length); |
150 | 6.80k | if (result != OPENSSL_NPN_NEGOTIATED) { |
151 | 0 | return SSL_TLSEXT_ERR_ALERT_FATAL; |
152 | 0 | } |
153 | 6.80k | *selected = match; |
154 | 6.80k | *selected_length = match_length; |
155 | 6.80k | return SSL_TLSEXT_ERR_OK; |
156 | 6.80k | } |
157 | | |
158 | | /// Append one profile-selected certificate without transferring a malformed |
159 | | /// object into the context on parse failure. |
160 | 378 | bool AddExtraChainCertificate(SSL_CTX* context, const char* certificate_pem) { |
161 | 378 | BIO* certificate_bio = BIO_new_mem_buf(certificate_pem, -1); |
162 | 378 | if (certificate_bio == nullptr) { |
163 | 0 | return false; |
164 | 0 | } |
165 | 378 | X509* certificate = PEM_read_bio_X509(certificate_bio, nullptr, nullptr, nullptr); |
166 | 378 | BIO_free(certificate_bio); |
167 | 378 | if (certificate == nullptr) { |
168 | 0 | return false; |
169 | 0 | } |
170 | 378 | if (SSL_CTX_add_extra_chain_cert(context, certificate) != 1) { |
171 | 0 | X509_free(certificate); |
172 | 0 | return false; |
173 | 0 | } |
174 | 378 | return true; |
175 | 378 | } |
176 | | |
177 | | /// Parse the checked-in key and profile-selected chain entirely in memory. |
178 | 7.04k | bool LoadCredentials(SSL_CTX* context, curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain) { |
179 | 7.04k | BIO* certificate_bio = BIO_new_mem_buf(tls_test_credentials::kCertificatePem, -1); |
180 | 7.04k | BIO* key_bio = BIO_new_mem_buf(tls_test_credentials::kPrivateKeyPem, -1); |
181 | 7.04k | if (certificate_bio == nullptr || key_bio == nullptr) { |
182 | 0 | BIO_free(certificate_bio); |
183 | 0 | BIO_free(key_bio); |
184 | 0 | return false; |
185 | 0 | } |
186 | | |
187 | 7.04k | X509* certificate = PEM_read_bio_X509(certificate_bio, nullptr, nullptr, nullptr); |
188 | 7.04k | EVP_PKEY* key = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, nullptr); |
189 | 7.04k | BIO_free(certificate_bio); |
190 | 7.04k | BIO_free(key_bio); |
191 | 7.04k | if (certificate == nullptr || key == nullptr) { |
192 | 0 | X509_free(certificate); |
193 | 0 | EVP_PKEY_free(key); |
194 | 0 | return false; |
195 | 0 | } |
196 | | |
197 | 7.04k | const bool loaded = SSL_CTX_use_certificate(context, certificate) == 1 && SSL_CTX_use_PrivateKey(context, key) == 1 && |
198 | 7.04k | SSL_CTX_check_private_key(context) == 1; |
199 | 7.04k | X509_free(certificate); |
200 | 7.04k | EVP_PKEY_free(key); |
201 | 7.04k | if (!loaded) { |
202 | 0 | return false; |
203 | 0 | } |
204 | | |
205 | 7.04k | if (certificate_chain == curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_ALL_KEY_TYPES) { |
206 | 126 | return AddExtraChainCertificate(context, tls_test_credentials::kRsaCertificatePem) && |
207 | 126 | AddExtraChainCertificate(context, tls_test_credentials::kDsaCertificatePem) && |
208 | 126 | AddExtraChainCertificate(context, tls_test_credentials::kDhCertificatePem); |
209 | 126 | } |
210 | 6.91k | return true; |
211 | 7.04k | } |
212 | | |
213 | | /// Create a TLS 1.3 context for ngtcp2's OpenSSL crypto adapter. |
214 | 7.04k | SSL_CTX* CreateTlsContext(curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain) { |
215 | 7.04k | OpenSslErrorQueueGuard error_guard; |
216 | 7.04k | SSL_CTX* context = SSL_CTX_new(TLS_server_method()); |
217 | 7.04k | if (context == nullptr || !LoadCredentials(context, certificate_chain)) { |
218 | 0 | SSL_CTX_free(context); |
219 | 0 | return nullptr; |
220 | 0 | } |
221 | | |
222 | 7.04k | if (SSL_CTX_set_min_proto_version(context, TLS1_3_VERSION) != 1 || |
223 | 7.04k | SSL_CTX_set_max_proto_version(context, TLS1_3_VERSION) != 1) { |
224 | 0 | SSL_CTX_free(context); |
225 | 0 | return nullptr; |
226 | 0 | } |
227 | 7.04k | (void)SSL_CTX_set_options(context, SSL_OP_NO_COMPRESSION); |
228 | 7.04k | SSL_CTX_set_alpn_select_cb(context, &SelectHttp3Alpn, nullptr); |
229 | 7.04k | return context; |
230 | 7.04k | } |
231 | | |
232 | | /// Encode one QUIC variable-length integer in its shortest representation. |
233 | 3.81k | void AppendQuicVarint(std::string* output, std::uint64_t value) { |
234 | 3.81k | value &= scenario_limits::kMaxQuicVarint; |
235 | 3.84k | for (std::size_t encoding = 0; encoding < kQuicVarintByteWidths.size(); ++encoding) { |
236 | 3.84k | const std::size_t byte_width = kQuicVarintByteWidths[encoding]; |
237 | 3.84k | const unsigned int value_bits = static_cast<unsigned int>(byte_width * CHAR_BIT - kQuicVarintTagBits); |
238 | 3.84k | if (value >= (std::uint64_t{1} << value_bits)) { |
239 | 35 | continue; |
240 | 35 | } |
241 | | |
242 | 7.69k | for (std::size_t byte = byte_width; byte != 0; --byte) { |
243 | 3.88k | const unsigned int shift = static_cast<unsigned int>((byte - 1) * CHAR_BIT); |
244 | 3.88k | unsigned char encoded_byte = static_cast<unsigned char>(value >> shift); |
245 | 3.88k | if (byte == byte_width) { |
246 | 3.81k | encoded_byte |= static_cast<unsigned char>(encoding << kQuicVarintTagShift); |
247 | 3.81k | } |
248 | 3.88k | output->push_back(static_cast<char>(encoded_byte)); |
249 | 3.88k | } |
250 | 3.81k | return; |
251 | 3.84k | } |
252 | 3.81k | } |
253 | | |
254 | | /// Normalize a header name again at the runtime boundary. Fixed H3 targets |
255 | | /// already run the postprocessor; this also keeps direct unit-test scenarios |
256 | | /// safe to pass to nghttp3. |
257 | 1.50k | std::string NormalizeHeaderName(const std::string& source) { |
258 | 1.50k | std::string result = source.substr(0, scenario_limits::kMaxHttp3HeaderNameBytes); |
259 | 1.50k | if (result.empty()) { |
260 | 0 | return "x-fuzz"; |
261 | 0 | } |
262 | 29.8k | for (char& byte : result) { |
263 | 29.8k | const unsigned char value = static_cast<unsigned char>(byte); |
264 | 29.8k | const bool lower = value >= 'a' && value <= 'z'; |
265 | 29.8k | const bool upper = value >= 'A' && value <= 'Z'; |
266 | 29.8k | const bool digit = value >= '0' && value <= '9'; |
267 | 29.8k | const bool punctuation = value == '!' || value == '#' || value == '$' || value == '%' || value == '&' || |
268 | 24.4k | value == '\'' || value == '*' || value == '+' || value == '-' || value == '.' || |
269 | 13.8k | value == '^' || value == '_' || value == '`' || value == '|' || value == '~'; |
270 | 29.8k | if (upper) { |
271 | 0 | byte = static_cast<char>(value - 'A' + 'a'); |
272 | 29.8k | } else if (!lower && !digit && !punctuation) { |
273 | 0 | byte = '-'; |
274 | 0 | } |
275 | 29.8k | } |
276 | 1.50k | return result; |
277 | 1.50k | } |
278 | | |
279 | | /// Remove control bytes that would make a structured field value malformed. |
280 | 1.50k | std::string NormalizeHeaderValue(const std::string& source) { |
281 | 1.50k | std::string result = source.substr(0, scenario_limits::kMaxHttp3HeaderValueBytes); |
282 | 93.9k | for (char& byte : result) { |
283 | 93.9k | const unsigned char value = static_cast<unsigned char>(byte); |
284 | 93.9k | if ((value < 0x20U && value != '\t') || value == 0x7fU) { |
285 | 0 | byte = ' '; |
286 | 0 | } |
287 | 93.9k | } |
288 | 1.50k | return result; |
289 | 1.50k | } |
290 | | |
291 | | /// Return ngtcp2's nanosecond-resolution monotonic timestamp. |
292 | 369k | ngtcp2_tstamp QuicNow() { |
293 | 369k | const auto now = std::chrono::steady_clock::now().time_since_epoch(); |
294 | 369k | return static_cast<ngtcp2_tstamp>(std::chrono::duration_cast<std::chrono::nanoseconds>(now).count()); |
295 | 369k | } |
296 | | |
297 | | } // namespace |
298 | | |
299 | | /// Private transport and HTTP/3 state. Keeping ngtcp2, OpenSSL, and nghttp3 |
300 | | /// out of the public header lets other proto targets compile without their |
301 | | /// include trees. |
302 | | class Http3MockServerImpl { |
303 | | public: |
304 | | explicit Http3MockServerImpl(curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain) |
305 | 7.04k | : context_(CreateTlsContext(certificate_chain)), crypto_connection_ref_{&GetQuicConnection, this} { |
306 | 7.04k | role_stream_ids_.fill(kNoStreamId); |
307 | 7.04k | streams_.reserve(kMaxStreams); |
308 | 7.04k | retained_writes_.reserve(kMaxWritesPerTurn); |
309 | 7.04k | } |
310 | | |
311 | 7.04k | ~Http3MockServerImpl() { |
312 | 7.04k | ResetPeer(); |
313 | 7.04k | OpenSslErrorQueueGuard error_guard; |
314 | 7.04k | SSL_CTX_free(context_); |
315 | 7.04k | } |
316 | | |
317 | | Http3MockServerImpl(const Http3MockServerImpl&) = delete; |
318 | | Http3MockServerImpl& operator=(const Http3MockServerImpl&) = delete; |
319 | | |
320 | 6.80k | curl_socket_t OpenSocket(curlsocktype purpose, struct curl_sockaddr* address) { |
321 | 6.80k | if (purpose != CURLSOCKTYPE_IPCXN || address == nullptr || socket_opened_ || context_ == nullptr) { |
322 | 2 | return CURL_SOCKET_BAD; |
323 | 2 | } |
324 | 6.80k | socket_opened_ = true; |
325 | | |
326 | 6.80k | struct sockaddr_in listener_address = {}; |
327 | 6.80k | server_fd_ = OpenLoopbackSocket(&listener_address); |
328 | 6.80k | if (server_fd_ < 0 || !RewriteDestination(address, listener_address)) { |
329 | 0 | ResetPeer(); |
330 | 0 | return CURL_SOCKET_BAD; |
331 | 0 | } |
332 | 6.80k | server_port_ = ntohs(listener_address.sin_port); |
333 | 6.80k | local_address_ = listener_address; |
334 | | |
335 | 6.80k | const int client_fd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); |
336 | 6.80k | if (client_fd < 0 || !ConfigureSocket(client_fd)) { |
337 | 0 | if (client_fd >= 0) { |
338 | 0 | (void)::close(client_fd); |
339 | 0 | } |
340 | 0 | ResetPeer(); |
341 | 0 | return CURL_SOCKET_BAD; |
342 | 0 | } |
343 | 6.80k | return static_cast<curl_socket_t>(client_fd); |
344 | 6.80k | } |
345 | | |
346 | 7.04k | void BeginScenario(const curl::fuzzer::proto::Scenario& scenario) { |
347 | 7.04k | ResetPeer(); |
348 | 7.04k | scenario_ = &scenario; |
349 | 7.04k | default_response_.Clear(); |
350 | 7.04k | default_response_.set_status_code(200); |
351 | 7.04k | default_response_.set_finish_stream(true); |
352 | 7.04k | } |
353 | | |
354 | 137k | std::size_t DrivePeerTurn() { |
355 | 137k | if (server_fd_ < 0 || transport_failed_) { |
356 | 832 | return 0; |
357 | 832 | } |
358 | | |
359 | 136k | OpenSslErrorQueueGuard error_guard; |
360 | 136k | std::size_t progress = HandleExpiry(); |
361 | 136k | if (transport_failed_) { |
362 | 0 | return progress; |
363 | 0 | } |
364 | 136k | progress += ReadDatagrams(); |
365 | 136k | if (quic_connection_ == nullptr || transport_failed_) { |
366 | 26 | return progress; |
367 | 26 | } |
368 | | |
369 | 136k | if (handshake_complete_ && !close_requested_ && !close_sent_) { |
370 | 122k | progress += CreateServerStreams(); |
371 | 122k | progress += DriveApplicationOutput(); |
372 | 122k | } |
373 | 136k | progress += FlushTransportPackets(); |
374 | 136k | return progress; |
375 | 136k | } |
376 | | |
377 | 0 | bool handshake_complete() const { return handshake_complete_; } |
378 | | |
379 | 0 | bool request_headers_received() const { return request_headers_received_; } |
380 | | |
381 | 0 | std::size_t executed_action_count() const { return next_action_; } |
382 | | |
383 | 0 | 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 | 288k | bool active() const { return stream_id != kNoStreamId; } |
414 | | |
415 | 38.7k | void Clear() { |
416 | 38.7k | stream_id = kNoStreamId; |
417 | 38.7k | bytes = nullptr; |
418 | 38.7k | offset = 0; |
419 | 38.7k | finish_stream = false; |
420 | 38.7k | nghttp3_managed = false; |
421 | 38.7k | completes_action = false; |
422 | 38.7k | } |
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 | 14.0k | void ResetPeer() { |
443 | 14.0k | OpenSslErrorQueueGuard error_guard; |
444 | 14.0k | pending_write_.Clear(); |
445 | 14.0k | nghttp3_conn_del(http3_connection_); |
446 | 14.0k | http3_connection_ = nullptr; |
447 | 14.0k | ngtcp2_conn_del(quic_connection_); |
448 | 14.0k | quic_connection_ = nullptr; |
449 | 14.0k | if (tls_connection_ != nullptr) { |
450 | 6.80k | SSL_set_app_data(tls_connection_, nullptr); |
451 | 6.80k | SSL_free(tls_connection_); |
452 | 6.80k | tls_connection_ = nullptr; |
453 | 6.80k | } |
454 | 14.0k | ngtcp2_crypto_ossl_ctx_del(crypto_context_); |
455 | 14.0k | crypto_context_ = nullptr; |
456 | 14.0k | streams_.clear(); |
457 | 14.0k | retained_writes_.clear(); |
458 | 14.0k | if (server_fd_ >= 0) { |
459 | 6.80k | (void)::close(server_fd_); |
460 | 6.80k | server_fd_ = -1; |
461 | 6.80k | } |
462 | | |
463 | 14.0k | scenario_ = nullptr; |
464 | 14.0k | role_stream_ids_.fill(kNoStreamId); |
465 | 14.0k | std::memset(&local_address_, 0, sizeof(local_address_)); |
466 | 14.0k | std::memset(&remote_address_, 0, sizeof(remote_address_)); |
467 | 14.0k | ngtcp2_path_storage_zero(&path_storage_); |
468 | 14.0k | response_body_.chunks.clear(); |
469 | 14.0k | response_body_.next_chunk = 0; |
470 | 14.0k | response_body_.finish_stream = false; |
471 | 14.0k | response_body_.has_trailers = false; |
472 | 14.0k | next_action_ = 0; |
473 | 14.0k | server_port_ = 0; |
474 | 14.0k | socket_opened_ = false; |
475 | 14.0k | handshake_complete_ = false; |
476 | 14.0k | request_headers_received_ = false; |
477 | 14.0k | server_streams_bound_ = false; |
478 | 14.0k | bootstrap_drained_ = false; |
479 | 14.0k | waiting_for_h3_drain_ = false; |
480 | 14.0k | waiting_drain_completes_action_ = false; |
481 | 14.0k | final_response_submitted_ = false; |
482 | 14.0k | default_response_started_ = false; |
483 | 14.0k | http3_failed_ = false; |
484 | 14.0k | transport_failed_ = false; |
485 | 14.0k | close_requested_ = false; |
486 | 14.0k | close_sent_ = false; |
487 | 14.0k | close_error_code_ = 0; |
488 | 14.0k | random_counter_ = 0; |
489 | 14.0k | } |
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 | 306k | static ngtcp2_conn* GetQuicConnection(ngtcp2_crypto_conn_ref* connection_ref) { |
495 | 306k | auto* self = static_cast<Http3MockServerImpl*>(connection_ref->user_data); |
496 | 306k | return self == nullptr ? nullptr : self->quic_connection_; |
497 | 306k | } |
498 | | |
499 | 6.71k | static int HandshakeCompleted(ngtcp2_conn* /*connection*/, void* user_data) { |
500 | 6.71k | auto* self = static_cast<Http3MockServerImpl*>(user_data); |
501 | 6.71k | if (self == nullptr) { |
502 | 0 | return NGTCP2_ERR_CALLBACK_FAILURE; |
503 | 0 | } |
504 | 6.71k | self->handshake_complete_ = true; |
505 | 6.71k | return 0; |
506 | 6.71k | } |
507 | | |
508 | 13.6k | static int ReceiveTransmitKey(ngtcp2_conn* /*connection*/, ngtcp2_encryption_level level, void* user_data) { |
509 | 13.6k | auto* self = static_cast<Http3MockServerImpl*>(user_data); |
510 | 13.6k | if (self == nullptr) { |
511 | 0 | return NGTCP2_ERR_CALLBACK_FAILURE; |
512 | 0 | } |
513 | 13.6k | if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT || self->server_streams_bound_) { |
514 | 6.80k | return 0; |
515 | 6.80k | } |
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 | 6.80k | if (!self->CreateHttp3Connection()) { |
519 | 0 | return NGTCP2_ERR_CALLBACK_FAILURE; |
520 | 0 | } |
521 | 6.80k | (void)self->CreateServerStreams(); |
522 | 6.80k | return self->server_streams_bound_ ? 0 : NGTCP2_ERR_CALLBACK_FAILURE; |
523 | 6.80k | } |
524 | | |
525 | | static void GenerateRandomBytes(std::uint8_t* destination, std::size_t length, |
526 | 27.2k | const ngtcp2_rand_ctx* random_context) { |
527 | 27.2k | if (random_context == nullptr) { |
528 | 0 | std::memset(destination, 0, length); |
529 | 0 | return; |
530 | 0 | } |
531 | 27.2k | auto* self = static_cast<Http3MockServerImpl*>(random_context->native_handle); |
532 | 27.2k | if (self == nullptr) { |
533 | 0 | std::memset(destination, 0, length); |
534 | 0 | return; |
535 | 0 | } |
536 | 27.2k | self->FillRandom(destination, length); |
537 | 27.2k | } |
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 | 6.80k | void* user_data) { |
542 | 6.80k | auto* self = static_cast<Http3MockServerImpl*>(user_data); |
543 | 6.80k | if (self == nullptr || connection_id == nullptr || reset_token == nullptr || |
544 | 6.80k | connection_id_length > sizeof(connection_id->data)) { |
545 | 0 | return NGTCP2_ERR_CALLBACK_FAILURE; |
546 | 0 | } |
547 | 6.80k | connection_id->datalen = connection_id_length; |
548 | 6.80k | self->FillRandom(connection_id->data, connection_id_length); |
549 | 6.80k | self->FillRandom(reset_token->data, sizeof(reset_token->data)); |
550 | 6.80k | return 0; |
551 | 6.80k | } |
552 | | |
553 | 26.8k | static int OpenRemoteStream(ngtcp2_conn* /*connection*/, std::int64_t stream_id, void* user_data) { |
554 | 26.8k | auto* self = static_cast<Http3MockServerImpl*>(user_data); |
555 | 26.8k | if (self == nullptr || self->streams_.size() >= kMaxStreams) { |
556 | 0 | return NGTCP2_ERR_CALLBACK_FAILURE; |
557 | 0 | } |
558 | 26.8k | StreamState state; |
559 | 26.8k | state.id = stream_id; |
560 | 26.8k | self->streams_.push_back(state); |
561 | 26.8k | if (self->role_stream_ids_[kResponseStream] == kNoStreamId && ngtcp2_is_bidi_stream(stream_id)) { |
562 | 6.71k | self->role_stream_ids_[kResponseStream] = stream_id; |
563 | 6.71k | } |
564 | 26.8k | return 0; |
565 | 26.8k | } |
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 | 28.9k | void* user_data, void* /*stream_user_data*/) { |
573 | 28.9k | auto* self = static_cast<Http3MockServerImpl*>(user_data); |
574 | 28.9k | if (self == nullptr || self->http3_connection_ == nullptr || self->http3_failed_) { |
575 | 0 | return NGTCP2_ERR_CALLBACK_FAILURE; |
576 | 0 | } |
577 | 28.9k | const nghttp3_ssize consumed = |
578 | 28.9k | nghttp3_conn_read_stream2(self->http3_connection_, stream_id, data, length, flags & NGTCP2_STREAM_DATA_FLAG_FIN, |
579 | 28.9k | ngtcp2_conn_get_timestamp(connection)); |
580 | 28.9k | if (consumed < 0) { |
581 | 26 | self->http3_failed_ = true; |
582 | 26 | return NGTCP2_ERR_CALLBACK_FAILURE; |
583 | 26 | } |
584 | 28.9k | ngtcp2_conn_extend_max_stream_offset(connection, stream_id, static_cast<std::uint64_t>(consumed)); |
585 | 28.9k | ngtcp2_conn_extend_max_offset(connection, static_cast<std::uint64_t>(consumed)); |
586 | 28.9k | return 0; |
587 | 28.9k | } |
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 | 20.9k | std::uint64_t length, void* user_data, void* /*stream_user_data*/) { |
594 | 20.9k | auto* self = static_cast<Http3MockServerImpl*>(user_data); |
595 | 20.9k | if (self == nullptr || self->http3_connection_ == nullptr || self->http3_failed_) { |
596 | 0 | return 0; |
597 | 0 | } |
598 | | |
599 | 20.9k | StreamState* stream = self->FindStream(stream_id); |
600 | 20.9k | if (stream == nullptr || offset > UINT64_MAX - length) { |
601 | 0 | return NGTCP2_ERR_CALLBACK_FAILURE; |
602 | 0 | } |
603 | | |
604 | 20.9k | if (length == 0) { |
605 | 6 | if (stream->next_managed_fin_offset < stream->managed_fin_offsets.size() && |
606 | 0 | stream->managed_fin_offsets[stream->next_managed_fin_offset] == offset) { |
607 | 0 | ++stream->next_managed_fin_offset; |
608 | 0 | if (nghttp3_conn_add_ack_offset(self->http3_connection_, stream_id, 0) != 0) { |
609 | 0 | self->http3_failed_ = true; |
610 | 0 | return NGTCP2_ERR_CALLBACK_FAILURE; |
611 | 0 | } |
612 | 0 | } |
613 | 6 | return 0; |
614 | 6 | } |
615 | | |
616 | 20.9k | const std::uint64_t end = offset + length; |
617 | 20.9k | std::uint64_t managed_length = 0; |
618 | 41.4k | for (std::size_t index = stream->next_managed_ack_range; index < stream->managed_write_ranges.size(); ++index) { |
619 | 20.7k | const StreamState::ManagedWriteRange& range = stream->managed_write_ranges[index]; |
620 | 20.7k | if (range.end <= offset) { |
621 | 0 | stream->next_managed_ack_range = index + 1; |
622 | 0 | continue; |
623 | 0 | } |
624 | 20.7k | if (range.begin >= end) { |
625 | 234 | break; |
626 | 234 | } |
627 | 20.5k | managed_length += std::min(range.end, end) - std::max(range.begin, offset); |
628 | 20.5k | if (range.end <= end) { |
629 | 20.5k | stream->next_managed_ack_range = index + 1; |
630 | 20.5k | } |
631 | 20.5k | } |
632 | | |
633 | 20.9k | if (managed_length != 0 && nghttp3_conn_add_ack_offset(self->http3_connection_, stream_id, managed_length) != 0) { |
634 | 0 | self->http3_failed_ = true; |
635 | 0 | return NGTCP2_ERR_CALLBACK_FAILURE; |
636 | 0 | } |
637 | 20.9k | return 0; |
638 | 20.9k | } |
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 | 131 | void* /*stream_user_data*/) { |
645 | 131 | auto* self = static_cast<Http3MockServerImpl*>(user_data); |
646 | 131 | if (self == nullptr || self->http3_connection_ == nullptr || self->http3_failed_) { |
647 | 0 | return 0; |
648 | 0 | } |
649 | 131 | std::uint32_t http3_flags = NGHTTP3_STREAM_CLOSE_FLAG_NONE; |
650 | 131 | if ((flags & NGTCP2_STREAM_CLOSE2_FLAG_RX_APP_ERROR_CODE_SET) != 0) { |
651 | 0 | http3_flags |= NGHTTP3_STREAM_CLOSE_FLAG_RX_APP_ERROR_CODE_SET; |
652 | 0 | } |
653 | 131 | if ((flags & NGTCP2_STREAM_CLOSE2_FLAG_TX_APP_ERROR_CODE_SET) != 0) { |
654 | 112 | http3_flags |= NGHTTP3_STREAM_CLOSE_FLAG_TX_APP_ERROR_CODE_SET; |
655 | 112 | } |
656 | 131 | const int result = nghttp3_conn_close_stream2(self->http3_connection_, http3_flags, stream_id, receive_error_code, |
657 | 131 | transmit_error_code); |
658 | 131 | if (result != 0 && result != NGHTTP3_ERR_STREAM_NOT_FOUND) { |
659 | 0 | self->http3_failed_ = true; |
660 | 0 | return NGTCP2_ERR_CALLBACK_FAILURE; |
661 | 0 | } |
662 | 131 | return 0; |
663 | 131 | } |
664 | | |
665 | | static int ResetQuicStream(ngtcp2_conn* /*connection*/, std::int64_t stream_id, std::uint64_t /*final_size*/, |
666 | 0 | std::uint64_t /*application_error_code*/, void* user_data, void* /*stream_user_data*/) { |
667 | 0 | return ShutdownHttp3StreamRead(stream_id, user_data); |
668 | 0 | } |
669 | | |
670 | | static int StopSendingQuicStream(ngtcp2_conn* /*connection*/, std::int64_t stream_id, |
671 | | std::uint64_t /*application_error_code*/, void* user_data, |
672 | 0 | void* /*stream_user_data*/) { |
673 | 0 | return ShutdownHttp3StreamRead(stream_id, user_data); |
674 | 0 | } |
675 | | |
676 | 0 | static int ShutdownHttp3StreamRead(std::int64_t stream_id, void* user_data) { |
677 | 0 | auto* self = static_cast<Http3MockServerImpl*>(user_data); |
678 | 0 | if (self == nullptr || self->http3_connection_ == nullptr || self->http3_failed_) { |
679 | 0 | return 0; |
680 | 0 | } |
681 | 0 | const int result = nghttp3_conn_shutdown_stream_read(self->http3_connection_, stream_id); |
682 | 0 | if (result != 0 && result != NGHTTP3_ERR_STREAM_NOT_FOUND) { |
683 | 0 | self->http3_failed_ = true; |
684 | 0 | return NGTCP2_ERR_CALLBACK_FAILURE; |
685 | 0 | } |
686 | 0 | return 0; |
687 | 0 | } |
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 | 6.80k | bool CreateHttp3Connection() { |
693 | 6.80k | if (http3_connection_ != nullptr) { |
694 | 0 | return true; |
695 | 0 | } |
696 | 6.80k | nghttp3_callbacks callbacks = {}; |
697 | 6.80k | callbacks.recv_data = &ReceiveRequestData; |
698 | 6.80k | callbacks.deferred_consume = &DeferredConsumeRequestData; |
699 | 6.80k | callbacks.end_headers = &EndRequestHeaders; |
700 | 6.80k | callbacks.end_stream = &EndRequestStream; |
701 | 6.80k | nghttp3_settings settings; |
702 | 6.80k | nghttp3_settings_default(&settings); |
703 | 6.80k | settings.qpack_max_dtable_capacity = 0; |
704 | 6.80k | settings.qpack_encoder_max_dtable_capacity = 0; |
705 | 6.80k | settings.qpack_blocked_streams = 0; |
706 | 6.80k | if (nghttp3_conn_server_new(&http3_connection_, &callbacks, &settings, nullptr, this) != 0) { |
707 | 0 | http3_failed_ = true; |
708 | 0 | return false; |
709 | 0 | } |
710 | 6.80k | nghttp3_conn_set_max_concurrent_streams(http3_connection_, kMaxStreams); |
711 | 6.80k | return true; |
712 | 6.80k | } |
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 | 47.6k | void FillRandom(std::uint8_t* destination, std::size_t length) { |
718 | 47.6k | std::uint64_t state = ++random_counter_ * UINT64_C(0x9e3779b97f4a7c15); |
719 | 523k | for (std::size_t index = 0; index < length; ++index) { |
720 | 476k | state ^= state >> 12U; |
721 | 476k | state ^= state << 25U; |
722 | 476k | state ^= state >> 27U; |
723 | 476k | destination[index] = static_cast<std::uint8_t>((state * UINT64_C(0x2545f4914f6cdd1d)) >> 56U); |
724 | 476k | } |
725 | 47.6k | } |
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 | 6.80k | bool InitializeConnection(const ngtcp2_pkt_hd& header, const struct sockaddr_in& remote_address) { |
731 | 6.80k | if (quic_connection_ != nullptr || header.type != NGTCP2_PKT_INITIAL) { |
732 | 0 | return false; |
733 | 0 | } |
734 | | |
735 | 6.80k | remote_address_ = remote_address; |
736 | 6.80k | ngtcp2_path_storage_init(&path_storage_, reinterpret_cast<const ngtcp2_sockaddr*>(&local_address_), |
737 | 6.80k | sizeof(local_address_), reinterpret_cast<const ngtcp2_sockaddr*>(&remote_address_), |
738 | 6.80k | sizeof(remote_address_), nullptr); |
739 | | |
740 | 6.80k | ngtcp2_callbacks callbacks = {}; |
741 | 6.80k | callbacks.recv_client_initial = ngtcp2_crypto_recv_client_initial_cb; |
742 | 6.80k | callbacks.recv_crypto_data = ngtcp2_crypto_recv_crypto_data_cb; |
743 | 6.80k | callbacks.handshake_completed = &HandshakeCompleted; |
744 | 6.80k | callbacks.encrypt = ngtcp2_crypto_encrypt_cb; |
745 | 6.80k | callbacks.decrypt = ngtcp2_crypto_decrypt_cb; |
746 | 6.80k | callbacks.hp_mask = ngtcp2_crypto_hp_mask_cb; |
747 | 6.80k | callbacks.recv_stream_data = &ReceiveQuicStreamData; |
748 | 6.80k | callbacks.acked_stream_data_offset = &AckedStreamDataOffset; |
749 | 6.80k | callbacks.stream_open = &OpenRemoteStream; |
750 | 6.80k | callbacks.stream_reset = &ResetQuicStream; |
751 | 6.80k | callbacks.rand = &GenerateRandomBytes; |
752 | 6.80k | callbacks.update_key = ngtcp2_crypto_update_key_cb; |
753 | 6.80k | callbacks.delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb; |
754 | 6.80k | callbacks.delete_crypto_cipher_ctx = ngtcp2_crypto_delete_crypto_cipher_ctx_cb; |
755 | 6.80k | callbacks.stream_stop_sending = &StopSendingQuicStream; |
756 | 6.80k | callbacks.version_negotiation = ngtcp2_crypto_version_negotiation_cb; |
757 | 6.80k | callbacks.recv_tx_key = &ReceiveTransmitKey; |
758 | 6.80k | callbacks.get_new_connection_id2 = &GetNewConnectionId; |
759 | 6.80k | callbacks.get_path_challenge_data2 = ngtcp2_crypto_get_path_challenge_data2_cb; |
760 | 6.80k | callbacks.stream_close2 = &CloseQuicStream; |
761 | | |
762 | 6.80k | ngtcp2_settings settings; |
763 | 6.80k | ngtcp2_settings_default(&settings); |
764 | 6.80k | settings.initial_ts = QuicNow(); |
765 | 6.80k | settings.rand_ctx.native_handle = this; |
766 | 6.80k | settings.no_pmtud = 1; |
767 | 6.80k | settings.max_tx_udp_payload_size = packet_buffer_.size(); |
768 | | |
769 | 6.80k | ngtcp2_transport_params parameters; |
770 | 6.80k | ngtcp2_transport_params_default(¶meters); |
771 | 6.80k | parameters.initial_max_stream_data_bidi_local = kMaxPendingPlaintextBytes; |
772 | 6.80k | parameters.initial_max_stream_data_bidi_remote = kMaxPendingPlaintextBytes; |
773 | 6.80k | parameters.initial_max_stream_data_uni = kMaxPendingPlaintextBytes; |
774 | 6.80k | parameters.initial_max_data = kMaxPendingPlaintextBytes * 4U; |
775 | 6.80k | parameters.initial_max_streams_bidi = kMaxStreams; |
776 | 6.80k | parameters.initial_max_streams_uni = kMaxStreams; |
777 | 6.80k | parameters.max_idle_timeout = 5U * NGTCP2_SECONDS; |
778 | 6.80k | parameters.active_connection_id_limit = 2; |
779 | 6.80k | parameters.original_dcid = header.dcid; |
780 | 6.80k | parameters.original_dcid_present = 1; |
781 | | |
782 | 6.80k | ngtcp2_cid server_cid = {}; |
783 | 6.80k | server_cid.datalen = kServerConnectionIdBytes; |
784 | 6.80k | FillRandom(server_cid.data, server_cid.datalen); |
785 | 6.80k | if (ngtcp2_conn_server_new(&quic_connection_, &header.scid, &server_cid, &path_storage_.path, header.version, |
786 | 6.80k | &callbacks, &settings, ¶meters, nullptr, this) != 0) { |
787 | 0 | return false; |
788 | 0 | } |
789 | | |
790 | 6.80k | if (ngtcp2_crypto_ossl_ctx_new(&crypto_context_, nullptr) != 0) { |
791 | 0 | return false; |
792 | 0 | } |
793 | 6.80k | tls_connection_ = SSL_new(context_); |
794 | 6.80k | if (tls_connection_ == nullptr) { |
795 | 0 | return false; |
796 | 0 | } |
797 | 6.80k | ngtcp2_crypto_ossl_ctx_set_ssl(crypto_context_, tls_connection_); |
798 | 6.80k | if (ngtcp2_crypto_ossl_configure_server_session(tls_connection_) != 0) { |
799 | 0 | return false; |
800 | 0 | } |
801 | 6.80k | SSL_set_app_data(tls_connection_, &crypto_connection_ref_); |
802 | 6.80k | SSL_set_accept_state(tls_connection_); |
803 | 6.80k | ngtcp2_conn_set_tls_native_handle(quic_connection_, crypto_context_); |
804 | 6.80k | return true; |
805 | 6.80k | } |
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 | 136k | std::size_t ReadDatagrams() { |
810 | 136k | std::size_t progress = 0; |
811 | 192k | for (std::size_t packet = 0; packet < kMaxDatagramsPerTurn; ++packet) { |
812 | 192k | struct sockaddr_in remote_address = {}; |
813 | 192k | socklen_t remote_length = sizeof(remote_address); |
814 | 192k | const ssize_t received = ::recvfrom(server_fd_, receive_buffer_.data(), receive_buffer_.size(), 0, |
815 | 192k | reinterpret_cast<struct sockaddr*>(&remote_address), &remote_length); |
816 | 192k | if (received < 0) { |
817 | 136k | if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) { |
818 | 0 | transport_failed_ = true; |
819 | 0 | } |
820 | 136k | break; |
821 | 136k | } |
822 | 55.8k | if (received == 0 || remote_length != sizeof(remote_address) || remote_address.sin_family != AF_INET) { |
823 | 0 | continue; |
824 | 0 | } |
825 | 55.8k | ++progress; |
826 | | |
827 | 55.8k | if (quic_connection_ == nullptr) { |
828 | 6.80k | ngtcp2_pkt_hd header = {}; |
829 | 6.80k | if (ngtcp2_accept(&header, receive_buffer_.data(), static_cast<std::size_t>(received)) != 0) { |
830 | 0 | continue; |
831 | 0 | } |
832 | 6.80k | if (!InitializeConnection(header, remote_address)) { |
833 | 0 | transport_failed_ = true; |
834 | 0 | return progress; |
835 | 0 | } |
836 | 49.0k | } else if (remote_address.sin_port != remote_address_.sin_port || |
837 | 49.0k | remote_address.sin_addr.s_addr != remote_address_.sin_addr.s_addr) { |
838 | 0 | continue; |
839 | 0 | } |
840 | | |
841 | 55.8k | ngtcp2_pkt_info packet_info = {}; |
842 | 55.8k | const int result = ngtcp2_conn_read_pkt(quic_connection_, &path_storage_.path, &packet_info, |
843 | 55.8k | receive_buffer_.data(), static_cast<std::size_t>(received), QuicNow()); |
844 | 55.8k | if (result != 0) { |
845 | 26 | transport_failed_ = true; |
846 | 26 | return progress; |
847 | 26 | } |
848 | 55.8k | } |
849 | 136k | return progress; |
850 | 136k | } |
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 | 136k | std::size_t HandleExpiry() { |
855 | 136k | if (quic_connection_ == nullptr) { |
856 | 6.80k | return 0; |
857 | 6.80k | } |
858 | 129k | const ngtcp2_tstamp now = QuicNow(); |
859 | 129k | if (ngtcp2_conn_get_expiry2(quic_connection_) > now) { |
860 | 120k | return 0; |
861 | 120k | } |
862 | 8.65k | const int result = ngtcp2_conn_handle_expiry(quic_connection_, now); |
863 | 8.65k | if (result != 0) { |
864 | 0 | transport_failed_ = true; |
865 | 0 | } |
866 | 8.65k | return 1; |
867 | 129k | } |
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 | 43.1k | bool SendPacket(const ngtcp2_path& path, const std::uint8_t* data, std::size_t length) { |
872 | 43.1k | const ngtcp2_addr* destination = &path.remote; |
873 | 43.1k | if (destination->addr == nullptr || destination->addrlen == 0) { |
874 | 0 | destination = &path_storage_.path.remote; |
875 | 0 | } |
876 | 43.1k | const ssize_t sent = ::sendto(server_fd_, data, length, 0, |
877 | 43.1k | reinterpret_cast<const struct sockaddr*>(destination->addr), destination->addrlen); |
878 | 43.1k | if (sent == static_cast<ssize_t>(length)) { |
879 | 43.1k | return true; |
880 | 43.1k | } |
881 | 0 | if (sent < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)) { |
882 | 0 | return false; |
883 | 0 | } |
884 | 0 | transport_failed_ = true; |
885 | 0 | return false; |
886 | 0 | } |
887 | | |
888 | | // Serialize all QUIC control traffic after application writes. A requested |
889 | | // CONNECTION_CLOSE takes priority and is emitted at most once. |
890 | 136k | std::size_t FlushTransportPackets() { |
891 | 136k | if (quic_connection_ == nullptr || transport_failed_ || close_sent_) { |
892 | 0 | return 0; |
893 | 0 | } |
894 | | |
895 | 136k | if (close_requested_) { |
896 | 2.30k | ngtcp2_ccerr error; |
897 | 2.30k | ngtcp2_ccerr_default(&error); |
898 | 2.30k | ngtcp2_ccerr_set_application_error(&error, close_error_code_, nullptr, 0); |
899 | 2.30k | ngtcp2_path_storage output_path; |
900 | 2.30k | ngtcp2_path_storage_zero(&output_path); |
901 | 2.30k | ngtcp2_pkt_info packet_info = {}; |
902 | 2.30k | const ngtcp2_tstamp now = QuicNow(); |
903 | 2.30k | const ngtcp2_ssize written = ngtcp2_conn_write_connection_close( |
904 | 2.30k | quic_connection_, &output_path.path, &packet_info, packet_buffer_.data(), packet_buffer_.size(), &error, now); |
905 | 2.30k | if (written < 0) { |
906 | 0 | transport_failed_ = true; |
907 | 0 | return 0; |
908 | 0 | } |
909 | 2.30k | close_sent_ = true; |
910 | 2.30k | if (written == 0) { |
911 | 0 | return 1; |
912 | 0 | } |
913 | 2.30k | ngtcp2_conn_update_pkt_tx_time(quic_connection_, now); |
914 | 2.30k | (void)SendPacket(output_path.path, packet_buffer_.data(), static_cast<std::size_t>(written)); |
915 | 2.30k | return 1; |
916 | 2.30k | } |
917 | | |
918 | 133k | std::size_t progress = 0; |
919 | 151k | for (std::size_t packet = 0; packet < kMaxWritesPerTurn; ++packet) { |
920 | 151k | ngtcp2_path_storage output_path; |
921 | 151k | ngtcp2_path_storage_zero(&output_path); |
922 | 151k | ngtcp2_pkt_info packet_info = {}; |
923 | 151k | const ngtcp2_tstamp now = QuicNow(); |
924 | 151k | const ngtcp2_ssize written = ngtcp2_conn_write_pkt(quic_connection_, &output_path.path, &packet_info, |
925 | 151k | packet_buffer_.data(), packet_buffer_.size(), now); |
926 | 151k | if (written < 0) { |
927 | 0 | transport_failed_ = true; |
928 | 0 | break; |
929 | 0 | } |
930 | 151k | if (written == 0) { |
931 | 133k | break; |
932 | 133k | } |
933 | 17.7k | ngtcp2_conn_update_pkt_tx_time(quic_connection_, now); |
934 | 17.7k | if (!SendPacket(output_path.path, packet_buffer_.data(), static_cast<std::size_t>(written))) { |
935 | 0 | break; |
936 | 0 | } |
937 | 17.7k | ++progress; |
938 | 17.7k | } |
939 | 133k | return progress; |
940 | 136k | } |
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 | 129k | std::size_t CreateServerStreams() { |
946 | 129k | if (server_streams_bound_ || quic_connection_ == nullptr || http3_connection_ == nullptr || http3_failed_) { |
947 | 122k | return 0; |
948 | 122k | } |
949 | | |
950 | 6.80k | std::size_t progress = 0; |
951 | 6.80k | const std::array<StreamRoleIndex, 3> roles = {kControlStream, kQpackEncoderStream, kQpackDecoderStream}; |
952 | 20.4k | for (const StreamRoleIndex role : roles) { |
953 | 20.4k | if (role_stream_ids_[role] != kNoStreamId) { |
954 | 0 | continue; |
955 | 0 | } |
956 | 20.4k | const std::int64_t stream_id = OpenUnidirectionalStream(); |
957 | 20.4k | if (stream_id == kNoStreamId) { |
958 | 0 | return progress; |
959 | 0 | } |
960 | 20.4k | role_stream_ids_[role] = stream_id; |
961 | 20.4k | ++progress; |
962 | 20.4k | } |
963 | | |
964 | 6.80k | if (role_stream_ids_[kControlStream] == kNoStreamId || role_stream_ids_[kQpackEncoderStream] == kNoStreamId || |
965 | 6.80k | role_stream_ids_[kQpackDecoderStream] == kNoStreamId) { |
966 | 0 | return progress; |
967 | 0 | } |
968 | 6.80k | if (nghttp3_conn_bind_qpack_streams(http3_connection_, role_stream_ids_[kQpackEncoderStream], |
969 | 6.80k | role_stream_ids_[kQpackDecoderStream]) != 0 || |
970 | 6.80k | nghttp3_conn_bind_control_stream(http3_connection_, role_stream_ids_[kControlStream]) != 0) { |
971 | 0 | http3_failed_ = true; |
972 | 0 | return progress; |
973 | 0 | } |
974 | 6.80k | server_streams_bound_ = true; |
975 | 6.80k | return progress + 1; |
976 | 6.80k | } |
977 | | |
978 | 21.4k | std::int64_t OpenUnidirectionalStream() { |
979 | 21.4k | if (quic_connection_ == nullptr || streams_.size() >= kMaxStreams) { |
980 | 0 | return kNoStreamId; |
981 | 0 | } |
982 | 21.4k | std::int64_t stream_id = kNoStreamId; |
983 | 21.4k | if (ngtcp2_conn_open_uni_stream(quic_connection_, &stream_id, nullptr) != 0) { |
984 | 0 | return kNoStreamId; |
985 | 0 | } |
986 | | |
987 | 21.4k | StreamState state; |
988 | 21.4k | state.id = stream_id; |
989 | 21.4k | streams_.push_back(state); |
990 | 21.4k | return stream_id; |
991 | 21.4k | } |
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 | 122k | std::size_t DriveApplicationOutput() { |
997 | 122k | std::size_t progress = 0; |
998 | | |
999 | 122k | if (pending_write_.active()) { |
1000 | 3.43k | const bool completes_action = pending_write_.completes_action; |
1001 | 3.43k | const WriteResult result = FlushPendingWrite(&progress); |
1002 | 3.43k | if (result == WriteResult::kBlocked) { |
1003 | 131 | return progress; |
1004 | 131 | } |
1005 | 3.30k | if (result == WriteResult::kFailed && transport_failed_) { |
1006 | 0 | return progress; |
1007 | 0 | } |
1008 | 3.30k | if (completes_action) { |
1009 | 3.29k | ++next_action_; |
1010 | 3.29k | return progress + 1; |
1011 | 3.29k | } |
1012 | 14 | if (result == WriteResult::kFailed && waiting_for_h3_drain_) { |
1013 | 0 | waiting_for_h3_drain_ = false; |
1014 | 0 | if (waiting_drain_completes_action_) { |
1015 | 0 | ++next_action_; |
1016 | 0 | } |
1017 | 0 | return progress + 1; |
1018 | 0 | } |
1019 | 14 | } |
1020 | | |
1021 | 119k | if (!server_streams_bound_) { |
1022 | 0 | return progress; |
1023 | 0 | } |
1024 | 119k | if (!bootstrap_drained_) { |
1025 | 6.68k | if (http3_failed_ || PumpNghttp3(&progress)) { |
1026 | 6.68k | bootstrap_drained_ = true; |
1027 | 6.68k | ++progress; |
1028 | 6.68k | } |
1029 | 6.68k | return progress; |
1030 | 6.68k | } |
1031 | 112k | if (waiting_for_h3_drain_) { |
1032 | 1.22k | if (http3_failed_ || PumpNghttp3(&progress)) { |
1033 | 1.21k | waiting_for_h3_drain_ = false; |
1034 | 1.21k | if (waiting_drain_completes_action_) { |
1035 | 1.21k | ++next_action_; |
1036 | 1.21k | } |
1037 | 1.21k | waiting_drain_completes_action_ = false; |
1038 | 1.21k | ++progress; |
1039 | 1.21k | } |
1040 | 1.22k | return progress; |
1041 | 1.22k | } |
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 | 111k | if (!http3_failed_ && !PumpNghttp3(&progress)) { |
1046 | 0 | return progress; |
1047 | 0 | } |
1048 | 111k | if (transport_failed_) { |
1049 | 0 | return progress; |
1050 | 0 | } |
1051 | | |
1052 | 111k | const std::size_t action_count = |
1053 | 111k | scenario_ == nullptr |
1054 | 111k | ? 0 |
1055 | 111k | : std::min<std::size_t>(scenario_limits::kMaxHttp3Actions, scenario_->http3_plan().actions_size()); |
1056 | 111k | if (next_action_ >= action_count) { |
1057 | | // An empty H3 plan still needs one successful response after a request. |
1058 | 103k | if (action_count == 0 && !default_response_started_ && request_headers_received_) { |
1059 | 0 | default_response_started_ = true; |
1060 | 0 | if (SubmitStructuredResponse(default_response_)) { |
1061 | 0 | waiting_for_h3_drain_ = true; |
1062 | 0 | waiting_drain_completes_action_ = false; |
1063 | 0 | ++progress; |
1064 | 0 | } |
1065 | 0 | } |
1066 | 103k | return progress; |
1067 | 103k | } |
1068 | | |
1069 | 7.73k | const auto& action = scenario_->http3_plan().actions(static_cast<int>(next_action_)); |
1070 | 7.73k | if (action.has_structured_response()) { |
1071 | 1.27k | if (!request_headers_received_) { |
1072 | 0 | return progress; |
1073 | 0 | } |
1074 | 1.27k | if (SubmitStructuredResponse(action.structured_response())) { |
1075 | 1.21k | waiting_for_h3_drain_ = true; |
1076 | 1.21k | waiting_drain_completes_action_ = true; |
1077 | 1.21k | } else { |
1078 | 61 | ++next_action_; |
1079 | 61 | } |
1080 | 1.27k | return progress + 1; |
1081 | 1.27k | } |
1082 | | |
1083 | 6.45k | if (action.has_stream_write()) { |
1084 | 1.00k | const auto& write = action.stream_write(); |
1085 | 1.00k | const std::int64_t stream_id = StreamForRole(write.role()); |
1086 | 1.00k | if (stream_id == kNoStreamId) { |
1087 | 0 | return progress; |
1088 | 0 | } |
1089 | 1.00k | const std::size_t length = std::min<std::size_t>(scenario_limits::kMaxHttp3RawWriteBytes, write.data().size()); |
1090 | 1.00k | QueueWrite(stream_id, write.data().substr(0, length), write.finish_stream(), false, true); |
1091 | 1.00k | return progress + 1; |
1092 | 1.00k | } |
1093 | | |
1094 | 5.45k | if (action.has_open_unidirectional_stream()) { |
1095 | 1.02k | const auto& open = action.open_unidirectional_stream(); |
1096 | 1.02k | const std::int64_t stream_id = OpenUnidirectionalStream(); |
1097 | 1.02k | if (stream_id == kNoStreamId) { |
1098 | 0 | ++next_action_; |
1099 | 0 | return progress + 1; |
1100 | 0 | } |
1101 | 1.02k | const std::size_t length = std::min<std::size_t>(scenario_limits::kMaxHttp3RawWriteBytes, open.data().size()); |
1102 | 1.02k | QueueWrite(stream_id, open.data().substr(0, length), open.finish_stream(), false, true); |
1103 | 1.02k | return progress + 1; |
1104 | 1.02k | } |
1105 | | |
1106 | 4.43k | if (action.has_stream_reset()) { |
1107 | 855 | const auto& reset = action.stream_reset(); |
1108 | 855 | const std::int64_t stream_id = StreamForRole(reset.role()); |
1109 | 855 | if (stream_id == kNoStreamId) { |
1110 | 0 | return progress; |
1111 | 0 | } |
1112 | 855 | (void)ngtcp2_conn_shutdown_stream_write(quic_connection_, 0, stream_id, |
1113 | 855 | reset.application_error_code() & scenario_limits::kMaxQuicVarint); |
1114 | 855 | ++next_action_; |
1115 | 855 | return progress + 1; |
1116 | 855 | } |
1117 | | |
1118 | 3.57k | if (action.has_goaway()) { |
1119 | 1.27k | const std::int64_t control_stream_id = StreamForRole(curl::fuzzer::proto::HTTP3_STREAM_CONTROL); |
1120 | 1.27k | if (control_stream_id == kNoStreamId) { |
1121 | 0 | return progress; |
1122 | 0 | } |
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 | 1.27k | std::string payload; |
1126 | 1.27k | AppendQuicVarint(&payload, action.goaway().id()); |
1127 | 1.27k | std::string frame; |
1128 | 1.27k | AppendQuicVarint(&frame, 0x07U); |
1129 | 1.27k | AppendQuicVarint(&frame, payload.size()); |
1130 | 1.27k | frame.append(payload); |
1131 | 1.27k | QueueWrite(control_stream_id, std::move(frame), false, false, true); |
1132 | 1.27k | return progress + 1; |
1133 | 1.27k | } |
1134 | | |
1135 | 2.30k | if (action.has_connection_close()) { |
1136 | 2.30k | close_error_code_ = action.connection_close().application_error_code() & scenario_limits::kMaxQuicVarint; |
1137 | 2.30k | close_requested_ = true; |
1138 | 2.30k | ++next_action_; |
1139 | 2.30k | return progress + 1; |
1140 | 2.30k | } |
1141 | | |
1142 | 0 | ++next_action_; |
1143 | 0 | return progress + 1; |
1144 | 2.30k | } |
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 | 24.6k | bool completes_action) { |
1150 | 24.6k | retained_writes_.push_back(std::make_unique<std::string>(std::move(bytes))); |
1151 | 24.6k | pending_write_.stream_id = stream_id; |
1152 | 24.6k | pending_write_.bytes = retained_writes_.back().get(); |
1153 | 24.6k | pending_write_.offset = 0; |
1154 | 24.6k | pending_write_.finish_stream = finish_stream; |
1155 | 24.6k | pending_write_.nghttp3_managed = nghttp3_managed; |
1156 | 24.6k | pending_write_.completes_action = completes_action; |
1157 | 24.6k | } |
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 | 24.8k | WriteResult FlushPendingWrite(std::size_t* progress) { |
1163 | 24.8k | if (!pending_write_.active()) { |
1164 | 0 | return WriteResult::kComplete; |
1165 | 0 | } |
1166 | | |
1167 | 24.8k | if (pending_write_.bytes == nullptr || pending_write_.offset > pending_write_.bytes->size()) { |
1168 | 0 | pending_write_.Clear(); |
1169 | 0 | return WriteResult::kFailed; |
1170 | 0 | } |
1171 | | |
1172 | 24.8k | const std::size_t remaining = pending_write_.bytes->size() - pending_write_.offset; |
1173 | 24.8k | if (remaining == 0 && !pending_write_.finish_stream) { |
1174 | 1.77k | pending_write_.Clear(); |
1175 | 1.77k | ++*progress; |
1176 | 1.77k | return WriteResult::kComplete; |
1177 | 1.77k | } |
1178 | | |
1179 | 23.0k | ngtcp2_vec vector = {}; |
1180 | 23.0k | vector.base = |
1181 | 23.0k | reinterpret_cast<std::uint8_t*>(const_cast<char*>(pending_write_.bytes->data() + pending_write_.offset)); |
1182 | 23.0k | vector.len = remaining; |
1183 | 23.0k | const std::size_t vector_count = remaining == 0 ? 0 : 1; |
1184 | 23.0k | const std::uint32_t flags = |
1185 | 23.0k | pending_write_.finish_stream ? NGTCP2_WRITE_STREAM_FLAG_FIN : NGTCP2_WRITE_STREAM_FLAG_NONE; |
1186 | 23.0k | ngtcp2_path_storage output_path; |
1187 | 23.0k | ngtcp2_path_storage_zero(&output_path); |
1188 | 23.0k | ngtcp2_pkt_info packet_info = {}; |
1189 | 23.0k | ngtcp2_ssize submitted = -1; |
1190 | 23.0k | const ngtcp2_tstamp now = QuicNow(); |
1191 | 23.0k | const ngtcp2_ssize packet_length = ngtcp2_conn_writev_stream( |
1192 | 23.0k | quic_connection_, &output_path.path, &packet_info, packet_buffer_.data(), packet_buffer_.size(), &submitted, |
1193 | 23.0k | flags, pending_write_.stream_id, vector_count == 0 ? nullptr : &vector, vector_count, now); |
1194 | 23.0k | if (packet_length == NGTCP2_ERR_STREAM_DATA_BLOCKED) { |
1195 | 0 | return WriteResult::kBlocked; |
1196 | 0 | } |
1197 | 23.0k | if (packet_length == NGTCP2_ERR_STREAM_SHUT_WR || packet_length == NGTCP2_ERR_STREAM_NOT_FOUND) { |
1198 | 46 | pending_write_.Clear(); |
1199 | 46 | return WriteResult::kFailed; |
1200 | 46 | } |
1201 | 23.0k | if (packet_length < 0) { |
1202 | 0 | transport_failed_ = true; |
1203 | 0 | pending_write_.Clear(); |
1204 | 0 | return WriteResult::kFailed; |
1205 | 0 | } |
1206 | 23.0k | if (packet_length == 0) { |
1207 | 0 | return WriteResult::kBlocked; |
1208 | 0 | } |
1209 | 23.0k | ngtcp2_conn_update_pkt_tx_time(quic_connection_, now); |
1210 | 23.0k | (void)SendPacket(output_path.path, packet_buffer_.data(), static_cast<std::size_t>(packet_length)); |
1211 | 23.0k | ++*progress; |
1212 | 23.0k | if (transport_failed_) { |
1213 | 0 | pending_write_.Clear(); |
1214 | 0 | return WriteResult::kFailed; |
1215 | 0 | } |
1216 | | |
1217 | 23.0k | if (submitted >= 0) { |
1218 | 23.0k | StreamState* stream = FindStream(pending_write_.stream_id); |
1219 | 23.0k | if (stream == nullptr || static_cast<std::uint64_t>(submitted) > UINT64_MAX - stream->write_offset) { |
1220 | 0 | transport_failed_ = true; |
1221 | 0 | pending_write_.Clear(); |
1222 | 0 | return WriteResult::kFailed; |
1223 | 0 | } |
1224 | 23.0k | const std::uint64_t write_begin = stream->write_offset; |
1225 | 23.0k | stream->write_offset += static_cast<std::uint64_t>(submitted); |
1226 | 23.0k | if (pending_write_.nghttp3_managed) { |
1227 | 21.4k | if (nghttp3_conn_add_write_offset(http3_connection_, pending_write_.stream_id, |
1228 | 21.4k | static_cast<std::uint64_t>(submitted)) != 0) { |
1229 | 0 | http3_failed_ = true; |
1230 | 0 | pending_write_.Clear(); |
1231 | 0 | return WriteResult::kFailed; |
1232 | 0 | } |
1233 | 21.4k | if (submitted != 0) { |
1234 | 21.4k | stream->managed_write_ranges.push_back({write_begin, stream->write_offset}); |
1235 | 21.4k | } else if (pending_write_.finish_stream && pending_write_.offset == pending_write_.bytes->size()) { |
1236 | 0 | stream->managed_fin_offsets.push_back(write_begin); |
1237 | 0 | } |
1238 | 21.4k | } |
1239 | 23.0k | pending_write_.offset += static_cast<std::size_t>(submitted); |
1240 | 23.0k | } |
1241 | 23.0k | if (submitted < 0 || pending_write_.offset != pending_write_.bytes->size()) { |
1242 | 145 | return WriteResult::kBlocked; |
1243 | 145 | } |
1244 | 22.8k | pending_write_.Clear(); |
1245 | 22.8k | return WriteResult::kComplete; |
1246 | 23.0k | } |
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 | 119k | bool PumpNghttp3(std::size_t* progress) { |
1252 | 119k | if (http3_connection_ == nullptr || http3_failed_) { |
1253 | 0 | return true; |
1254 | 0 | } |
1255 | | |
1256 | 140k | for (std::size_t operation = 0; operation < kMaxWritesPerTurn; ++operation) { |
1257 | 140k | if (pending_write_.active()) { |
1258 | 21.3k | if (!pending_write_.nghttp3_managed) { |
1259 | 0 | return false; |
1260 | 0 | } |
1261 | 21.3k | const WriteResult result = FlushPendingWrite(progress); |
1262 | 21.3k | if (result == WriteResult::kBlocked) { |
1263 | 14 | return false; |
1264 | 14 | } |
1265 | 21.3k | if (result == WriteResult::kFailed) { |
1266 | 0 | http3_failed_ = true; |
1267 | 0 | return true; |
1268 | 0 | } |
1269 | 21.3k | } |
1270 | | |
1271 | 140k | std::array<nghttp3_vec, 16> vectors; |
1272 | 140k | std::int64_t stream_id = -1; |
1273 | 140k | int finish_stream = 0; |
1274 | 140k | const nghttp3_ssize vector_count = |
1275 | 140k | nghttp3_conn_writev_stream(http3_connection_, &stream_id, &finish_stream, vectors.data(), vectors.size()); |
1276 | 140k | if (vector_count < 0) { |
1277 | 0 | http3_failed_ = true; |
1278 | 0 | return true; |
1279 | 0 | } |
1280 | 140k | if (vector_count == 0 && stream_id == -1) { |
1281 | 119k | return true; |
1282 | 119k | } |
1283 | | |
1284 | 21.3k | StreamState* stream = FindStream(stream_id); |
1285 | 21.3k | if (stream == nullptr) { |
1286 | 0 | http3_failed_ = true; |
1287 | 0 | return true; |
1288 | 0 | } |
1289 | | |
1290 | 21.3k | std::string bytes; |
1291 | 21.3k | if (vector_count > 0) { |
1292 | 21.3k | std::size_t total = 0; |
1293 | 43.1k | for (nghttp3_ssize index = 0; index < vector_count; ++index) { |
1294 | 21.7k | if (vectors[static_cast<std::size_t>(index)].len > kMaxPendingPlaintextBytes - total) { |
1295 | 0 | http3_failed_ = true; |
1296 | 0 | return true; |
1297 | 0 | } |
1298 | 21.7k | total += vectors[static_cast<std::size_t>(index)].len; |
1299 | 21.7k | } |
1300 | 21.3k | bytes.reserve(total); |
1301 | 43.1k | for (nghttp3_ssize index = 0; index < vector_count; ++index) { |
1302 | 21.7k | const nghttp3_vec& vector = vectors[static_cast<std::size_t>(index)]; |
1303 | 21.7k | bytes.append(reinterpret_cast<const char*>(vector.base), vector.len); |
1304 | 21.7k | } |
1305 | 21.3k | } |
1306 | | |
1307 | 21.3k | QueueWrite(stream->id, std::move(bytes), finish_stream != 0, true, false); |
1308 | 21.3k | } |
1309 | 0 | return false; |
1310 | 119k | } |
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 | 1.27k | bool SubmitStructuredResponse(const curl::fuzzer::proto::Http3Response& response) { |
1315 | 1.27k | if (http3_connection_ == nullptr || http3_failed_ || role_stream_ids_[kResponseStream] == kNoStreamId) { |
1316 | 0 | return false; |
1317 | 0 | } |
1318 | | |
1319 | 1.27k | std::uint32_t status_code = response.status_code(); |
1320 | 1.27k | if (status_code < 100U || status_code > 599U) { |
1321 | 0 | status_code = 100U + status_code % 500U; |
1322 | 0 | } |
1323 | 1.27k | std::vector<std::pair<std::string, std::string>> fields; |
1324 | 1.27k | fields.reserve(1 + std::min<std::size_t>(scenario_limits::kMaxHttp3Headers, response.response_headers_size())); |
1325 | 1.27k | fields.emplace_back(":status", std::to_string(status_code)); |
1326 | 1.27k | AppendHeaders(&fields, response.response_headers(), scenario_limits::kMaxHttp3Headers); |
1327 | 1.27k | const std::vector<nghttp3_nv> name_values = MakeNameValues(fields); |
1328 | 1.27k | const std::int64_t response_stream_id = static_cast<std::int64_t>(role_stream_ids_[kResponseStream]); |
1329 | | |
1330 | 1.27k | if (status_code < 200U) { |
1331 | 372 | return nghttp3_conn_submit_info(http3_connection_, response_stream_id, name_values.data(), name_values.size()) == |
1332 | 372 | 0; |
1333 | 372 | } |
1334 | 901 | if (final_response_submitted_) { |
1335 | 61 | return false; |
1336 | 61 | } |
1337 | | |
1338 | 840 | response_body_.chunks.clear(); |
1339 | 840 | response_body_.next_chunk = 0; |
1340 | 840 | response_body_.finish_stream = response.finish_stream(); |
1341 | 840 | response_body_.has_trailers = response.response_trailers_size() != 0; |
1342 | 840 | std::size_t body_bytes = 0; |
1343 | 840 | const std::size_t body_chunks = |
1344 | 840 | std::min<std::size_t>(scenario_limits::kMaxHttp3BodyChunks, response.body_chunks_size()); |
1345 | 840 | response_body_.chunks.reserve(body_chunks); |
1346 | 1.06k | for (std::size_t index = 0; index < body_chunks && body_bytes < scenario_limits::kMaxHttp3BodyBytes; ++index) { |
1347 | 228 | const std::string& source = response.body_chunks(static_cast<int>(index)); |
1348 | 228 | const std::size_t length = std::min(source.size(), scenario_limits::kMaxHttp3BodyBytes - body_bytes); |
1349 | 228 | response_body_.chunks.emplace_back(source.data(), length); |
1350 | 228 | body_bytes += length; |
1351 | 228 | } |
1352 | | |
1353 | 840 | nghttp3_data_reader reader = {}; |
1354 | 840 | reader.read_data = &ReadResponseBody; |
1355 | 840 | if (nghttp3_conn_submit_response(http3_connection_, response_stream_id, name_values.data(), name_values.size(), |
1356 | 840 | &reader) != 0) { |
1357 | 0 | return false; |
1358 | 0 | } |
1359 | 840 | final_response_submitted_ = true; |
1360 | | |
1361 | 840 | if (response_body_.has_trailers) { |
1362 | 142 | std::vector<std::pair<std::string, std::string>> trailers; |
1363 | 142 | trailers.reserve(std::min<std::size_t>(scenario_limits::kMaxHttp3Trailers, response.response_trailers_size())); |
1364 | 142 | AppendHeaders(&trailers, response.response_trailers(), scenario_limits::kMaxHttp3Trailers); |
1365 | 142 | const std::vector<nghttp3_nv> trailer_values = MakeNameValues(trailers); |
1366 | 142 | if (nghttp3_conn_submit_trailers(http3_connection_, response_stream_id, trailer_values.data(), |
1367 | 142 | trailer_values.size()) != 0) { |
1368 | 0 | http3_failed_ = true; |
1369 | 0 | } |
1370 | 142 | } |
1371 | 840 | return true; |
1372 | 840 | } |
1373 | | |
1374 | | template <typename RepeatedHeaders> |
1375 | | static void AppendHeaders(std::vector<std::pair<std::string, std::string>>* destination, |
1376 | 1.41k | const RepeatedHeaders& source, std::size_t count_limit) { |
1377 | 1.41k | std::size_t remaining = scenario_limits::kMaxHttp3HeaderBytes; |
1378 | 1.41k | const std::size_t count = std::min<std::size_t>(count_limit, source.size()); |
1379 | 2.91k | for (std::size_t index = 0; index < count && remaining != 0; ++index) { |
1380 | 1.50k | std::string name = NormalizeHeaderName(source.Get(static_cast<int>(index)).name()); |
1381 | 1.50k | std::string value = NormalizeHeaderValue(source.Get(static_cast<int>(index)).value()); |
1382 | 1.50k | if (name.size() > remaining) { |
1383 | 0 | name.resize(remaining); |
1384 | 0 | value.clear(); |
1385 | 1.50k | } else if (value.size() > remaining - name.size()) { |
1386 | 0 | value.resize(remaining - name.size()); |
1387 | 0 | } |
1388 | 1.50k | remaining -= name.size() + value.size(); |
1389 | 1.50k | destination->emplace_back(std::move(name), std::move(value)); |
1390 | 1.50k | } |
1391 | 1.41k | } |
1392 | | |
1393 | 1.41k | static std::vector<nghttp3_nv> MakeNameValues(const std::vector<std::pair<std::string, std::string>>& fields) { |
1394 | 1.41k | std::vector<nghttp3_nv> name_values; |
1395 | 1.41k | name_values.reserve(fields.size()); |
1396 | 2.77k | for (const auto& field : fields) { |
1397 | 2.77k | nghttp3_nv name_value = {}; |
1398 | 2.77k | name_value.name = reinterpret_cast<std::uint8_t*>(const_cast<char*>(field.first.data())); |
1399 | 2.77k | name_value.value = reinterpret_cast<std::uint8_t*>(const_cast<char*>(field.second.data())); |
1400 | 2.77k | name_value.namelen = field.first.size(); |
1401 | 2.77k | name_value.valuelen = field.second.size(); |
1402 | 2.77k | name_value.flags = NGHTTP3_NV_FLAG_NONE; |
1403 | 2.77k | name_values.push_back(name_value); |
1404 | 2.77k | } |
1405 | 1.41k | return name_values; |
1406 | 1.41k | } |
1407 | | |
1408 | 65.3k | StreamState* FindStream(std::int64_t stream_id) { |
1409 | 148k | for (StreamState& stream : streams_) { |
1410 | 148k | if (stream.id == stream_id) { |
1411 | 65.3k | return &stream; |
1412 | 65.3k | } |
1413 | 148k | } |
1414 | 0 | return nullptr; |
1415 | 65.3k | } |
1416 | | |
1417 | 3.12k | std::int64_t StreamForRole(curl::fuzzer::proto::Http3StreamRole role) { |
1418 | 3.12k | const int role_value = static_cast<int>(role); |
1419 | 3.12k | if (role_value < 0 || role_value >= static_cast<int>(kStreamRoleCount)) { |
1420 | 0 | return kNoStreamId; |
1421 | 0 | } |
1422 | 3.12k | return role_stream_ids_[static_cast<std::size_t>(role_value)]; |
1423 | 3.12k | } |
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 | 1.66k | std::size_t data_length, void* user_data, void* /*stream_user_data*/) { |
1429 | 1.66k | auto* self = static_cast<Http3MockServerImpl*>(user_data); |
1430 | 1.66k | if (self != nullptr && self->quic_connection_ != nullptr) { |
1431 | 1.66k | ngtcp2_conn_extend_max_stream_offset(self->quic_connection_, stream_id, data_length); |
1432 | 1.66k | ngtcp2_conn_extend_max_offset(self->quic_connection_, data_length); |
1433 | 1.66k | } |
1434 | 1.66k | return 0; |
1435 | 1.66k | } |
1436 | | |
1437 | | static int DeferredConsumeRequestData(nghttp3_conn* /*connection*/, std::int64_t stream_id, std::size_t consumed, |
1438 | 0 | void* user_data, void* /*stream_user_data*/) { |
1439 | 0 | auto* self = static_cast<Http3MockServerImpl*>(user_data); |
1440 | 0 | if (self != nullptr && self->quic_connection_ != nullptr) { |
1441 | 0 | ngtcp2_conn_extend_max_stream_offset(self->quic_connection_, stream_id, consumed); |
1442 | 0 | ngtcp2_conn_extend_max_offset(self->quic_connection_, consumed); |
1443 | 0 | } |
1444 | 0 | return 0; |
1445 | 0 | } |
1446 | | |
1447 | | static int EndRequestHeaders(nghttp3_conn* /*connection*/, std::int64_t stream_id, int /*finish_stream*/, |
1448 | 6.67k | void* user_data, void* /*stream_user_data*/) { |
1449 | 6.67k | auto* self = static_cast<Http3MockServerImpl*>(user_data); |
1450 | 6.67k | if (self != nullptr) { |
1451 | 6.67k | if (self->role_stream_ids_[kResponseStream] == kNoStreamId) { |
1452 | 0 | self->role_stream_ids_[kResponseStream] = stream_id; |
1453 | 0 | if (self->FindStream(stream_id) == nullptr && self->streams_.size() < kMaxStreams) { |
1454 | 0 | StreamState state; |
1455 | 0 | state.id = stream_id; |
1456 | 0 | self->streams_.push_back(state); |
1457 | 0 | } |
1458 | 0 | } |
1459 | 6.67k | if (self->role_stream_ids_[kResponseStream] == stream_id) { |
1460 | 6.67k | self->request_headers_received_ = true; |
1461 | 6.67k | } |
1462 | 6.67k | } |
1463 | 6.67k | return 0; |
1464 | 6.67k | } |
1465 | | |
1466 | | static int EndRequestStream(nghttp3_conn* /*connection*/, std::int64_t /*stream_id*/, void* /*user_data*/, |
1467 | 6.62k | void* /*stream_user_data*/) { |
1468 | 6.62k | return 0; |
1469 | 6.62k | } |
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 | 958 | void* /*stream_user_data*/) { |
1476 | 958 | auto* self = static_cast<Http3MockServerImpl*>(user_data); |
1477 | 958 | if (self == nullptr || vectors == nullptr || vector_count == 0 || flags == nullptr) { |
1478 | 0 | return NGHTTP3_ERR_CALLBACK_FAILURE; |
1479 | 0 | } |
1480 | | |
1481 | 992 | while (self->response_body_.next_chunk < self->response_body_.chunks.size() && |
1482 | 228 | self->response_body_.chunks[self->response_body_.next_chunk].empty()) { |
1483 | 34 | ++self->response_body_.next_chunk; |
1484 | 34 | } |
1485 | 958 | if (self->response_body_.next_chunk == self->response_body_.chunks.size()) { |
1486 | 764 | *flags = NGHTTP3_DATA_FLAG_EOF; |
1487 | 764 | if (!self->response_body_.finish_stream && !self->response_body_.has_trailers) { |
1488 | 93 | *flags |= NGHTTP3_DATA_FLAG_NO_END_STREAM; |
1489 | 93 | } |
1490 | 764 | return 0; |
1491 | 764 | } |
1492 | | |
1493 | 194 | const std::string& chunk = self->response_body_.chunks[self->response_body_.next_chunk++]; |
1494 | 194 | vectors[0].base = reinterpret_cast<std::uint8_t*>(const_cast<char*>(chunk.data())); |
1495 | 194 | vectors[0].len = chunk.size(); |
1496 | 194 | *flags = NGHTTP3_DATA_FLAG_NONE; |
1497 | 194 | if (self->response_body_.next_chunk == self->response_body_.chunks.size()) { |
1498 | 76 | *flags |= NGHTTP3_DATA_FLAG_EOF; |
1499 | 76 | if (!self->response_body_.finish_stream && !self->response_body_.has_trailers) { |
1500 | 34 | *flags |= NGHTTP3_DATA_FLAG_NO_END_STREAM; |
1501 | 34 | } |
1502 | 76 | } |
1503 | 194 | return 1; |
1504 | 958 | } |
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 | | |
1546 | 0 | Http3MockServer::Http3MockServer() : Http3MockServer(curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_DEFAULT_EC) {} |
1547 | | |
1548 | | Http3MockServer::Http3MockServer(curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain) |
1549 | 7.04k | : impl_(new Http3MockServerImpl(certificate_chain)) {} |
1550 | | |
1551 | 7.04k | Http3MockServer::~Http3MockServer() = default; |
1552 | | |
1553 | 7.04k | void Http3MockServer::Install(CURL* easy) { |
1554 | | // Curl trusts the checked-in server certificate directly and must use HTTP/3 |
1555 | | // rather than silently falling back to an HTTP/1 or HTTP/2 code path. |
1556 | 7.04k | MockServerBase::Install(easy); |
1557 | 7.04k | struct curl_blob trust_anchor = {const_cast<char*>(tls_test_credentials::kCertificatePem), |
1558 | 7.04k | sizeof(tls_test_credentials::kCertificatePem) - 1, CURL_BLOB_NOCOPY}; |
1559 | 7.04k | (void)curl_easy_setopt(easy, CURLOPT_CAINFO_BLOB, &trust_anchor); |
1560 | 7.04k | (void)curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L); |
1561 | 7.04k | (void)curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L); |
1562 | 7.04k | (void)curl_easy_setopt(easy, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_3ONLY); |
1563 | 7.04k | } |
1564 | | |
1565 | 0 | bool Http3MockServer::handshake_complete() const { return impl_ != nullptr && impl_->handshake_complete(); } |
1566 | | |
1567 | 0 | bool Http3MockServer::request_headers_received() const { return impl_ != nullptr && impl_->request_headers_received(); } |
1568 | | |
1569 | 0 | std::size_t Http3MockServer::executed_action_count() const { |
1570 | 0 | return impl_ == nullptr ? 0 : impl_->executed_action_count(); |
1571 | 0 | } |
1572 | | |
1573 | 0 | std::uint16_t Http3MockServer::server_port() const { return impl_ == nullptr ? 0 : impl_->server_port(); } |
1574 | | |
1575 | 6.80k | curl_socket_t Http3MockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* address) { |
1576 | 6.80k | return impl_ == nullptr ? CURL_SOCKET_BAD : impl_->OpenSocket(purpose, address); |
1577 | 6.80k | } |
1578 | | |
1579 | | SocketSetupDisposition Http3MockServer::GetSocketSetupDisposition(curl_socket_t /*curlfd*/, |
1580 | 6.80k | curlsocktype /*purpose*/) const { |
1581 | 6.80k | return SocketSetupDisposition::kNeedsSetup; |
1582 | 6.80k | } |
1583 | | |
1584 | 7.04k | void Http3MockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) { |
1585 | 7.04k | (void)easy; |
1586 | 7.04k | if (impl_ == nullptr) { |
1587 | 0 | return; |
1588 | 0 | } |
1589 | 7.04k | impl_->BeginScenario(scenario); |
1590 | | |
1591 | 7.04k | int still_running = 1; |
1592 | 7.04k | int idle_iterations = 0; |
1593 | 7.04k | if (MultiSocketDriver* driver = multi_socket_driver(); driver != nullptr) { |
1594 | | // Alternate curl readiness processing with one bounded peer turn. Neither |
1595 | | // side blocks, so an idle limit prevents a malformed case spinning forever. |
1596 | 7.04k | CURLMcode result = driver->Start(&still_running); |
1597 | 141k | for (int iteration = 0; result == CURLM_OK && iteration < kMaxDriveIterations && still_running != 0; ++iteration) { |
1598 | 137k | std::size_t progress = impl_->DrivePeerTurn(); |
1599 | 137k | const MultiSocketDriver::DriveResult drive_result = driver->DriveReady(&still_running); |
1600 | 137k | result = drive_result.code; |
1601 | 137k | progress += drive_result.made_progress ? 1U : 0U; |
1602 | 137k | if (progress != 0) { |
1603 | 35.1k | idle_iterations = 0; |
1604 | 101k | } else if (++idle_iterations >= kHttp3IdleIterations) { |
1605 | 2.81k | break; |
1606 | 2.81k | } |
1607 | 137k | } |
1608 | 7.04k | return; |
1609 | 7.04k | } |
1610 | | |
1611 | | // The fallback exercises curl's public multi-perform API while preserving |
1612 | | // the same peer-first/idle-limit behavior as the socket-action driver. |
1613 | 0 | for (int iteration = 0; iteration < kMaxDriveIterations && still_running != 0; ++iteration) { |
1614 | 0 | const int running_before = still_running; |
1615 | 0 | const CURLMcode result = curl_multi_perform(multi, &still_running); |
1616 | 0 | if (result != CURLM_OK) { |
1617 | 0 | break; |
1618 | 0 | } |
1619 | | |
1620 | 0 | std::size_t progress = still_running == running_before ? 0U : 1U; |
1621 | 0 | progress += impl_->DrivePeerTurn(); |
1622 | 0 | if (progress != 0) { |
1623 | 0 | idle_iterations = 0; |
1624 | 0 | } else if (++idle_iterations >= kHttp3IdleIterations) { |
1625 | 0 | break; |
1626 | 0 | } |
1627 | 0 | } |
1628 | 0 | } |
1629 | | |
1630 | | } // namespace proto_fuzzer |