| // Copyright (c) 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <iostream> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "absl/strings/str_cat.h" |
| #include "quiche/quic/core/crypto/quic_client_session_cache.h" |
| #include "quiche/quic/core/io/quic_default_event_loop.h" |
| #include "quiche/quic/core/io/quic_event_loop.h" |
| #include "quiche/quic/core/quic_default_clock.h" |
| #include "quiche/quic/core/quic_types.h" |
| #include "quiche/quic/core/quic_versions.h" |
| #include "quiche/quic/test_tools/quic_connection_peer.h" |
| #include "quiche/quic/test_tools/quic_session_peer.h" |
| #include "quiche/quic/tools/fake_proof_verifier.h" |
| #include "quiche/quic/tools/quic_default_client.h" |
| #include "quiche/quic/tools/quic_name_lookup.h" |
| #include "quiche/quic/tools/quic_url.h" |
| #include "quiche/common/platform/api/quiche_command_line_flags.h" |
| #include "quiche/common/platform/api/quiche_system_event_loop.h" |
| #include "quiche/spdy/core/http2_header_block.h" |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG(std::string, host, "", |
| "The IP or hostname to connect to."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG( |
| std::string, quic_version, "", |
| "The QUIC version to use. Defaults to most recent IETF QUIC version."); |
| |
| DEFINE_QUICHE_COMMAND_LINE_FLAG(int32_t, port, 0, "The port to connect to."); |
| |
| namespace quic { |
| |
| enum class Feature { |
| // First row of features ("table stakes") |
| // A version negotiation response is elicited and acted on. |
| kVersionNegotiation, |
| // The handshake completes successfully. |
| kHandshake, |
| // Stream data is being exchanged and ACK'ed. |
| kStreamData, |
| // The connection close procedcure completes with a zero error code. |
| kConnectionClose, |
| // The connection was established using TLS resumption. |
| kResumption, |
| // 0-RTT data is being sent and acted on. |
| kZeroRtt, |
| // A RETRY packet was successfully processed. |
| kRetry, |
| // A handshake using a ClientHello that spans multiple packets completed |
| // successfully. |
| kQuantum, |
| |
| // Second row of features (anything else protocol-related) |
| // We switched to a different port and the server migrated to it. |
| kRebinding, |
| // One endpoint can update keys and its peer responds correctly. |
| kKeyUpdate, |
| |
| // Third row of features (H3 tests) |
| // An H3 transaction succeeded. |
| kHttp3, |
| // One or both endpoints insert entries into dynamic table and subsequenly |
| // reference them from header blocks. |
| kDynamicEntryReferenced, |
| }; |
| |
| char MatrixLetter(Feature f) { |
| switch (f) { |
| case Feature::kVersionNegotiation: |
| return 'V'; |
| case Feature::kHandshake: |
| return 'H'; |
| case Feature::kStreamData: |
| return 'D'; |
| case Feature::kConnectionClose: |
| return 'C'; |
| case Feature::kResumption: |
| return 'R'; |
| case Feature::kZeroRtt: |
| return 'Z'; |
| case Feature::kRetry: |
| return 'S'; |
| case Feature::kQuantum: |
| return 'Q'; |
| case Feature::kRebinding: |
| return 'B'; |
| case Feature::kKeyUpdate: |
| return 'U'; |
| case Feature::kHttp3: |
| return '3'; |
| case Feature::kDynamicEntryReferenced: |
| return 'd'; |
| } |
| } |
| |
| class QuicClientInteropRunner : QuicConnectionDebugVisitor { |
| public: |
| QuicClientInteropRunner() {} |
| |
| void InsertFeature(Feature feature) { features_.insert(feature); } |
| |
| std::set<Feature> features() const { return features_; } |
| |
| // Attempts a resumption using |client| by disconnecting and reconnecting. If |
| // resumption is successful, |features_| is modified to add |
| // Feature::kResumption to it, otherwise it is left unmodified. |
| void AttemptResumption(QuicDefaultClient* client, |
| const std::string& authority); |
| |
| void AttemptRequest(QuicSocketAddress addr, std::string authority, |
| QuicServerId server_id, ParsedQuicVersion version, |
| bool test_version_negotiation, bool attempt_rebind, |
| bool attempt_multi_packet_chlo, bool attempt_key_update); |
| |
| // Constructs a Http2HeaderBlock containing the pseudo-headers needed to make |
| // a GET request to "/" on the hostname |authority|. |
| spdy::Http2HeaderBlock ConstructHeaderBlock(const std::string& authority); |
| |
| // Sends an HTTP request represented by |header_block| using |client|. |
| void SendRequest(QuicDefaultClient* client, |
| const spdy::Http2HeaderBlock& header_block); |
| |
| void OnConnectionCloseFrame(const QuicConnectionCloseFrame& frame) override { |
| switch (frame.close_type) { |
| case GOOGLE_QUIC_CONNECTION_CLOSE: |
| QUIC_LOG(ERROR) << "Received unexpected GoogleQUIC connection close"; |
| break; |
| case IETF_QUIC_TRANSPORT_CONNECTION_CLOSE: |
| if (frame.wire_error_code == NO_IETF_QUIC_ERROR) { |
| InsertFeature(Feature::kConnectionClose); |
| } else { |
| QUIC_LOG(ERROR) << "Received transport connection close " |
| << QuicIetfTransportErrorCodeString( |
| static_cast<QuicIetfTransportErrorCodes>( |
| frame.wire_error_code)); |
| } |
| break; |
| case IETF_QUIC_APPLICATION_CONNECTION_CLOSE: |
| if (frame.wire_error_code == 0) { |
| InsertFeature(Feature::kConnectionClose); |
| } else { |
| QUIC_LOG(ERROR) << "Received application connection close " |
| << frame.wire_error_code; |
| } |
| break; |
| } |
| } |
| |
| void OnVersionNegotiationPacket( |
| const QuicVersionNegotiationPacket& /*packet*/) override { |
| InsertFeature(Feature::kVersionNegotiation); |
| } |
| |
| private: |
| std::set<Feature> features_; |
| }; |
| |
| void QuicClientInteropRunner::AttemptResumption(QuicDefaultClient* client, |
| const std::string& authority) { |
| client->Disconnect(); |
| if (!client->Initialize()) { |
| QUIC_LOG(ERROR) << "Failed to reinitialize client"; |
| return; |
| } |
| if (!client->Connect()) { |
| return; |
| } |
| |
| bool zero_rtt_attempt = !client->session()->OneRttKeysAvailable(); |
| |
| spdy::Http2HeaderBlock header_block = ConstructHeaderBlock(authority); |
| SendRequest(client, header_block); |
| |
| if (!client->session()->OneRttKeysAvailable()) { |
| return; |
| } |
| |
| if (static_cast<QuicCryptoClientStream*>( |
| test::QuicSessionPeer::GetMutableCryptoStream(client->session())) |
| ->IsResumption()) { |
| InsertFeature(Feature::kResumption); |
| } |
| if (static_cast<QuicCryptoClientStream*>( |
| test::QuicSessionPeer::GetMutableCryptoStream(client->session())) |
| ->EarlyDataAccepted() && |
| zero_rtt_attempt && client->latest_response_code() != -1) { |
| InsertFeature(Feature::kZeroRtt); |
| } |
| } |
| |
| void QuicClientInteropRunner::AttemptRequest( |
| QuicSocketAddress addr, std::string authority, QuicServerId server_id, |
| ParsedQuicVersion version, bool test_version_negotiation, |
| bool attempt_rebind, bool attempt_multi_packet_chlo, |
| bool attempt_key_update) { |
| ParsedQuicVersionVector versions = {version}; |
| if (test_version_negotiation) { |
| versions.insert(versions.begin(), QuicVersionReservedForNegotiation()); |
| } |
| |
| auto proof_verifier = std::make_unique<FakeProofVerifier>(); |
| auto session_cache = std::make_unique<QuicClientSessionCache>(); |
| QuicConfig config; |
| QuicTime::Delta timeout = QuicTime::Delta::FromSeconds(20); |
| config.SetIdleNetworkTimeout(timeout); |
| if (attempt_multi_packet_chlo) { |
| // Make the ClientHello span multiple packets by adding a custom transport |
| // parameter. |
| constexpr auto kCustomParameter = |
| static_cast<TransportParameters::TransportParameterId>(0x173E); |
| std::string custom_value(2000, '?'); |
| config.custom_transport_parameters_to_send()[kCustomParameter] = |
| custom_value; |
| } |
| std::unique_ptr<QuicEventLoop> event_loop = |
| GetDefaultEventLoop()->Create(QuicDefaultClock::Get()); |
| auto client = std::make_unique<QuicDefaultClient>( |
| addr, server_id, versions, config, event_loop.get(), |
| std::move(proof_verifier), std::move(session_cache)); |
| client->set_connection_debug_visitor(this); |
| if (!client->Initialize()) { |
| QUIC_LOG(ERROR) << "Failed to initialize client"; |
| return; |
| } |
| const bool connect_result = client->Connect(); |
| QuicConnection* connection = client->session()->connection(); |
| if (connection == nullptr) { |
| QUIC_LOG(ERROR) << "No QuicConnection object"; |
| return; |
| } |
| QuicConnectionStats client_stats = connection->GetStats(); |
| if (client_stats.retry_packet_processed) { |
| InsertFeature(Feature::kRetry); |
| } |
| if (test_version_negotiation && connection->version() == version) { |
| InsertFeature(Feature::kVersionNegotiation); |
| } |
| if (test_version_negotiation && !connect_result) { |
| // Failed to negotiate version, retry without version negotiation. |
| AttemptRequest(addr, authority, server_id, version, |
| /*test_version_negotiation=*/false, attempt_rebind, |
| attempt_multi_packet_chlo, attempt_key_update); |
| return; |
| } |
| if (!client->session()->OneRttKeysAvailable()) { |
| if (attempt_multi_packet_chlo) { |
| // Failed to handshake with multi-packet client hello, retry without it. |
| AttemptRequest(addr, authority, server_id, version, |
| test_version_negotiation, attempt_rebind, |
| /*attempt_multi_packet_chlo=*/false, attempt_key_update); |
| return; |
| } |
| return; |
| } |
| InsertFeature(Feature::kHandshake); |
| if (attempt_multi_packet_chlo) { |
| InsertFeature(Feature::kQuantum); |
| } |
| |
| spdy::Http2HeaderBlock header_block = ConstructHeaderBlock(authority); |
| SendRequest(client.get(), header_block); |
| |
| if (!client->connected()) { |
| return; |
| } |
| |
| if (client->latest_response_code() != -1) { |
| InsertFeature(Feature::kHttp3); |
| |
| if (client->client_session()->dynamic_table_entry_referenced()) { |
| InsertFeature(Feature::kDynamicEntryReferenced); |
| } |
| |
| if (attempt_rebind) { |
| // Now make a second request after switching to a different client port. |
| if (client->ChangeEphemeralPort()) { |
| client->SendRequestAndWaitForResponse(header_block, "", /*fin=*/true); |
| if (!client->connected()) { |
| // Rebinding does not work, retry without attempting it. |
| AttemptRequest(addr, authority, server_id, version, |
| test_version_negotiation, /*attempt_rebind=*/false, |
| attempt_multi_packet_chlo, attempt_key_update); |
| return; |
| } |
| InsertFeature(Feature::kRebinding); |
| |
| if (client->client_session()->dynamic_table_entry_referenced()) { |
| InsertFeature(Feature::kDynamicEntryReferenced); |
| } |
| } else { |
| QUIC_LOG(ERROR) << "Failed to change ephemeral port"; |
| } |
| } |
| |
| if (attempt_key_update) { |
| if (connection->IsKeyUpdateAllowed()) { |
| if (connection->InitiateKeyUpdate( |
| KeyUpdateReason::kLocalForInteropRunner)) { |
| client->SendRequestAndWaitForResponse(header_block, "", /*fin=*/true); |
| if (!client->connected()) { |
| // Key update does not work, retry without attempting it. |
| AttemptRequest(addr, authority, server_id, version, |
| test_version_negotiation, attempt_rebind, |
| attempt_multi_packet_chlo, |
| /*attempt_key_update=*/false); |
| return; |
| } |
| InsertFeature(Feature::kKeyUpdate); |
| } else { |
| QUIC_LOG(ERROR) << "Failed to initiate key update"; |
| } |
| } else { |
| QUIC_LOG(ERROR) << "Key update not allowed"; |
| } |
| } |
| } |
| |
| if (connection->connected()) { |
| connection->CloseConnection( |
| QUIC_NO_ERROR, "Graceful close", |
| ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET); |
| InsertFeature(Feature::kConnectionClose); |
| } |
| |
| AttemptResumption(client.get(), authority); |
| } |
| |
| spdy::Http2HeaderBlock QuicClientInteropRunner::ConstructHeaderBlock( |
| const std::string& authority) { |
| // Construct and send a request. |
| spdy::Http2HeaderBlock header_block; |
| header_block[":method"] = "GET"; |
| header_block[":scheme"] = "https"; |
| header_block[":authority"] = authority; |
| header_block[":path"] = "/"; |
| return header_block; |
| } |
| |
| void QuicClientInteropRunner::SendRequest( |
| QuicDefaultClient* client, const spdy::Http2HeaderBlock& header_block) { |
| client->set_store_response(true); |
| client->SendRequestAndWaitForResponse(header_block, "", /*fin=*/true); |
| |
| QuicConnection* connection = client->session()->connection(); |
| if (connection == nullptr) { |
| QUIC_LOG(ERROR) << "No QuicConnection object"; |
| return; |
| } |
| QuicConnectionStats client_stats = connection->GetStats(); |
| QuicSentPacketManager* sent_packet_manager = |
| test::QuicConnectionPeer::GetSentPacketManager(connection); |
| const bool received_forward_secure_ack = |
| sent_packet_manager != nullptr && |
| sent_packet_manager->GetLargestAckedPacket(ENCRYPTION_FORWARD_SECURE) |
| .IsInitialized(); |
| if (client_stats.stream_bytes_received > 0 && received_forward_secure_ack) { |
| InsertFeature(Feature::kStreamData); |
| } |
| } |
| |
| std::set<Feature> ServerSupport(std::string dns_host, std::string url_host, |
| int port, ParsedQuicVersion version) { |
| std::cout << "Attempting interop with version " << version << std::endl; |
| |
| // Build the client, and try to connect. |
| QuicSocketAddress addr = tools::LookupAddress(dns_host, absl::StrCat(port)); |
| if (!addr.IsInitialized()) { |
| QUIC_LOG(ERROR) << "Failed to resolve " << dns_host; |
| return std::set<Feature>(); |
| } |
| QuicServerId server_id(url_host, port, false); |
| std::string authority = absl::StrCat(url_host, ":", port); |
| |
| QuicClientInteropRunner runner; |
| |
| runner.AttemptRequest(addr, authority, server_id, version, |
| /*test_version_negotiation=*/true, |
| /*attempt_rebind=*/true, |
| /*attempt_multi_packet_chlo=*/true, |
| /*attempt_key_update=*/true); |
| |
| return runner.features(); |
| } |
| |
| } // namespace quic |
| |
| int main(int argc, char* argv[]) { |
| quiche::QuicheSystemEventLoop event_loop("quic_client"); |
| const char* usage = "Usage: quic_client_interop_test [options] [url]"; |
| |
| std::vector<std::string> args = |
| quiche::QuicheParseCommandLineFlags(usage, argc, argv); |
| if (args.size() > 1) { |
| quiche::QuichePrintCommandLineFlagHelp(usage); |
| exit(1); |
| } |
| std::string dns_host = quiche::GetQuicheCommandLineFlag(FLAGS_host); |
| std::string url_host = ""; |
| int port = quiche::GetQuicheCommandLineFlag(FLAGS_port); |
| |
| if (!args.empty()) { |
| quic::QuicUrl url(args[0], "https"); |
| url_host = url.host(); |
| if (dns_host.empty()) { |
| dns_host = url_host; |
| } |
| if (port == 0) { |
| port = url.port(); |
| } |
| } |
| if (port == 0) { |
| port = 443; |
| } |
| if (dns_host.empty()) { |
| quiche::QuichePrintCommandLineFlagHelp(usage); |
| exit(1); |
| } |
| if (url_host.empty()) { |
| url_host = dns_host; |
| } |
| |
| // Pick QUIC version to use. |
| quic::QuicVersionInitializeSupportForIetfDraft(); |
| quic::ParsedQuicVersion version = quic::UnsupportedQuicVersion(); |
| std::string quic_version_string = |
| quiche::GetQuicheCommandLineFlag(FLAGS_quic_version); |
| if (!quic_version_string.empty()) { |
| version = quic::ParseQuicVersionString(quic_version_string); |
| } else { |
| for (const quic::ParsedQuicVersion& vers : quic::AllSupportedVersions()) { |
| // Use the most recent IETF QUIC version. |
| if (vers.HasIetfQuicFrames() && vers.UsesHttp3() && vers.UsesTls()) { |
| version = vers; |
| break; |
| } |
| } |
| } |
| QUICHE_CHECK(version.IsKnown()); |
| QuicEnableVersion(version); |
| |
| auto supported_features = |
| quic::ServerSupport(dns_host, url_host, port, version); |
| std::cout << "Results for " << url_host << ":" << port << std::endl; |
| int current_row = 1; |
| for (auto feature : supported_features) { |
| if (current_row < 2 && feature >= quic::Feature::kRebinding) { |
| std::cout << std::endl; |
| current_row = 2; |
| } |
| if (current_row < 3 && feature >= quic::Feature::kHttp3) { |
| std::cout << std::endl; |
| current_row = 3; |
| } |
| std::cout << MatrixLetter(feature); |
| } |
| std::cout << std::endl; |
| } |