Support draft-07 version of WebTransport.

See <https://www.ietf.org/archive/id/draft-ietf-webtrans-http3-07.html#name-changes-between-draft-versi> for the draft version changelog.

This CL makes QUICHE capable of negotiating draft-07, and enforces the parameter constraints related to draft-07; specifically, the requirement to explicitly support extended CONNECT and HTTP datagrams (it also de-supports pre-RFC datagrams SETTINGS codepoint for the new version only).

Of non-backwards-compatible changes, our code is still missing WEBTRANSPORT_STREAMS position enforcement, and MAX_SESSIONS support.

PiperOrigin-RevId: 542260756
diff --git a/quiche/quic/core/http/http_constants.cc b/quiche/quic/core/http/http_constants.cc
index e585535..4bd8d62 100644
--- a/quiche/quic/core/http/http_constants.cc
+++ b/quiche/quic/core/http/http_constants.cc
@@ -20,6 +20,7 @@
     RETURN_STRING_LITERAL(SETTINGS_H3_DATAGRAM_DRAFT04);
     RETURN_STRING_LITERAL(SETTINGS_H3_DATAGRAM);
     RETURN_STRING_LITERAL(SETTINGS_WEBTRANS_DRAFT00);
+    RETURN_STRING_LITERAL(SETTINGS_WEBTRANS_MAX_SESSIONS_DRAFT07);
     RETURN_STRING_LITERAL(SETTINGS_ENABLE_CONNECT_PROTOCOL);
     RETURN_STRING_LITERAL(SETTINGS_ENABLE_METADATA);
   }
diff --git a/quiche/quic/core/http/http_constants.h b/quiche/quic/core/http/http_constants.h
index feec521..a8b5fd3 100644
--- a/quiche/quic/core/http/http_constants.h
+++ b/quiche/quic/core/http/http_constants.h
@@ -42,8 +42,9 @@
   SETTINGS_H3_DATAGRAM_DRAFT04 = 0xffd277,
   // RFC 9297.
   SETTINGS_H3_DATAGRAM = 0x33,
-  // draft-ietf-webtrans-http3-00
+  // draft-ietf-webtrans-http3
   SETTINGS_WEBTRANS_DRAFT00 = 0x2b603742,
+  SETTINGS_WEBTRANS_MAX_SESSIONS_DRAFT07 = 0xc671706a,
   // draft-ietf-httpbis-h3-websockets
   SETTINGS_ENABLE_CONNECT_PROTOCOL = 0x08,
   SETTINGS_ENABLE_METADATA = 0x4d44,
diff --git a/quiche/quic/core/http/quic_spdy_session.cc b/quiche/quic/core/http/quic_spdy_session.cc
index 0e18ebb..44fff6c 100644
--- a/quiche/quic/core/http/quic_spdy_session.cc
+++ b/quiche/quic/core/http/quic_spdy_session.cc
@@ -16,6 +16,7 @@
 #include "absl/strings/numbers.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
 #include "quiche/quic/core/http/http_constants.h"
 #include "quiche/quic/core/http/http_decoder.h"
 #include "quiche/quic/core/http/http_frames.h"
@@ -64,6 +65,8 @@
 // Only used for Google QUIC, not IETF QUIC.
 constexpr uint64_t kHpackEncoderDynamicTableSizeLimit = 16384;
 
+constexpr QuicStreamCount kDefaultMaxWebTransportSessions = 16;
+
 #define ENDPOINT \
   (perspective() == Perspective::IS_SERVER ? "Server: " : "Client: ")
 
