14#include <netinet/in.h>
15#include <sys/socket.h>
27#include "proto_fuzzer/tls_test_credentials.h"
29namespace proto_fuzzer {
33constexpr char kDohPath[] =
"/dns-query";
34constexpr unsigned short kDnsQtypeA = 1;
35constexpr unsigned short kDnsQtypeAAAA = 28;
36constexpr unsigned short kDnsQtypeHTTPS = 65;
37constexpr std::size_t kDnsHeaderSize = 12;
38constexpr std::size_t kDnsAnswerHeaderSize = 12;
43bool EchNeedsPendingHttpsRr(
const curl::fuzzer::proto::Scenario& scenario) {
45 for (std::size_t index = 0; index < option_count; ++index) {
46 const auto& option = scenario.options(
static_cast<int>(index));
47 if (option.option_id() != curl::fuzzer::proto::CURLOPT_ECH) {
50 const std::string& value = option.string_value();
51 if (value ==
"true" || value ==
"hard" || value.rfind(
"pn:", 0) == 0) {
62void SaveEnvironment(
const char* name, std::string* value,
bool* was_set) {
65 const char* current = std::getenv(name);
66 if (current !=
nullptr) {
67 value->assign(current);
77void RestoreEnvironment(
const char* name,
const std::string& value,
bool was_set) {
79 (void)setenv(name, value.c_str(), 1);
88bool MakeNonBlocking(
int fd) {
89 const int flags = fcntl(fd, F_GETFL, 0);
90 return flags >= 0 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) >= 0;
96std::size_t FindHeaderEnd(
const std::string& request) {
97 const std::size_t end = request.find(
"\r\n\r\n");
98 return end == std::string::npos ? std::string::npos : end + 4;
106bool ParseContentLength(
const std::string& request, std::size_t header_end, std::size_t* content_length) {
107 std::size_t position = 0;
108 while (position + 1 < header_end) {
109 const std::size_t line_end = request.find(
"\r\n", position);
110 if (line_end == std::string::npos || line_end >= header_end) {
113 constexpr char kName[] =
"content-length:";
114 const std::size_t name_size =
sizeof(kName) - 1;
115 if ((line_end - position) > name_size) {
117 for (std::size_t index = 0; index < name_size; ++index) {
118 const char actual = request[position + index];
119 const char expected = kName[index];
120 const char folded = (actual >=
'A' && actual <= 'Z') ? static_cast<char>(actual -
'A' +
'a') : actual;
121 if (folded != expected) {
127 const char* value = request.c_str() + position + name_size;
128 char* parsed_end =
nullptr;
129 const unsigned long parsed = std::strtoul(value, &parsed_end, 10);
130 if (parsed_end != value) {
131 *content_length =
static_cast<std::size_t
>(parsed);
136 position = line_end + 2;
150bool BuildDnsResponse(
const std::string& query, std::string* response,
bool* httpsrr) {
151 const auto* bytes =
reinterpret_cast<const unsigned char*
>(query.data());
152 const std::size_t query_len = query.size();
153 if (query_len < kDnsHeaderSize) {
157 std::size_t position = kDnsHeaderSize;
158 while (position < query_len) {
159 const unsigned char label = bytes[position];
167 if ((position + 1 + label) > query_len) {
170 position +=
static_cast<std::size_t
>(1) + label;
172 if ((position + 4) > query_len) {
176 const std::size_t qname_end = position;
177 const unsigned short qtype =
static_cast<unsigned short>((bytes[qname_end] << 8) | bytes[qname_end + 1]);
178 const std::size_t question_len = (qname_end + 4) - kDnsHeaderSize;
179 const bool has_answer = qtype == kDnsQtypeA || qtype == kDnsQtypeAAAA || qtype == kDnsQtypeHTTPS;
181 std::size_t rdlength = 0;
198 const std::size_t total = kDnsHeaderSize + question_len + (has_answer ? (kDnsAnswerHeaderSize + rdlength) : 0);
203 response->assign(total,
'\0');
204 auto* out =
reinterpret_cast<unsigned char*
>(response->data());
208 out[3] = has_answer ? 0x80 : 0x84;
210 out[7] = has_answer ? 1 : 0;
211 std::memcpy(&out[kDnsHeaderSize], &bytes[kDnsHeaderSize], question_len);
214 unsigned char* answer = &out[kDnsHeaderSize + question_len];
217 *answer++ =
static_cast<unsigned char>(qtype >> 8);
218 *answer++ =
static_cast<unsigned char>(qtype & 0xff);
226 *answer++ =
static_cast<unsigned char>(rdlength);
235 std::memset(answer, 0, 16);
238 case kDnsQtypeHTTPS: {
239 static const unsigned char kHttpsRecord[] = {
247 std::memcpy(answer, kHttpsRecord,
sizeof(kHttpsRecord));
265void DohHttpsRrMockServer::DohConnection::ObserveIncomingData(
const unsigned char* data, std::size_t size) {
266 if (request_overflow || data ==
nullptr || request.size() > kMaxRequestBytes ||
267 size > kMaxRequestBytes - request.size()) {
268 request_overflow =
true;
271 request.append(
reinterpret_cast<const char*
>(data), size);
281 option_probe_mode_(scenario.doh().option_probe_mode()),
282 transport_mode_(scenario.doh().transport_mode()),
283 diagnostic_stream_(nullptr),
284 tls_path_fixture_(nullptr),
285 internal_ssl_ctx_callback_count_(0),
287 transport_mode_ == curl::fuzzer::proto::DOH_TRANSPORT_HTTPS
290 doh_request_count_(0),
291 httpsrr_answer_count_(0),
292 response_mode_(scenario.doh().response_mode()),
293 defer_httpsrr_(EchNeedsPendingHttpsRr(scenario)),
295 proxy_bypass_active_(false),
296 lower_no_proxy_was_set_(false),
297 upper_no_proxy_was_set_(false) {
298 if (option_probe_mode_ == curl::fuzzer::proto::DOH_OPTION_PROBE_CUSTOM_STDERR) {
299 diagnostic_stream_ = std::tmpfile();
300 }
else if (option_probe_mode_ == curl::fuzzer::proto::DOH_OPTION_PROBE_TLS_PATHS) {
301 tls_path_fixture_ = std::make_unique<BoundedAnonymousInputFile>(
sizeof(tls_test_credentials::kCertificatePem) - 1);
302 (void)tls_path_fixture_->Write(
reinterpret_cast<const std::uint8_t*
>(tls_test_credentials::kCertificatePem),
303 sizeof(tls_test_credentials::kCertificatePem) - 1);
312 if (diagnostic_stream_ !=
nullptr) {
313 std::fclose(diagnostic_stream_);
322 if (diagnostic_stream_ ==
nullptr || std::fflush(diagnostic_stream_) != 0) {
325 const int fd = fileno(diagnostic_stream_);
329 std::array<char, kMaxDiagnosticBytes> buffer{};
332 size = pread(fd, buffer.data(), buffer.size(), 0);
333 }
while (size < 0 && errno == EINTR);
334 return size > 0 ? std::string(buffer.data(),
static_cast<std::size_t
>(size)) : std::string();
339 return tls_path_fixture_ !=
nullptr && tls_path_fixture_->path() !=
nullptr;
350 if (easy_ ==
nullptr) {
353 if (!ConfigureHttpsOptions()) {
356 if (!StartListener()) {
359 if (curl_easy_setopt(easy_, CURLOPT_DOH_URL, doh_url_.c_str()) != CURLE_OK) {
363 if (!BeginProxyBypass()) {
377 if (easy_ ==
nullptr || doh_url_.empty() || !proxy_bypass_active_) {
380 if (!ConfigureHttpsOptions()) {
383 if (curl_easy_setopt(easy_, CURLOPT_DOH_URL, doh_url_.c_str()) != CURLE_OK) {
386 setup_ok_ = curl_easy_setopt(easy_, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS) == CURLE_OK;
409 ServiceDohConnections();
415bool DohHttpsRrMockServer::StartListener() {
416 if (listener_ >= 0) {
420 struct sockaddr_in address;
421 std::memset(&address, 0,
sizeof(address));
422 address.sin_family = AF_INET;
423 address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
424 address.sin_port = 0;
426 const int fd = socket(AF_INET, SOCK_STREAM, 0);
431 (void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse,
sizeof(reuse));
432 if (bind(fd,
reinterpret_cast<struct sockaddr*
>(&address),
sizeof(address)) < 0 || listen(fd, 8) < 0 ||
433 !MakeNonBlocking(fd)) {
438 socklen_t address_len =
sizeof(address);
439 if (getsockname(fd,
reinterpret_cast<struct sockaddr*
>(&address), &address_len) < 0) {
445 const char* scheme = transport_mode_ == curl::fuzzer::proto::DOH_TRANSPORT_HTTPS ?
"https" :
"http";
447 std::string(scheme) +
"://127.0.0.1:" + std::to_string(
static_cast<int>(ntohs(address.sin_port))) + kDohPath;
457bool DohHttpsRrMockServer::ConfigureHttpsOptions() {
458 if (transport_mode_ != curl::fuzzer::proto::DOH_TRANSPORT_HTTPS) {
462 bool configured =
true;
463 configured = curl_easy_setopt(easy_, CURLOPT_DOH_SSL_VERIFYHOST, 0L) == CURLE_OK && configured;
464 configured = curl_easy_setopt(easy_, CURLOPT_DOH_SSL_VERIFYPEER, 1L) == CURLE_OK && configured;
465 configured = curl_easy_setopt(easy_, CURLOPT_CERTINFO, 1L) == CURLE_OK && configured;
466 configured = curl_easy_setopt(easy_, CURLOPT_NOSIGNAL, 1L) == CURLE_OK && configured;
468 switch (option_probe_mode_) {
469 case curl::fuzzer::proto::DOH_OPTION_PROBE_CUSTOM_STDERR:
470 if (diagnostic_stream_ ==
nullptr) {
473 configured = curl_easy_setopt(easy_, CURLOPT_STDERR, diagnostic_stream_) == CURLE_OK && configured;
475 case curl::fuzzer::proto::DOH_OPTION_PROBE_TLS_PATHS: {
476 const char* path = tls_path_fixture_ ==
nullptr ? nullptr : tls_path_fixture_->path();
477 if (path ==
nullptr) {
480 configured = curl_easy_setopt(easy_, CURLOPT_CAINFO, path) == CURLE_OK && configured;
481 configured = curl_easy_setopt(easy_, CURLOPT_CAPATH, path) == CURLE_OK && configured;
482 configured = curl_easy_setopt(easy_, CURLOPT_CRLFILE, path) == CURLE_OK && configured;
485 case curl::fuzzer::proto::DOH_OPTION_PROBE_SSL_CTX:
487 curl_easy_setopt(easy_, CURLOPT_SSL_CTX_FUNCTION, &DohHttpsRrMockServer::ObserveSslContext) == CURLE_OK &&
489 configured = curl_easy_setopt(easy_, CURLOPT_SSL_CTX_DATA,
this) == CURLE_OK && configured;
491 case curl::fuzzer::proto::DOH_OPTION_PROBE_DEFAULT:
504CURLcode DohHttpsRrMockServer::ObserveSslContext(CURL* easy,
void* ,
void* user_data) {
506 if (server !=
nullptr && easy != server->easy_) {
507 ++server->internal_ssl_ctx_callback_count_;
513void DohHttpsRrMockServer::StopListener() {
514 if (listener_ >= 0) {
518 for (DohConnection&
connection : connections_) {
527bool DohHttpsRrMockServer::BeginProxyBypass() {
528 if (proxy_bypass_active_) {
531 SaveEnvironment(
"no_proxy", &saved_no_proxy_lower_, &lower_no_proxy_was_set_);
532 SaveEnvironment(
"NO_PROXY", &saved_no_proxy_upper_, &upper_no_proxy_was_set_);
533 proxy_bypass_active_ =
true;
534 if (setenv(
"no_proxy",
"*", 1) != 0 || setenv(
"NO_PROXY",
"*", 1) != 0) {
542void DohHttpsRrMockServer::EndProxyBypass() {
543 if (!proxy_bypass_active_) {
546 RestoreEnvironment(
"no_proxy", saved_no_proxy_lower_, lower_no_proxy_was_set_);
547 RestoreEnvironment(
"NO_PROXY", saved_no_proxy_upper_, upper_no_proxy_was_set_);
548 saved_no_proxy_lower_.clear();
549 saved_no_proxy_upper_.clear();
550 lower_no_proxy_was_set_ =
false;
551 upper_no_proxy_was_set_ =
false;
552 proxy_bypass_active_ =
false;
557void DohHttpsRrMockServer::CloseConnection(DohConnection* connection) {
572void DohHttpsRrMockServer::AcceptConnections() {
577 const int fd = accept(listener_,
nullptr,
nullptr);
581 if (!MakeNonBlocking(fd)) {
585 DohConnection* slot =
nullptr;
586 for (DohConnection&
connection : connections_) {
592 if (slot ==
nullptr) {
596 slot->request.clear();
597 slot->response.clear();
598 slot->response_offset = 0;
599 slot->deferred_write_turns = 0;
600 slot->writing =
false;
601 slot->request_overflow =
false;
602 if (transport_mode_ == curl::fuzzer::proto::DOH_TRANSPORT_HTTPS) {
603 if (doh_tls_transport_ ==
nullptr) {
607 std::unique_ptr<MockConnection> tls = doh_tls_transport_->CreateAcceptedConnection(fd);
608 if (tls ==
nullptr || !tls->ok()) {
611 tls->SetIncomingDataObserver(slot);
612 slot->tls = std::move(tls);
621void DohHttpsRrMockServer::ServiceDohConnections() {
624 for (DohConnection&
connection : connections_) {
638 const ssize_t received = recv(
connection.fd, buffer,
sizeof(buffer), 0);
640 if (
connection.request.size() +
static_cast<std::size_t
>(received) > kMaxRequestBytes) {
644 connection.request.append(buffer,
static_cast<std::size_t
>(received));
651 if (errno == EINTR) {
654 if (errno == EAGAIN || errno == EWOULDBLOCK) {
681 if (!
connection.tls->WriteAll(
reinterpret_cast<const unsigned char*
>(
connection.response.data()),
701 connection.response_offset +=
static_cast<std::size_t
>(sent);
704 if (sent < 0 && errno == EINTR) {
707 if (sent < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
724bool DohHttpsRrMockServer::CompleteRequest(DohConnection* connection) {
725 const std::size_t header_end = FindHeaderEnd(
connection->request);
726 if (header_end == std::string::npos) {
730 std::size_t content_length = 0;
731 const bool has_length = ParseContentLength(
connection->request, header_end, &content_length);
733 if (content_length > (kMaxRequestBytes - header_end)) {
737 if (
connection->request.size() < header_end + content_length) {
741 content_length =
connection->request.size() - header_end;
744 if (response_mode_ == curl::fuzzer::proto::DOH_RESPONSE_MODE_TRANSPORT_ERROR) {
745 ++doh_request_count_;
750 const std::string body =
connection->request.substr(header_end, content_length);
752 bool httpsrr =
false;
753 if (!BuildDnsResponse(body, &dns, &httpsrr)) {
756 if (response_mode_ == curl::fuzzer::proto::DOH_RESPONSE_MODE_MALFORMED_DNS) {
764 if (response_mode_ == curl::fuzzer::proto::DOH_RESPONSE_MODE_HTTP_ERROR) {
770 (response_mode_ == curl::fuzzer::proto::DOH_RESPONSE_MODE_HTTP_ERROR ?
"HTTP/1.1 404 DoH\r\n"
771 :
"HTTP/1.1 200 DoH\r\n") +
773 "Content-Type: application/dns-message\r\n"
774 "Content-Length: ") +
775 std::to_string(dns.size()) +
777 "Connection: close\r\n"
780 if (
connection->response.size() > kMaxResponseBytes) {
785 connection->deferred_write_turns = httpsrr && defer_httpsrr_ ? 1 : 0;
786 ++doh_request_count_;
788 ++httpsrr_answer_count_;
void Install(CURL *easy) override
Bind the DoH listener, point CURLOPT_DOH_URL at it, and bypass ambient proxies for curl's internal ha...
void ConfigureRequestData(ScenarioRequestData *request_data) override
Reassert the listener URL and fixed H2 selection after the scenario's option prefix has run.
DohHttpsRrMockServer(const curl::fuzzer::proto::Scenario &scenario)
Construct an idle responder.
~DohHttpsRrMockServer() override
Release the loopback listener and restore the proxy environment before the base carrier tears down it...
void ObserveActiveTransfer(CURL *easy) override
Observe the outer transfer and advance every pending DoH exchange.
void RunLoop(CURLM *multi, CURL *easy, const curl::fuzzer::proto::Scenario &scenario) override
Refuse to enter libcurl when any local resolver setup step failed.
std::string custom_stderr_contents()
Read a bounded prefix without changing the stream position retained by libcurl.
bool tls_path_fixture_ready() const
H2OriginMockServer(curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain=curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_DEFAULT_EC, curl::fuzzer::proto::TlsGroupProfile group_profile=curl::fuzzer::proto::TLS_GROUP_X25519)
Construct an HTTP/2 TLS peer with the selected certificate chain.
void RunLoop(CURLM *multi, CURL *easy, const curl::fuzzer::proto::Scenario &scenario) override
Run the raw HTTP/2 response driver and probe server push and upkeep APIs.
void Install(CURL *easy) override
Keep transport selection out of the mutation grammar.
MockConnection * connection()
Builds HTTP headers or TELNET options, MIME state, and upload callbacks whose storage or userdata lib...
void ObserveActiveTransfer(CURL *easy) override
Query while the connection filters are attached.
Reusable TLS carrier for protocol-specific mock peers.
DoH responder that feeds a synthesized HTTPS resource record to curl's HTTPS-connect ALPN filter.
std::size_t RuntimeOptionCount(const curl::fuzzer::proto::Scenario &scenario)
Return the runtime-visible option prefix length.
Translates Scenario SetOption messages into curl_easy_setopt calls, plus the fixed baseline setopts t...
TlsApplicationProtocol
Application protocol selected by the in-process TLS peer.
@ kHttp11
Ordinary HTTPS scripts contain HTTP/1.1 response bytes.