Add kDSNI connection option to permit sending debugging_sni despite ECH GREASE

Protected by QUIC connection option `DSNI`.

PiperOrigin-RevId: 831956111
diff --git a/quiche/quic/core/crypto/crypto_protocol.h b/quiche/quic/core/crypto/crypto_protocol.h
index a3fc864..47a5d8f 100644
--- a/quiche/quic/core/crypto/crypto_protocol.h
+++ b/quiche/quic/core/crypto/crypto_protocol.h
@@ -419,6 +419,8 @@
                                // it as the initial rtt.
 DEFINE_STATIC_QUIC_TAG(SNI);   // Server name
                                // indication
+DEFINE_STATIC_QUIC_TAG(DSNI);  // Enables the debugging_sni transport parameter
+                               // to be sent alongside ECH GREASE, but not ECH.
 DEFINE_STATIC_QUIC_TAG(PUBS);  // Public key values
 DEFINE_STATIC_QUIC_TAG(SCID);  // Server config id
 DEFINE_STATIC_QUIC_TAG(OBIT);  // Server orbit.
diff --git a/quiche/quic/core/tls_client_handshaker.cc b/quiche/quic/core/tls_client_handshaker.cc
index 8af37c8..85619b3 100644
--- a/quiche/quic/core/tls_client_handshaker.cc
+++ b/quiche/quic/core/tls_client_handshaker.cc
@@ -5,7 +5,7 @@
 #include "quiche/quic/core/tls_client_handshaker.h"
 
 #include <algorithm>
-#include <cstring>
+#include <cstdint>
 #include <limits>
 #include <memory>
 #include <optional>
@@ -13,17 +13,27 @@
 #include <utility>
 #include <vector>
 
-#include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
 #include "openssl/ssl.h"
+#include "quiche/quic/core/crypto/client_proof_source.h"
+#include "quiche/quic/core/crypto/crypto_handshake.h"
+#include "quiche/quic/core/crypto/crypto_protocol.h"
+#include "quiche/quic/core/crypto/proof_verifier.h"
 #include "quiche/quic/core/crypto/quic_crypto_client_config.h"
-#include "quiche/quic/core/crypto/quic_encrypter.h"
 #include "quiche/quic/core/crypto/transport_parameters.h"
+#include "quiche/quic/core/quic_crypto_client_stream.h"
+#include "quiche/quic/core/quic_data_writer.h"
+#include "quiche/quic/core/quic_error_codes.h"
+#include "quiche/quic/core/quic_server_id.h"
 #include "quiche/quic/core/quic_session.h"
 #include "quiche/quic/core/quic_types.h"
+#include "quiche/quic/core/quic_versions.h"
+#include "quiche/quic/core/tls_handshaker.h"
 #include "quiche/quic/platform/api/quic_bug_tracker.h"
 #include "quiche/quic/platform/api/quic_flags.h"
 #include "quiche/quic/platform/api/quic_hostname_utils.h"
+#include "quiche/quic/platform/api/quic_logging.h"
+#include "quiche/common/platform/api/quiche_logging.h"
 #include "quiche/common/quiche_text_utils.h"
 
 namespace quic {
@@ -289,11 +299,14 @@
   }
 
   // The `debugging_sni` field must not be sent when attempting Encrypted Client
-  // Hello (ECH) because it would reveal the real SNI in cleartext. Further, it
-  // must not be sent when GREASEing ECH because that would reveal to observers
-  // whether the ECH was real or GREASE.
+  // Hello (ECH) because it would reveal the real SNI in cleartext. When only
+  // ECH GREASE will be sent, it's still sensible to omit `debugging_sni`
+  // because it would enable observers to discriminate real ECH from GREASE. The
+  // `kDSNI` option forces `debugging_sni` to be sent despite ECH GREASE.
   if (!tls_connection_.ssl_config().ech_config_list.empty() ||
-      tls_connection_.ssl_config().ech_grease_enabled) {
+      (tls_connection_.ssl_config().ech_grease_enabled &&
+       !session_->config()->HasClientSentConnectionOption(
+           kDSNI, Perspective::IS_CLIENT))) {
     params.debugging_sni.reset();
   }
 
diff --git a/quiche/quic/core/tls_client_handshaker_test.cc b/quiche/quic/core/tls_client_handshaker_test.cc
index 730e769..723c270 100644
--- a/quiche/quic/core/tls_client_handshaker_test.cc
+++ b/quiche/quic/core/tls_client_handshaker_test.cc
@@ -3,6 +3,8 @@
 // found in the LICENSE file.
 
 #include <algorithm>
+#include <cstddef>
+#include <cstdint>
 #include <memory>
 #include <optional>
 #include <string>
@@ -10,22 +12,25 @@
 #include <vector>
 
 #include "absl/base/macros.h"
+#include "absl/strings/string_view.h"
 #include "openssl/aead.h"
 #include "openssl/hpke.h"
 #include "openssl/ssl.h"
 #include "openssl/tls1.h"