@@ -537,7 +540,19 @@
     }
   }
   if (WillNegotiateWebTransport()) {
-    settings_.values[SETTINGS_WEBTRANS_DRAFT00] = 1;
+    WebTransportHttp3VersionSet versions =
+        LocallySupportedWebTransportVersions();
+    if (versions.IsSet(WebTransportHttp3Version::kDraft02)) {
+      settings_.values[SETTINGS_WEBTRANS_DRAFT00] = 1;
+    }
+    if (versions.IsSet(WebTransportHttp3Version::kDraft07)) {
+      QUICHE_BUG_IF(
+          WT_enabled_extended_connect_disabled,
+          perspective() == Perspective::IS_SERVER && !allow_extended_connect())
+          << "WebTransport enabled, but extended CONNECT is not";
+      settings_.values[SETTINGS_WEBTRANS_MAX_SESSIONS_DRAFT07] =
+          kDefaultMaxWebTransportSessions;
+    }
   }
   if (allow_extended_connect()) {
     settings_.values[SETTINGS_ENABLE_CONNECT_PROTOCOL] = 1;
@@ -889,7 +904,14 @@
   }
 }
 
-bool QuicSpdySession::ShouldNegotiateWebTransport() { return false; }
+bool QuicSpdySession::ShouldNegotiateWebTransport() const {
+  return LocallySupportedWebTransportVersions().Any();
+}
+
+WebTransportHttp3VersionSet
+QuicSpdySession::LocallySupportedWebTransportVersions() const {
+  return WebTransportHttp3VersionSet();
+}
 
 bool QuicSpdySession::WillNegotiateWebTransport() {
   return LocalHttpDatagramSupport() != HttpDatagramSupport::kNone &&
@@ -1019,6 +1041,10 @@
     }
   }
 
+  if (!ValidateWebTransportSettingsConsistency()) {
+    return false;
+  }
+
   // This is the last point in the connection when we can receive new SETTINGS
   // values (ALPS and settings from the session ticket come before, and only one
   // SETTINGS frame per connection is allowed).  Notify all the streams that are
@@ -1040,6 +1066,40 @@
   return true;
 }
 
+bool QuicSpdySession::ValidateWebTransportSettingsConsistency() {
+  // Only apply the following checks to draft-07 or later.
+  absl::optional<WebTransportHttp3Version> version =
+      NegotiatedWebTransportVersion();
+  if (!version.has_value() || *version == WebTransportHttp3Version::kDraft02) {
+    return true;
+  }
+
+  if (!allow_extended_connect_) {
+    CloseConnectionWithDetails(
+        QUIC_HTTP_INVALID_SETTING_VALUE,
+        "Negotiated use of WebTransport over HTTP/3 (draft-07 or later), but "
+        "failed to negotiate extended CONNECT");
+    return false;
+  }
+
+  if (http_datagram_support_ == HttpDatagramSupport::kDraft04) {
+    CloseConnectionWithDetails(
+        QUIC_HTTP_INVALID_SETTING_VALUE,
+        "WebTransport over HTTP/3 version draft-07 and beyond requires the "
+        "RFC version of HTTP datagrams");
+    return false;
+  }
+
+  if (http_datagram_support_ != HttpDatagramSupport::kRfc) {
+    CloseConnectionWithDetails(
+        QUIC_HTTP_INVALID_SETTING_VALUE,
+        "WebTransport over HTTP/3 requires HTTP datagrams support");
+    return false;
+  }
+
+  return true;
+}
+
 absl::optional<std::string> QuicSpdySession::OnSettingsFrameViaAlps(
     const SettingsFrame& frame) {
   QUICHE_DCHECK(VersionUsesHttp3(transport_version()));
@@ -1216,14 +1276,32 @@
           break;
         }
         QUIC_DVLOG(1) << ENDPOINT
-                      << "SETTINGS_ENABLE_WEBTRANSPORT received with value "
+                      << "SETTINGS_ENABLE_WEBTRANSPORT(02) received with value "
                       << value;
         if (!VerifySettingIsZeroOrOne(id, value)) {
           return false;
         }
-        peer_supports_webtransport_ = (value == 1);
-        if (perspective() == Perspective::IS_CLIENT && value == 1) {
-          allow_extended_connect_ = true;
+        if (value == 1) {
+          peer_web_transport_versions_.Set(WebTransportHttp3Version::kDraft02);
+          if (perspective() == Perspective::IS_CLIENT) {
+            allow_extended_connect_ = true;
+          }
+        }
+        break;
+      case SETTINGS_WEBTRANS_MAX_SESSIONS_DRAFT07:
+        if (!WillNegotiateWebTransport()) {
+          break;
+        }
+        QUIC_DVLOG(1)
+            << ENDPOINT
+            << "SETTINGS_WEBTRANS_MAX_SESSIONS_DRAFT07 received with value "
+            << value;
+        if (value > 0) {
+          peer_web_transport_versions_.Set(WebTransportHttp3Version::kDraft07);
+          if (perspective() == Perspective::IS_CLIENT) {
+            max_webtransport_sessions_[WebTransportHttp3Version::kDraft07] =
+                value;
+          }
         }
         break;
       default:
@@ -1699,7 +1777,15 @@
 
 bool QuicSpdySession::SupportsWebTransport() {
   return WillNegotiateWebTransport() && SupportsH3Datagram() &&
-         peer_supports_webtransport_ && allow_extended_connect_;
+         NegotiatedWebTransportVersion().has_value() && allow_extended_connect_;
+}
+
+absl::optional<WebTransportHttp3Version>
+QuicSpdySession::SupportedWebTransportVersion() {
+  if (!SupportsWebTransport()) {
+    return absl::nullopt;
+  }
+  return NegotiatedWebTransportVersion();
 }
 
 bool QuicSpdySession::SupportsH3Datagram() const {
diff --git a/quiche/quic/core/http/quic_spdy_session.h b/quiche/quic/core/http/quic_spdy_session.h
index 74ecd4f..627e226 100644
--- a/quiche/quic/core/http/quic_spdy_session.h
+++ b/quiche/quic/core/http/quic_spdy_session.h
@@ -29,6 +29,7 @@
 #include "quiche/quic/core/quic_stream_priority.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_export.h"
 #include "quiche/spdy/core/http2_frame_decoder_adapter.h"
@@ -124,6 +125,26 @@
                    // version.
 };
 
+// Versions of WebTransport over HTTP/3 protocol extension.
+enum class WebTransportHttp3Version : uint8_t {
+  // <https://www.ietf.org/archive/id/draft-ietf-webtrans-http3-02.html>
+  // The first version to be ever publicly shipped in Chrome. Sometimes referred
+  // to as "draft-00", since draft-02 was backwards-compatible with draft-00.
+  kDraft02,
+  // <https://www.ietf.org/archive/id/draft-ietf-webtrans-http3-07.html>
+  // See the changelog in the appendix for differences between draft-02 and
+  // draft-07.
+  kDraft07,
+};
+using WebTransportHttp3VersionSet = BitMask<WebTransportHttp3Version, uint8_t>;
+
+// Note that by default, WebTransport is not enabled. Thus, those are the
+// versions primarily used in the tools and unit tests.
+inline constexpr WebTransportHttp3VersionSet
+    kDefaultSupportedWebTransportVersions =
+        WebTransportHttp3VersionSet({WebTransportHttp3Version::kDraft02,
+                                     WebTransportHttp3Version::kDraft07});
+
 QUIC_EXPORT_PRIVATE std::string HttpDatagramSupportToString(
     HttpDatagramSupport http_datagram_support);
 QUIC_EXPORT_PRIVATE std::ostream& operator<<(
@@ -396,6 +417,10 @@
   // Indicates whether the HTTP/3 session supports WebTransport.
   bool SupportsWebTransport();
 
+  // If SupportsWebTransport() is true, returns the version of WebTransport
+  // currently in use (which is the highest version supported by both peers).
+  absl::optional<WebTransportHttp3Version> SupportedWebTransportVersion();
+
   // Indicates whether both the peer and us support HTTP/3 Datagrams.
   bool SupportsH3Datagram() const;
 
@@ -473,7 +498,9 @@
 
   // Indicates whether the underlying backend can accept and process
   // WebTransport sessions over HTTP/3.
-  virtual bool ShouldNegotiateWebTransport();
+  virtual WebTransportHttp3VersionSet LocallySupportedWebTransportVersions()
+      const;
+  bool ShouldNegotiateWebTransport() const;
 
   // Returns true if there are open HTTP requests.
   bool ShouldKeepConnectionAlive() const override;
@@ -565,6 +592,16 @@
 
   bool VerifySettingIsZeroOrOne(uint64_t id, uint64_t value);
 
+  // Computes the highest WebTransport version supported by both peers.
+  absl::optional<WebTransportHttp3Version> NegotiatedWebTransportVersion()
+      const {
+    return (LocallySupportedWebTransportVersions() &
+            peer_web_transport_versions_)
+        .Max();
+  }
+
+  bool ValidateWebTransportSettingsConsistency();
+
   std::unique_ptr<QpackEncoder> qpack_encoder_;
   std::unique_ptr<QpackDecoder> qpack_decoder_;
 
@@ -643,8 +680,8 @@
   // draft is in use for this session.
   HttpDatagramSupport http_datagram_support_ = HttpDatagramSupport::kNone;
 
-  // Whether the peer has indicated WebTransport support.
-  bool peer_supports_webtransport_ = false;
+  // WebTransport protocol versions supported by the peer.
+  WebTransportHttp3VersionSet peer_web_transport_versions_;
 
   // Whether the SETTINGS frame has been received on the control stream.
   bool settings_received_ = false;
@@ -661,6 +698,13 @@
   // On the server side, if true, advertise and accept extended CONNECT method.
   // On the client side, true if the peer advertised extended CONNECT.
   bool allow_extended_connect_;
+
+  // Since WebTransport is versioned by renumbering
+  // SETTINGS_WEBTRANSPORT_MAX_SESSIONS, the max sessions value depends on the
+  // version we end up picking.  This is only stored on the client, as the
+  // server cannot initiate WebTransport sessions.
+  absl::flat_hash_map<WebTransportHttp3Version, QuicStreamCount>
+      max_webtransport_sessions_;
 };
 
 }  // namespace quic