-#include "quiche/quic/core/crypto/quic_decrypter.h"
-#include "quiche/quic/core/crypto/quic_encrypter.h"
+#include "quiche/quic/core/crypto/crypto_handshake_message.h"
+#include "quiche/quic/core/crypto/crypto_protocol.h"
+#include "quiche/quic/core/crypto/proof_verifier.h"
+#include "quiche/quic/core/crypto/quic_compressed_certs_cache.h"
+#include "quiche/quic/core/crypto/quic_crypto_client_config.h"
 #include "quiche/quic/core/crypto/transport_parameters.h"
 #include "quiche/quic/core/quic_connection.h"
+#include "quiche/quic/core/quic_constants.h"
 #include "quiche/quic/core/quic_error_codes.h"
-#include "quiche/quic/core/quic_packets.h"
 #include "quiche/quic/core/quic_server_id.h"
+#include "quiche/quic/core/quic_time.h"
 #include "quiche/quic/core/quic_types.h"
-#include "quiche/quic/core/quic_utils.h"
 #include "quiche/quic/core/quic_versions.h"
 #include "quiche/quic/platform/api/quic_expect_bug.h"
-#include "quiche/quic/platform/api/quic_flags.h"
 #include "quiche/quic/platform/api/quic_test.h"
 #include "quiche/quic/test_tools/crypto_test_utils.h"
 #include "quiche/quic/test_tools/quic_connection_peer.h"
@@ -34,7 +39,6 @@
 #include "quiche/quic/test_tools/quic_test_utils.h"
 #include "quiche/quic/test_tools/simple_session_cache.h"
 #include "quiche/quic/tools/fake_proof_verifier.h"
-#include "quiche/common/test_tools/quiche_test_utils.h"
 
 using testing::_;
 using testing::Eq;
@@ -983,6 +987,35 @@
   EXPECT_EQ(debug_visitor.debugging_sni(), std::nullopt);
 }
 
+TEST_P(TlsClientHandshakerTest, DebuggingSniDisabledByECHAndNotSavedByDSNI) {
+  ssl_config_.emplace();
+  bssl::UniquePtr<SSL_ECH_KEYS> ech_keys =
+      MakeTestEchKeys("public-name.example", /*max_name_len=*/64,
+                      &ssl_config_->ech_config_list);
+  ASSERT_TRUE(ech_keys);
+
+  // Configure the server to use the test ECH keys.
+  ASSERT_TRUE(
+      SSL_CTX_set1_ech_keys(server_crypto_config_->ssl_ctx(), ech_keys.get()));
+
+  // Recreate the client to pick up the new `ssl_config_`.
+  CreateConnection();
+  session_->config()->SetDebuggingSniToSend("secret.example");
+  session_->config()->AddConnectionOptionsToSend({kDSNI});
+
+  DebuggingSniExtractor debug_visitor;
+  connection_->set_debug_visitor(&debug_visitor);
+
+  // The handshake should complete and negotiate ECH.
+  CompleteCryptoHandshake();
+  EXPECT_EQ(PROTOCOL_TLS1_3, stream()->handshake_protocol());
+  EXPECT_TRUE(stream()->encryption_established());
+  EXPECT_TRUE(stream()->one_rtt_keys_available());
+  EXPECT_TRUE(stream()->crypto_negotiated_params().encrypted_client_hello);
+
+  EXPECT_EQ(debug_visitor.debugging_sni(), std::nullopt);
+}
+
 TEST_P(TlsClientHandshakerTest, DebuggingSniDisabledByECHGrease) {
   ssl_config_.emplace();
   ssl_config_->ech_grease_enabled = true;
@@ -1018,6 +1051,42 @@
   EXPECT_EQ(debug_visitor.debugging_sni(), std::nullopt);
 }
 
+TEST_P(TlsClientHandshakerTest, DebuggingSniDisabledByECHGreaseButSavedByDSNI) {
+  ssl_config_.emplace();
+  ssl_config_->ech_grease_enabled = true;
+  CreateConnection();
+  session_->config()->SetDebuggingSniToSend("secret.example");
+  session_->config()->AddConnectionOptionsToSend({kDSNI});
+
+  DebuggingSniExtractor debug_visitor;
+  connection_->set_debug_visitor(&debug_visitor);
+
+  stream()->CryptoConnect();
+
+  // Add a DoS callback on the server, to test that the client sent a GREASE
+  // message. This is a bit of a hack. TlsServerHandshaker already configures
+  // the certificate selection callback, but does not usefully expose any way
+  // for tests to inspect the ClientHello. So, instead, we register a different
+  // callback that also gets the ClientHello.
+  static bool callback_ran;
+  callback_ran = false;
+  SSL_CTX_set_dos_protection_cb(
+      server_crypto_config_->ssl_ctx(),
+      [](const SSL_CLIENT_HELLO* client_hello) -> int {
+        const uint8_t* data;
+        size_t len;
+        EXPECT_TRUE(SSL_early_callback_ctx_extension_get(
+            client_hello, TLSEXT_TYPE_encrypted_client_hello, &data, &len));
+        callback_ran = true;
+        return 1;
+      });
+
+  CompleteCryptoHandshake();
+  EXPECT_TRUE(callback_ran);
+
+  EXPECT_EQ(debug_visitor.debugging_sni(), "secret.example");
+}
+
 TEST_P(TlsClientHandshakerTest, DebuggingSniSentWithoutECH) {
   CreateConnection();
   session_->config()->SetDebuggingSniToSend("secret.example");