diff --git a/quiche/quic/core/http/quic_spdy_session_test.cc b/quiche/quic/core/http/quic_spdy_session_test.cc
index cf95ec7..ba1b6e3 100644
--- a/quiche/quic/core/http/quic_spdy_session_test.cc
+++ b/quiche/quic/core/http/quic_spdy_session_test.cc
@@ -379,8 +379,19 @@
                       GetEncryptionLevelToSendApplicationData());
   }
 
-  bool ShouldNegotiateWebTransport() override { return supports_webtransport_; }
-  void set_supports_webtransport(bool value) { supports_webtransport_ = value; }
+  WebTransportHttp3VersionSet LocallySupportedWebTransportVersions()
+      const override {
+    return locally_supported_web_transport_versions_;
+  }
+  void set_supports_webtransport(bool value) {
+    locally_supported_web_transport_versions_ =
+        value ? kDefaultSupportedWebTransportVersions
+              : WebTransportHttp3VersionSet();
+  }
+  void set_locally_supported_web_transport_versions(
+      WebTransportHttp3VersionSet versions) {
+    locally_supported_web_transport_versions_ = std::move(versions);
+  }
 
   HttpDatagramSupport LocalHttpDatagramSupport() override {
     return local_http_datagram_support_;
@@ -400,7 +411,7 @@
   StrictMock<TestCryptoStream> crypto_stream_;
 
   bool writev_consumes_all_data_;
-  bool supports_webtransport_ = false;
+  WebTransportHttp3VersionSet locally_supported_web_transport_versions_;
   HttpDatagramSupport local_http_datagram_support_ = HttpDatagramSupport::kNone;
 };
 
@@ -539,10 +550,16 @@
     testing::Mock::VerifyAndClearExpectations(connection_);
   }
 
-  void ReceiveWebTransportSettings() {
+  void ReceiveWebTransportSettings(WebTransportHttp3VersionSet versions =
+                                       kDefaultSupportedWebTransportVersions) {
     SettingsFrame settings;
-    settings.values[SETTINGS_H3_DATAGRAM_DRAFT04] = 1;
-    settings.values[SETTINGS_WEBTRANS_DRAFT00] = 1;
+    settings.values[SETTINGS_H3_DATAGRAM] = 1;
+    if (versions.IsSet(WebTransportHttp3Version::kDraft02)) {
+      settings.values[SETTINGS_WEBTRANS_DRAFT00] = 1;
+    }
+    if (versions.IsSet(WebTransportHttp3Version::kDraft07)) {
+      settings.values[SETTINGS_WEBTRANS_MAX_SESSIONS_DRAFT07] = 16;
+    }
     settings.values[SETTINGS_ENABLE_CONNECT_PROTOCOL] = 1;
     std::string data = std::string(1, kControlStream) +
                        HttpEncoder::SerializeSettingsFrame(settings);
@@ -3453,34 +3470,84 @@
       /*expected_support=*/HttpDatagramSupport::kRfc,
       /*expected_datagram_supported=*/true);
 }
-TEST_P(QuicSpdySessionTestClient, WebTransportSetting) {
+
+TEST_P(QuicSpdySessionTestClient, WebTransportSettingDraft02OnlyBothSides) {
   if (!version().UsesHttp3()) {
     return;
   }
-  session_.set_local_http_datagram_support(HttpDatagramSupport::kDraft04);
-  session_.set_supports_webtransport(true);
+  session_.set_local_http_datagram_support(HttpDatagramSupport::kRfcAndDraft04);
+  session_.set_locally_supported_web_transport_versions(
+      WebTransportHttp3VersionSet({WebTransportHttp3Version::kDraft02}));
 
   EXPECT_FALSE(session_.SupportsWebTransport());
-
-  StrictMock<MockHttp3DebugVisitor> debug_visitor;
-  // Note that this does not actually fill out correct settings because the
-  // settings are filled in at the construction time.
-  EXPECT_CALL(debug_visitor, OnSettingsFrameSent(_));
-  session_.set_debug_visitor(&debug_visitor);
   CompleteHandshake();
-
-  EXPECT_CALL(debug_visitor, OnPeerControlStreamCreated(_));
-  EXPECT_CALL(debug_visitor, OnSettingsFrameReceived(_));
-  ReceiveWebTransportSettings();
+  ReceiveWebTransportSettings(
+      WebTransportHttp3VersionSet({WebTransportHttp3Version::kDraft02}));
   EXPECT_TRUE(session_.ShouldProcessIncomingRequests());
   EXPECT_TRUE(session_.SupportsWebTransport());
+  EXPECT_EQ(session_.SupportedWebTransportVersion(),
+            WebTransportHttp3Version::kDraft02);
+}
+
+TEST_P(QuicSpdySessionTestClient, WebTransportSettingDraft07OnlyBothSides) {
+  if (!version().UsesHttp3()) {
+    return;
+  }
+  session_.set_local_http_datagram_support(HttpDatagramSupport::kRfcAndDraft04);
+  session_.set_locally_supported_web_transport_versions(
+      WebTransportHttp3VersionSet({WebTransportHttp3Version::kDraft07}));
+
+  EXPECT_FALSE(session_.SupportsWebTransport());
+  CompleteHandshake();
+  ReceiveWebTransportSettings(
+      WebTransportHttp3VersionSet({WebTransportHttp3Version::kDraft07}));
+  EXPECT_TRUE(session_.ShouldProcessIncomingRequests());
+  EXPECT_TRUE(session_.SupportsWebTransport());
+  EXPECT_EQ(session_.SupportedWebTransportVersion(),
+            WebTransportHttp3Version::kDraft07);
+}
+
+TEST_P(QuicSpdySessionTestClient, WebTransportSettingBothDraftsBothSides) {
+  if (!version().UsesHttp3()) {
+    return;
+  }
+  session_.set_local_http_datagram_support(HttpDatagramSupport::kRfcAndDraft04);
+  session_.set_locally_supported_web_transport_versions(
+      WebTransportHttp3VersionSet({WebTransportHttp3Version::kDraft02,
+                                   WebTransportHttp3Version::kDraft07}));
+
+  EXPECT_FALSE(session_.SupportsWebTransport());
+  CompleteHandshake();
+  ReceiveWebTransportSettings(
+      WebTransportHttp3VersionSet({WebTransportHttp3Version::kDraft02,
+                                   WebTransportHttp3Version::kDraft07}));
+  EXPECT_TRUE(session_.ShouldProcessIncomingRequests());
+  EXPECT_TRUE(session_.SupportsWebTransport());
+  EXPECT_EQ(session_.SupportedWebTransportVersion(),
+            WebTransportHttp3Version::kDraft07);
+}
+
+TEST_P(QuicSpdySessionTestClient, WebTransportSettingVersionMismatch) {
+  if (!version().UsesHttp3()) {
+    return;
+  }
+  session_.set_local_http_datagram_support(HttpDatagramSupport::kRfcAndDraft04);
+  session_.set_locally_supported_web_transport_versions(
+      WebTransportHttp3VersionSet({WebTransportHttp3Version::kDraft07}));
+
+  EXPECT_FALSE(session_.SupportsWebTransport());
+  CompleteHandshake();
+  ReceiveWebTransportSettings(
+      WebTransportHttp3VersionSet({WebTransportHttp3Version::kDraft02}));
+  EXPECT_FALSE(session_.SupportsWebTransport());
+  EXPECT_EQ(session_.SupportedWebTransportVersion(), absl::nullopt);
 }
 
 TEST_P(QuicSpdySessionTestClient, WebTransportSettingSetToZero) {
   if (!version().UsesHttp3()) {
     return;
   }
-  session_.set_local_http_datagram_support(HttpDatagramSupport::kDraft04);
+  session_.set_local_http_datagram_support(HttpDatagramSupport::kRfcAndDraft04);
   session_.set_supports_webtransport(true);
 
   EXPECT_FALSE(session_.SupportsWebTransport());
@@ -3510,7 +3577,7 @@
   if (!version().UsesHttp3()) {
     return;
   }
-  session_.set_local_http_datagram_support(HttpDatagramSupport::kDraft04);
+  session_.set_local_http_datagram_support(HttpDatagramSupport::kRfcAndDraft04);
   session_.set_supports_webtransport(true);
 
   EXPECT_FALSE(session_.SupportsWebTransport());
@@ -3527,7 +3594,7 @@
   if (!version().UsesHttp3()) {
     return;
   }
-  session_.set_local_http_datagram_support(HttpDatagramSupport::kDraft04);
+  session_.set_local_http_datagram_support(HttpDatagramSupport::kRfcAndDraft04);
   session_.set_supports_webtransport(true);
 
   CompleteHandshake();
@@ -3560,7 +3627,7 @@
   if (!version().UsesHttp3()) {
     return;
   }
-  session_.set_local_http_datagram_support(HttpDatagramSupport::kDraft04);
+  session_.set_local_http_datagram_support(HttpDatagramSupport::kRfcAndDraft04);
   session_.set_supports_webtransport(true);
 
   CompleteHandshake();
@@ -3601,7 +3668,7 @@
   if (!version().UsesHttp3()) {
     return;
   }
-  session_.set_local_http_datagram_support(HttpDatagramSupport::kDraft04);
+  session_.set_local_http_datagram_support(HttpDatagramSupport::kRfcAndDraft04);
   session_.set_supports_webtransport(true);
 
   CompleteHandshake();
@@ -3637,7 +3704,7 @@
     return;
   }
   SetQuicReloadableFlag(quic_act_upon_invalid_header, true);
-  session_.set_local_http_datagram_support(HttpDatagramSupport::kDraft04);
+  session_.set_local_http_datagram_support(HttpDatagramSupport::kRfcAndDraft04);
   session_.set_supports_webtransport(true);
 
   EXPECT_FALSE(session_.SupportsWebTransport());
diff --git a/quiche/quic/core/http/quic_spdy_stream_test.cc b/quiche/quic/core/http/quic_spdy_stream_test.cc
index 5e78cfb..024eefd 100644
--- a/quiche/quic/core/http/quic_spdy_stream_test.cc
+++ b/quiche/quic/core/http/quic_spdy_stream_test.cc
@@ -297,8 +297,14 @@
     return &crypto_stream_;
   }
 
-  bool ShouldNegotiateWebTransport() override { return enable_webtransport_; }
-  void EnableWebTransport() { enable_webtransport_ = true; }
+  WebTransportHttp3VersionSet LocallySupportedWebTransportVersions()
+      const override {
+    return locally_supported_webtransport_versions_;
+  }
+  void EnableWebTransport(WebTransportHttp3VersionSet versions =
+                              kDefaultSupportedWebTransportVersions) {
+    locally_supported_webtransport_versions_ = versions;
+  }
 
   HttpDatagramSupport LocalHttpDatagramSupport() override {
     return local_http_datagram_support_;
@@ -308,7 +314,7 @@
   }
 
  private:
-  bool enable_webtransport_ = false;
+  WebTransportHttp3VersionSet locally_supported_webtransport_versions_;
   HttpDatagramSupport local_http_datagram_support_ = HttpDatagramSupport::kNone;
   StrictMock<TestCryptoStream> crypto_stream_;
 };
diff --git a/quiche/quic/core/quic_utils.h b/quiche/quic/core/quic_utils.h
index 52108aa..4c3fdc9 100644
--- a/quiche/quic/core/quic_utils.h
+++ b/quiche/quic/core/quic_utils.h
@@ -11,9 +11,11 @@
 #include <string>
 #include <type_traits>
 
+#include "absl/numeric/bits.h"
 #include "absl/numeric/int128.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
 #include "absl/types/span.h"
 #include "quiche/quic/core/crypto/quic_random.h"
 #include "quiche/quic/core/frames/quic_frame.h"
@@ -253,12 +255,28 @@
 
   constexpr void ClearAll() { mask_ = 0; }
 
+  // Returns true if any of the bits is set.
+  bool Any() const { return mask_ != 0; }
+
+  // Returns the highest bit set, or nullopt if the mask is all zeroes.
+  absl::optional<Index> Max() const {
+    if (!Any()) {
+      return absl::nullopt;
+    }
+    return static_cast<Index>(NumBits() - absl::countl_zero(mask_) - 1);
+  }
+
   static constexpr size_t NumBits() { return 8 * sizeof(Mask); }
 
   friend bool operator==(const BitMask& lhs, const BitMask& rhs) {
     return lhs.mask_ == rhs.mask_;
   }
 
+  // Bitwise AND that can act as a set intersection between two bit masks.
+  BitMask<Index, Mask> operator&(const BitMask<Index, Mask>& rhs) const {
+    return BitMask<Index, Mask>(mask_ & rhs.mask_);
+  }
+
   std::string DebugString() const {
     return absl::StrCat("0x", absl::Hex(mask_));
   }
@@ -266,6 +284,8 @@
   constexpr Mask mask() const { return mask_; }
 
  private:
+  explicit constexpr BitMask(Mask mask) : mask_(mask) {}
+
   template <typename Bit>
   static constexpr std::enable_if_t<std::is_enum_v<Bit>, Mask> MakeMask(
       Bit bit) {
diff --git a/quiche/quic/core/quic_utils_test.cc b/quiche/quic/core/quic_utils_test.cc
index 29401fb..239812f 100644
--- a/quiche/quic/core/quic_utils_test.cc
+++ b/quiche/quic/core/quic_utils_test.cc
@@ -278,8 +278,10 @@
 
 TEST(QuicBitMaskTest, Integer) {
   BitMask<int> mask({1, 3});
+  EXPECT_EQ(mask.Max(), 3);
   mask.Set(3);
   mask.Set({5, 7, 9});
+  EXPECT_EQ(mask.Max(), 9);
   EXPECT_FALSE(mask.IsSet(0));
   EXPECT_TRUE(mask.IsSet(1));
   EXPECT_FALSE(mask.IsSet(2));
@@ -315,6 +317,24 @@
   EXPECT_TRUE(std::is_trivially_copyable<BitMask<int>>::value);
 }
 
+TEST(QuicBitMaskTest, Any) {
+  BitMask<int> mask;
+  EXPECT_FALSE(mask.Any());
+  mask.Set(3);
+  EXPECT_TRUE(mask.Any());
+  mask.Set(2);
+  EXPECT_TRUE(mask.Any());
+  mask.ClearAll();
+  EXPECT_FALSE(mask.Any());
+}
+
+TEST(QuicBitMaskTest, And) {
+  using Mask = BitMask<int>;
+  EXPECT_EQ(Mask({1, 3, 6}) & Mask({3, 5, 6}), Mask({3, 6}));
+  EXPECT_EQ(Mask({1, 2, 4}) & Mask({3, 5}), Mask({}));
+  EXPECT_EQ(Mask({1, 2, 3, 4, 5}) & Mask({}), Mask({}));
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quiche/quic/test_tools/quic_spdy_session_peer.cc b/quiche/quic/test_tools/quic_spdy_session_peer.cc
index dd99a05..4da7254 100644
--- a/quiche/quic/test_tools/quic_spdy_session_peer.cc
+++ b/quiche/quic/test_tools/quic_spdy_session_peer.cc
@@ -112,8 +112,8 @@
 // static
 void QuicSpdySessionPeer::EnableWebTransport(QuicSpdySession* session) {
   QUICHE_DCHECK(session->WillNegotiateWebTransport());
-  SetHttpDatagramSupport(session, HttpDatagramSupport::kDraft04);
-  session->peer_supports_webtransport_ = true;
+  SetHttpDatagramSupport(session, HttpDatagramSupport::kRfc);
+  session->peer_web_transport_versions_ = kDefaultSupportedWebTransportVersions;
 }
 
 }  // namespace test
diff --git a/quiche/quic/tools/quic_simple_client_session.cc b/quiche/quic/tools/quic_simple_client_session.cc
index ab44f5f..4bb27ae 100644
--- a/quiche/quic/tools/quic_simple_client_session.cc
+++ b/quiche/quic/tools/quic_simple_client_session.cc
@@ -46,12 +46,14 @@
   return stream;
 }
 
-bool QuicSimpleClientSession::ShouldNegotiateWebTransport() {
-  return enable_web_transport_;
+WebTransportHttp3VersionSet
+QuicSimpleClientSession::LocallySupportedWebTransportVersions() const {
+  return enable_web_transport_ ? kDefaultSupportedWebTransportVersions
+                               : WebTransportHttp3VersionSet();
 }
 
 HttpDatagramSupport QuicSimpleClientSession::LocalHttpDatagramSupport() {
-  return enable_web_transport_ ? HttpDatagramSupport::kDraft04
+  return enable_web_transport_ ? HttpDatagramSupport::kRfcAndDraft04
                                : HttpDatagramSupport::kNone;
 }
 
diff --git a/quiche/quic/tools/quic_simple_client_session.h b/quiche/quic/tools/quic_simple_client_session.h
index 9d1b4c7..4850647 100644
--- a/quiche/quic/tools/quic_simple_client_session.h
+++ b/quiche/quic/tools/quic_simple_client_session.h
@@ -38,7 +38,8 @@
                           bool drop_response_body, bool enable_web_transport);
 
   std::unique_ptr<QuicSpdyClientStream> CreateClientStream() override;
-  bool ShouldNegotiateWebTransport() override;
+  WebTransportHttp3VersionSet LocallySupportedWebTransportVersions()
+      const override;
   HttpDatagramSupport LocalHttpDatagramSupport() override;
   void CreateContextForMultiPortPath(
       std::unique_ptr<MultiPortPathContextObserver> context_observer) override;
diff --git a/quiche/quic/tools/quic_simple_server_session.h b/quiche/quic/tools/quic_simple_server_session.h
index bceb4e5..5585214 100644
--- a/quiche/quic/tools/quic_simple_server_session.h
+++ b/quiche/quic/tools/quic_simple_server_session.h
@@ -66,8 +66,11 @@
     return quic_simple_server_backend_;
   }
 
-  bool ShouldNegotiateWebTransport() override {
-    return quic_simple_server_backend_->SupportsWebTransport();
+  WebTransportHttp3VersionSet LocallySupportedWebTransportVersions()
+      const override {
+    return quic_simple_server_backend_->SupportsWebTransport()
+               ? kDefaultSupportedWebTransportVersions
+               : WebTransportHttp3VersionSet();
   }
   HttpDatagramSupport LocalHttpDatagramSupport() override {
     if (ShouldNegotiateWebTransport()) {