Add CONNECT-IP capsules This CL introduces the capsules from draft-ietf-masque-connect-ip-03, along with tests. None of the new code is used yet. PiperOrigin-RevId: 478570743
diff --git a/quiche/common/quiche_ip_address.cc b/quiche/common/quiche_ip_address.cc index fd90deb..342597b 100644 --- a/quiche/common/quiche_ip_address.cc +++ b/quiche/common/quiche_ip_address.cc
@@ -9,6 +9,7 @@ #include <cstring> #include <string> +#include "absl/strings/str_cat.h" #include "quiche/common/platform/api/quiche_bug_tracker.h" #include "quiche/common/platform/api/quiche_logging.h" #include "quiche/common/quiche_ip_address_family.h" @@ -223,4 +224,35 @@ return address_.v6; } +QuicheIpPrefix::QuicheIpPrefix() : prefix_length_(0) {} +QuicheIpPrefix::QuicheIpPrefix(const QuicheIpAddress& address) + : address_(address) { + if (address_.IsIPv6()) { + prefix_length_ = QuicheIpAddress::kIPv6AddressSize * 8; + } else if (address_.IsIPv4()) { + prefix_length_ = QuicheIpAddress::kIPv4AddressSize * 8; + } else { + prefix_length_ = 0; + } +} +QuicheIpPrefix::QuicheIpPrefix(const QuicheIpAddress& address, + uint8_t prefix_length) + : address_(address), prefix_length_(prefix_length) { + QUICHE_DCHECK(prefix_length <= QuicheIpPrefix(address).prefix_length()) + << "prefix_length cannot be longer than the size of the IP address"; +} + +std::string QuicheIpPrefix::ToString() const { + return absl::StrCat(address_.ToString(), "/", prefix_length_); +} + +bool operator==(const QuicheIpPrefix& lhs, const QuicheIpPrefix& rhs) { + return lhs.address_ == rhs.address_ && + lhs.prefix_length_ == rhs.prefix_length_; +} + +bool operator!=(const QuicheIpPrefix& lhs, const QuicheIpPrefix& rhs) { + return !(lhs == rhs); +} + } // namespace quiche
diff --git a/quiche/common/quiche_ip_address.h b/quiche/common/quiche_ip_address.h index ef7714c..9267922 100644 --- a/quiche/common/quiche_ip_address.h +++ b/quiche/common/quiche_ip_address.h
@@ -5,6 +5,7 @@ #ifndef QUICHE_COMMON_QUICHE_IP_ADDRESS_H_ #define QUICHE_COMMON_QUICHE_IP_ADDRESS_H_ +#include <cstdint> #if defined(_WIN32) #include <winsock2.h> #include <ws2tcpip.h> @@ -93,6 +94,37 @@ return os; } +// Represents an IP prefix, which is an IP address and a prefix length in bits. +class QUICHE_EXPORT_PRIVATE QuicheIpPrefix { + public: + QuicheIpPrefix(); + explicit QuicheIpPrefix(const QuicheIpAddress& address); + explicit QuicheIpPrefix(const QuicheIpAddress& address, + uint8_t prefix_length); + + QuicheIpAddress address() const { return address_; } + uint8_t prefix_length() const { return prefix_length_; } + // Human-readable string representation of the prefix suitable for logging. + std::string ToString() const; + + QuicheIpPrefix(const QuicheIpPrefix& other) = default; + QuicheIpPrefix& operator=(const QuicheIpPrefix& other) = default; + QuicheIpPrefix& operator=(QuicheIpPrefix&& other) = default; + QUICHE_EXPORT_PRIVATE friend bool operator==(const QuicheIpPrefix& lhs, + const QuicheIpPrefix& rhs); + QUICHE_EXPORT_PRIVATE friend bool operator!=(const QuicheIpPrefix& lhs, + const QuicheIpPrefix& rhs); + + private: + QuicheIpAddress address_; + uint8_t prefix_length_; +}; + +inline std::ostream& operator<<(std::ostream& os, const QuicheIpPrefix prefix) { + os << prefix.ToString(); + return os; +} + } // namespace quiche #endif // QUICHE_COMMON_QUICHE_IP_ADDRESS_H_
diff --git a/quiche/quic/core/http/capsule.cc b/quiche/quic/core/http/capsule.cc index 3fba85e..8f653f1 100644 --- a/quiche/quic/core/http/capsule.cc +++ b/quiche/quic/core/http/capsule.cc
@@ -14,7 +14,9 @@ #include "quiche/quic/core/quic_data_writer.h" #include "quiche/quic/core/quic_types.h" #include "quiche/quic/platform/api/quic_bug_tracker.h" +#include "quiche/quic/platform/api/quic_ip_address.h" #include "quiche/common/platform/api/quiche_logging.h" +#include "quiche/common/quiche_ip_address.h" namespace quic { @@ -26,6 +28,12 @@ return "DATAGRAM_WITHOUT_CONTEXT"; case CapsuleType::CLOSE_WEBTRANSPORT_SESSION: return "CLOSE_WEBTRANSPORT_SESSION"; + case CapsuleType::ADDRESS_REQUEST: + return "ADDRESS_REQUEST"; + case CapsuleType::ADDRESS_ASSIGN: + return "ADDRESS_ASSIGN"; + case CapsuleType::ROUTE_ADVERTISEMENT: + return "ROUTE_ADVERTISEMENT"; } return absl::StrCat("Unknown(", static_cast<uint64_t>(capsule_type), ")"); } @@ -41,7 +49,7 @@ static_assert( std::is_standard_layout<LegacyDatagramCapsule>::value && std::is_trivially_destructible<LegacyDatagramCapsule>::value, - "All capsule structs must have these properties"); + "All inline capsule structs must have these properties"); legacy_datagram_capsule_ = LegacyDatagramCapsule(); break; case CapsuleType::DATAGRAM_WITHOUT_CONTEXT: @@ -49,7 +57,7 @@ std::is_standard_layout<DatagramWithoutContextCapsule>::value && std::is_trivially_destructible< DatagramWithoutContextCapsule>::value, - "All capsule structs must have these properties"); + "All inline capsule structs must have these properties"); datagram_without_context_capsule_ = DatagramWithoutContextCapsule(); break; case CapsuleType::CLOSE_WEBTRANSPORT_SESSION: @@ -57,18 +65,50 @@ std::is_standard_layout<CloseWebTransportSessionCapsule>::value && std::is_trivially_destructible< CloseWebTransportSessionCapsule>::value, - "All capsule structs must have these properties"); + "All inline capsule structs must have these properties"); close_web_transport_session_capsule_ = CloseWebTransportSessionCapsule(); break; + case CapsuleType::ADDRESS_REQUEST: + address_request_capsule_ = new AddressRequestCapsule(); + break; + case CapsuleType::ADDRESS_ASSIGN: + address_assign_capsule_ = new AddressAssignCapsule(); + break; + case CapsuleType::ROUTE_ADVERTISEMENT: + route_advertisement_capsule_ = new RouteAdvertisementCapsule(); + break; default: unknown_capsule_data_ = absl::string_view(); break; } } +void Capsule::Free() { + switch (capsule_type_) { + // Inlined capsule types. + case CapsuleType::LEGACY_DATAGRAM: + case CapsuleType::DATAGRAM_WITHOUT_CONTEXT: + case CapsuleType::CLOSE_WEBTRANSPORT_SESSION: + // Do nothing, these are guaranteed to be trivially destructible. + break; + // Out-of-line capsule types. + case CapsuleType::ADDRESS_REQUEST: + delete address_request_capsule_; + break; + case CapsuleType::ADDRESS_ASSIGN: + delete address_assign_capsule_; + break; + case CapsuleType::ROUTE_ADVERTISEMENT: + delete route_advertisement_capsule_; + break; + } + capsule_type_ = static_cast<CapsuleType>(0x17); // Reserved unknown value. + unknown_capsule_data_ = absl::string_view(); +} +Capsule::~Capsule() { Free(); } + // static -Capsule Capsule::LegacyDatagram( - absl::string_view http_datagram_payload) { +Capsule Capsule::LegacyDatagram(absl::string_view http_datagram_payload) { Capsule capsule(CapsuleType::LEGACY_DATAGRAM); capsule.legacy_datagram_capsule().http_datagram_payload = http_datagram_payload; @@ -94,6 +134,21 @@ } // static +Capsule Capsule::AddressRequest() { + return Capsule(CapsuleType::ADDRESS_REQUEST); +} + +// static +Capsule Capsule::AddressAssign() { + return Capsule(CapsuleType::ADDRESS_ASSIGN); +} + +// static +Capsule Capsule::RouteAdvertisement() { + return Capsule(CapsuleType::ROUTE_ADVERTISEMENT); +} + +// static Capsule Capsule::Unknown(uint64_t capsule_type, absl::string_view unknown_capsule_data) { Capsule capsule(static_cast<CapsuleType>(capsule_type)); @@ -102,6 +157,7 @@ } Capsule& Capsule::operator=(const Capsule& other) { + Free(); capsule_type_ = other.capsule_type_; switch (capsule_type_) { case CapsuleType::LEGACY_DATAGRAM: @@ -115,6 +171,18 @@ close_web_transport_session_capsule_ = other.close_web_transport_session_capsule_; break; + case CapsuleType::ADDRESS_ASSIGN: + address_assign_capsule_ = new AddressAssignCapsule(); + *address_assign_capsule_ = *other.address_assign_capsule_; + break; + case CapsuleType::ADDRESS_REQUEST: + address_request_capsule_ = new AddressRequestCapsule(); + *address_request_capsule_ = *other.address_request_capsule_; + break; + case CapsuleType::ROUTE_ADVERTISEMENT: + route_advertisement_capsule_ = new RouteAdvertisementCapsule(); + *route_advertisement_capsule_ = *other.route_advertisement_capsule_; + break; default: unknown_capsule_data_ = other.unknown_capsule_data_; break; @@ -142,6 +210,15 @@ other.close_web_transport_session_capsule_.error_code && close_web_transport_session_capsule_.error_message == other.close_web_transport_session_capsule_.error_message; + case CapsuleType::ADDRESS_REQUEST: + return address_request_capsule_->requested_addresses == + other.address_request_capsule_->requested_addresses; + case CapsuleType::ADDRESS_ASSIGN: + return address_assign_capsule_->assigned_addresses == + other.address_assign_capsule_->assigned_addresses; + case CapsuleType::ROUTE_ADVERTISEMENT: + return route_advertisement_capsule_->ip_address_ranges == + other.route_advertisement_capsule_->ip_address_ranges; default: return unknown_capsule_data_ == other.unknown_capsule_data_; } @@ -169,6 +246,34 @@ ",error_message=\"", close_web_transport_session_capsule_.error_message, "\")"); break; + case CapsuleType::ADDRESS_REQUEST: { + absl::StrAppend(&rv, "["); + for (auto requested_address : + address_request_capsule_->requested_addresses) { + absl::StrAppend(&rv, "(", requested_address.request_id, "-", + requested_address.ip_prefix.ToString(), ")"); + } + absl::StrAppend(&rv, "]"); + } break; + case CapsuleType::ADDRESS_ASSIGN: { + absl::StrAppend(&rv, "["); + for (auto assigned_address : + address_assign_capsule_->assigned_addresses) { + absl::StrAppend(&rv, "(", assigned_address.request_id, "-", + assigned_address.ip_prefix.ToString(), ")"); + } + absl::StrAppend(&rv, "]"); + } break; + case CapsuleType::ROUTE_ADVERTISEMENT: { + absl::StrAppend(&rv, "["); + for (auto ip_address_range : + route_advertisement_capsule_->ip_address_ranges) { + absl::StrAppend(&rv, "(", ip_address_range.start_ip_address.ToString(), + "-", ip_address_range.end_ip_address.ToString(), "-", + static_cast<int>(ip_address_range.ip_protocol), ")"); + } + absl::StrAppend(&rv, "]"); + } break; default: absl::StrAppend(&rv, "[", absl::BytesToHexString(unknown_capsule_data_), "]"); @@ -205,6 +310,42 @@ sizeof(WebTransportSessionError) + capsule.close_web_transport_session_capsule().error_message.size(); break; + case CapsuleType::ADDRESS_REQUEST: + capsule_data_length = 0; + for (auto requested_address : + capsule.address_request_capsule().requested_addresses) { + capsule_data_length += + QuicDataWriter::GetVarInt62Len(requested_address.request_id) + 1 + + (requested_address.ip_prefix.address().IsIPv4() + ? QuicIpAddress::kIPv4AddressSize + : QuicIpAddress::kIPv6AddressSize) + + 1; + } + break; + case CapsuleType::ADDRESS_ASSIGN: + capsule_data_length = 0; + for (auto assigned_address : + capsule.address_assign_capsule().assigned_addresses) { + capsule_data_length += + QuicDataWriter::GetVarInt62Len(assigned_address.request_id) + 1 + + (assigned_address.ip_prefix.address().IsIPv4() + ? QuicIpAddress::kIPv4AddressSize + : QuicIpAddress::kIPv6AddressSize) + + 1; + } + break; + case CapsuleType::ROUTE_ADVERTISEMENT: + capsule_data_length = 0; + for (auto ip_address_range : + capsule.route_advertisement_capsule().ip_address_ranges) { + capsule_data_length += 1 + + (ip_address_range.start_ip_address.IsIPv4() + ? QuicIpAddress::kIPv4AddressSize + : QuicIpAddress::kIPv6AddressSize) * + 2 + + 1; + } + break; default: capsule_data_length = capsule.unknown_capsule_data().length(); break; @@ -254,6 +395,88 @@ return {}; } break; + case CapsuleType::ADDRESS_REQUEST: + for (auto requested_address : + capsule.address_request_capsule().requested_addresses) { + if (!writer.WriteVarInt62(requested_address.request_id)) { + QUIC_BUG(address request capsule id write fail) + << "Failed to write ADDRESS_REQUEST ID"; + return {}; + } + if (!writer.WriteUInt8( + requested_address.ip_prefix.address().IsIPv4() ? 4 : 6)) { + QUIC_BUG(address request capsule family write fail) + << "Failed to write ADDRESS_REQUEST family"; + return {}; + } + if (!writer.WriteStringPiece( + requested_address.ip_prefix.address().ToPackedString())) { + QUIC_BUG(address request capsule address write fail) + << "Failed to write ADDRESS_REQUEST address"; + return {}; + } + if (!writer.WriteUInt8(requested_address.ip_prefix.prefix_length())) { + QUIC_BUG(address request capsule prefix length write fail) + << "Failed to write ADDRESS_REQUEST prefix length"; + return {}; + } + } + break; + case CapsuleType::ADDRESS_ASSIGN: + for (auto assigned_address : + capsule.address_assign_capsule().assigned_addresses) { + if (!writer.WriteVarInt62(assigned_address.request_id)) { + QUIC_BUG(address request capsule id write fail) + << "Failed to write ADDRESS_ASSIGN ID"; + return {}; + } + if (!writer.WriteUInt8( + assigned_address.ip_prefix.address().IsIPv4() ? 4 : 6)) { + QUIC_BUG(address request capsule family write fail) + << "Failed to write ADDRESS_ASSIGN family"; + return {}; + } + if (!writer.WriteStringPiece( + assigned_address.ip_prefix.address().ToPackedString())) { + QUIC_BUG(address request capsule address write fail) + << "Failed to write ADDRESS_ASSIGN address"; + return {}; + } + if (!writer.WriteUInt8(assigned_address.ip_prefix.prefix_length())) { + QUIC_BUG(address request capsule prefix length write fail) + << "Failed to write ADDRESS_ASSIGN prefix length"; + return {}; + } + } + break; + case CapsuleType::ROUTE_ADVERTISEMENT: + for (auto ip_address_range : + capsule.route_advertisement_capsule().ip_address_ranges) { + if (!writer.WriteUInt8( + ip_address_range.start_ip_address.IsIPv4() ? 4 : 6)) { + QUIC_BUG(route advertisement capsule family write fail) + << "Failed to write ROUTE_ADVERTISEMENT family"; + return {}; + } + if (!writer.WriteStringPiece( + ip_address_range.start_ip_address.ToPackedString())) { + QUIC_BUG(route advertisement capsule start address write fail) + << "Failed to write ROUTE_ADVERTISEMENT start address"; + return {}; + } + if (!writer.WriteStringPiece( + ip_address_range.end_ip_address.ToPackedString())) { + QUIC_BUG(route advertisement capsule end address write fail) + << "Failed to write ROUTE_ADVERTISEMENT end address"; + return {}; + } + if (!writer.WriteUInt8(ip_address_range.ip_protocol)) { + QUIC_BUG(route advertisement capsule IP protocol write fail) + << "Failed to write ROUTE_ADVERTISEMENT IP protocol"; + return {}; + } + } + break; default: if (!writer.WriteStringPiece(capsule.unknown_capsule_data())) { QUIC_BUG(capsule data write fail) << "Failed to write CAPSULE data"; @@ -334,6 +557,154 @@ capsule.close_web_transport_session_capsule().error_message = capsule_data_reader.ReadRemainingPayload(); break; + case CapsuleType::ADDRESS_REQUEST: { + while (!capsule_data_reader.IsDoneReading()) { + PrefixWithId requested_address; + if (!capsule_data_reader.ReadVarInt62(&requested_address.request_id)) { + ReportParseFailure( + "Unable to parse capsule ADDRESS_REQUEST request ID"); + return 0; + } + uint8_t address_family; + if (!capsule_data_reader.ReadUInt8(&address_family)) { + ReportParseFailure("Unable to parse capsule ADDRESS_REQUEST family"); + return 0; + } + if (address_family != 4 && address_family != 6) { + ReportParseFailure("Bad ADDRESS_REQUEST family"); + return 0; + } + absl::string_view ip_address_bytes; + if (!capsule_data_reader.ReadStringPiece( + &ip_address_bytes, address_family == 4 + ? QuicIpAddress::kIPv4AddressSize + : QuicIpAddress::kIPv6AddressSize)) { + ReportParseFailure("Unable to read capsule ADDRESS_REQUEST address"); + return 0; + } + quiche::QuicheIpAddress ip_address; + if (!ip_address.FromPackedString(ip_address_bytes.data(), + ip_address_bytes.size())) { + ReportParseFailure("Unable to parse capsule ADDRESS_REQUEST address"); + return 0; + } + uint8_t ip_prefix_length; + if (!capsule_data_reader.ReadUInt8(&ip_prefix_length)) { + ReportParseFailure( + "Unable to parse capsule ADDRESS_REQUEST IP prefix length"); + return 0; + } + if (ip_prefix_length > + quiche::QuicheIpPrefix(ip_address).prefix_length()) { + ReportParseFailure("Invalid IP prefix length"); + return 0; + } + requested_address.ip_prefix = + quiche::QuicheIpPrefix(ip_address, ip_prefix_length); + capsule.address_request_capsule().requested_addresses.push_back( + requested_address); + } + } break; + case CapsuleType::ADDRESS_ASSIGN: { + while (!capsule_data_reader.IsDoneReading()) { + PrefixWithId assigned_address; + if (!capsule_data_reader.ReadVarInt62(&assigned_address.request_id)) { + ReportParseFailure( + "Unable to parse capsule ADDRESS_ASSIGN request ID"); + return 0; + } + uint8_t address_family; + if (!capsule_data_reader.ReadUInt8(&address_family)) { + ReportParseFailure("Unable to parse capsule ADDRESS_ASSIGN family"); + return 0; + } + if (address_family != 4 && address_family != 6) { + ReportParseFailure("Bad ADDRESS_ASSIGN family"); + return 0; + } + absl::string_view ip_address_bytes; + if (!capsule_data_reader.ReadStringPiece( + &ip_address_bytes, address_family == 4 + ? QuicIpAddress::kIPv4AddressSize + : QuicIpAddress::kIPv6AddressSize)) { + ReportParseFailure("Unable to read capsule ADDRESS_ASSIGN address"); + return 0; + } + quiche::QuicheIpAddress ip_address; + if (!ip_address.FromPackedString(ip_address_bytes.data(), + ip_address_bytes.size())) { + ReportParseFailure("Unable to parse capsule ADDRESS_ASSIGN address"); + return 0; + } + uint8_t ip_prefix_length; + if (!capsule_data_reader.ReadUInt8(&ip_prefix_length)) { + ReportParseFailure( + "Unable to parse capsule ADDRESS_ASSIGN IP prefix length"); + return 0; + } + if (ip_prefix_length > + quiche::QuicheIpPrefix(ip_address).prefix_length()) { + ReportParseFailure("Invalid IP prefix length"); + return 0; + } + assigned_address.ip_prefix = + quiche::QuicheIpPrefix(ip_address, ip_prefix_length); + capsule.address_assign_capsule().assigned_addresses.push_back( + assigned_address); + } + } break; + case CapsuleType::ROUTE_ADVERTISEMENT: { + while (!capsule_data_reader.IsDoneReading()) { + uint8_t address_family; + if (!capsule_data_reader.ReadUInt8(&address_family)) { + ReportParseFailure( + "Unable to parse capsule ROUTE_ADVERTISEMENT family"); + return 0; + } + if (address_family != 4 && address_family != 6) { + ReportParseFailure("Bad ROUTE_ADVERTISEMENT family"); + return 0; + } + IpAddressRange ip_address_range; + absl::string_view start_ip_address_bytes; + if (!capsule_data_reader.ReadStringPiece( + &start_ip_address_bytes, + address_family == 4 ? QuicIpAddress::kIPv4AddressSize + : QuicIpAddress::kIPv6AddressSize)) { + ReportParseFailure( + "Unable to read capsule ROUTE_ADVERTISEMENT start address"); + return 0; + } + if (!ip_address_range.start_ip_address.FromPackedString( + start_ip_address_bytes.data(), start_ip_address_bytes.size())) { + ReportParseFailure( + "Unable to parse capsule ROUTE_ADVERTISEMENT start address"); + return 0; + } + absl::string_view end_ip_address_bytes; + if (!capsule_data_reader.ReadStringPiece( + &end_ip_address_bytes, address_family == 4 + ? QuicIpAddress::kIPv4AddressSize + : QuicIpAddress::kIPv6AddressSize)) { + ReportParseFailure( + "Unable to read capsule ROUTE_ADVERTISEMENT end address"); + return 0; + } + if (!ip_address_range.end_ip_address.FromPackedString( + end_ip_address_bytes.data(), end_ip_address_bytes.size())) { + ReportParseFailure( + "Unable to parse capsule ROUTE_ADVERTISEMENT end address"); + return 0; + } + if (!capsule_data_reader.ReadUInt8(&ip_address_range.ip_protocol)) { + ReportParseFailure( + "Unable to parse capsule ROUTE_ADVERTISEMENT IP protocol"); + return 0; + } + capsule.route_advertisement_capsule().ip_address_ranges.push_back( + ip_address_range); + } + } break; default: capsule.unknown_capsule_data() = capsule_data_reader.ReadRemainingPayload(); @@ -363,4 +734,28 @@ } } +bool PrefixWithId::operator==(const PrefixWithId& other) const { + return request_id == other.request_id && ip_prefix == other.ip_prefix; +} + +bool IpAddressRange::operator==(const IpAddressRange& other) const { + return start_ip_address == other.start_ip_address && + end_ip_address == other.end_ip_address && + ip_protocol == other.ip_protocol; +} + +bool AddressAssignCapsule::operator==(const AddressAssignCapsule& other) const { + return assigned_addresses == other.assigned_addresses; +} + +bool AddressRequestCapsule::operator==( + const AddressRequestCapsule& other) const { + return requested_addresses == other.requested_addresses; +} + +bool RouteAdvertisementCapsule::operator==( + const RouteAdvertisementCapsule& other) const { + return ip_address_ranges == other.ip_address_ranges; +} + } // namespace quic
diff --git a/quiche/quic/core/http/capsule.h b/quiche/quic/core/http/capsule.h index 7b4cc46..3d83a55 100644 --- a/quiche/quic/core/http/capsule.h +++ b/quiche/quic/core/http/capsule.h
@@ -7,6 +7,7 @@ #include <cstdint> #include <string> +#include <vector> #include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" @@ -15,15 +16,20 @@ #include "quiche/quic/core/quic_types.h" #include "quiche/common/platform/api/quiche_logging.h" #include "quiche/common/quiche_buffer_allocator.h" +#include "quiche/common/quiche_ip_address.h" namespace quic { enum class CapsuleType : uint64_t { - // Casing in this enum matches the IETF specification. + // Casing in this enum matches the IETF specifications. LEGACY_DATAGRAM = 0xff37a0, // draft-ietf-masque-h3-datagram-04. DATAGRAM_WITHOUT_CONTEXT = 0xff37a5, // draft-ietf-masque-h3-datagram-05 to -08. CLOSE_WEBTRANSPORT_SESSION = 0x2843, + // draft-ietf-masque-connect-ip-03. + ADDRESS_ASSIGN = 0x1ECA6A00, + ADDRESS_REQUEST = 0x1ECA6A01, + ROUTE_ADVERTISEMENT = 0x1ECA6A02, }; QUIC_EXPORT_PRIVATE std::string CapsuleTypeToString(CapsuleType capsule_type); @@ -40,6 +46,29 @@ WebTransportSessionError error_code; absl::string_view error_message; }; +struct QUIC_EXPORT_PRIVATE PrefixWithId { + uint64_t request_id; + quiche::QuicheIpPrefix ip_prefix; + bool operator==(const PrefixWithId& other) const; +}; +struct QUIC_EXPORT_PRIVATE IpAddressRange { + quiche::QuicheIpAddress start_ip_address; + quiche::QuicheIpAddress end_ip_address; + uint8_t ip_protocol; + bool operator==(const IpAddressRange& other) const; +}; +struct QUIC_EXPORT_PRIVATE AddressAssignCapsule { + std::vector<PrefixWithId> assigned_addresses; + bool operator==(const AddressAssignCapsule& other) const; +}; +struct QUIC_EXPORT_PRIVATE AddressRequestCapsule { + std::vector<PrefixWithId> requested_addresses; + bool operator==(const AddressRequestCapsule& other) const; +}; +struct QUIC_EXPORT_PRIVATE RouteAdvertisementCapsule { + std::vector<IpAddressRange> ip_address_ranges; + bool operator==(const RouteAdvertisementCapsule& other) const; +}; // Capsule from draft-ietf-masque-h3-datagram. // IMPORTANT NOTE: Capsule does not own any of the absl::string_view memory it @@ -55,11 +84,15 @@ static Capsule CloseWebTransportSession( WebTransportSessionError error_code = 0, absl::string_view error_message = ""); + static Capsule AddressRequest(); + static Capsule AddressAssign(); + static Capsule RouteAdvertisement(); static Capsule Unknown( uint64_t capsule_type, absl::string_view unknown_capsule_data = absl::string_view()); explicit Capsule(CapsuleType capsule_type); + ~Capsule(); Capsule(const Capsule& other); Capsule& operator=(const Capsule& other); bool operator==(const Capsule& other) const; @@ -96,27 +129,61 @@ QUICHE_DCHECK_EQ(capsule_type_, CapsuleType::CLOSE_WEBTRANSPORT_SESSION); return close_web_transport_session_capsule_; } + AddressRequestCapsule& address_request_capsule() { + QUICHE_DCHECK_EQ(capsule_type_, CapsuleType::ADDRESS_REQUEST); + return *address_request_capsule_; + } + const AddressRequestCapsule& address_request_capsule() const { + QUICHE_DCHECK_EQ(capsule_type_, CapsuleType::ADDRESS_REQUEST); + return *address_request_capsule_; + } + AddressAssignCapsule& address_assign_capsule() { + QUICHE_DCHECK_EQ(capsule_type_, CapsuleType::ADDRESS_ASSIGN); + return *address_assign_capsule_; + } + const AddressAssignCapsule& address_assign_capsule() const { + QUICHE_DCHECK_EQ(capsule_type_, CapsuleType::ADDRESS_ASSIGN); + return *address_assign_capsule_; + } + RouteAdvertisementCapsule& route_advertisement_capsule() { + QUICHE_DCHECK_EQ(capsule_type_, CapsuleType::ROUTE_ADVERTISEMENT); + return *route_advertisement_capsule_; + } + const RouteAdvertisementCapsule& route_advertisement_capsule() const { + QUICHE_DCHECK_EQ(capsule_type_, CapsuleType::ROUTE_ADVERTISEMENT); + return *route_advertisement_capsule_; + } absl::string_view& unknown_capsule_data() { QUICHE_DCHECK(capsule_type_ != CapsuleType::LEGACY_DATAGRAM && capsule_type_ != CapsuleType::DATAGRAM_WITHOUT_CONTEXT && - capsule_type_ != CapsuleType::CLOSE_WEBTRANSPORT_SESSION) + capsule_type_ != CapsuleType::CLOSE_WEBTRANSPORT_SESSION && + capsule_type_ != CapsuleType::ADDRESS_REQUEST && + capsule_type_ != CapsuleType::ADDRESS_ASSIGN && + capsule_type_ != CapsuleType::ROUTE_ADVERTISEMENT) << capsule_type_; return unknown_capsule_data_; } const absl::string_view& unknown_capsule_data() const { QUICHE_DCHECK(capsule_type_ != CapsuleType::LEGACY_DATAGRAM && capsule_type_ != CapsuleType::DATAGRAM_WITHOUT_CONTEXT && - capsule_type_ != CapsuleType::CLOSE_WEBTRANSPORT_SESSION) + capsule_type_ != CapsuleType::CLOSE_WEBTRANSPORT_SESSION && + capsule_type_ != CapsuleType::ADDRESS_REQUEST && + capsule_type_ != CapsuleType::ADDRESS_ASSIGN && + capsule_type_ != CapsuleType::ROUTE_ADVERTISEMENT) << capsule_type_; return unknown_capsule_data_; } private: + void Free(); CapsuleType capsule_type_; union { LegacyDatagramCapsule legacy_datagram_capsule_; DatagramWithoutContextCapsule datagram_without_context_capsule_; CloseWebTransportSessionCapsule close_web_transport_session_capsule_; + AddressRequestCapsule* address_request_capsule_; + AddressAssignCapsule* address_assign_capsule_; + RouteAdvertisementCapsule* route_advertisement_capsule_; absl::string_view unknown_capsule_data_; }; };
diff --git a/quiche/quic/core/http/capsule_test.cc b/quiche/quic/core/http/capsule_test.cc index 1791bb6..a686095 100644 --- a/quiche/quic/core/http/capsule_test.cc +++ b/quiche/quic/core/http/capsule_test.cc
@@ -13,6 +13,7 @@ #include "absl/strings/string_view.h" #include "quiche/quic/platform/api/quic_test.h" #include "quiche/quic/test_tools/quic_test_utils.h" +#include "quiche/common/quiche_ip_address.h" #include "quiche/common/test_tools/quiche_test_utils.h" using ::testing::_; @@ -116,6 +117,123 @@ TestSerialization(expected_capsule, capsule_fragment); } +TEST_F(CapsuleTest, AddressAssignCapsule) { + std::string capsule_fragment = absl::HexStringToBytes( + "9ECA6A00" // ADDRESS_ASSIGN capsule type + "1A" // capsule length = 26 + // first assigned address + "00" // request ID = 0 + "04" // IP version = 4 + "C000022A" // 192.0.2.42 + "1F" // prefix length = 31 + // second assigned address + "01" // request ID = 1 + "06" // IP version = 6 + "20010db8123456780000000000000000" // 2001:db8:1234:5678:: + "40" // prefix length = 64 + ); + Capsule expected_capsule = Capsule::AddressAssign(); + quiche::QuicheIpAddress ip_address1; + ip_address1.FromString("192.0.2.42"); + PrefixWithId assigned_address1; + assigned_address1.request_id = 0; + assigned_address1.ip_prefix = + quiche::QuicheIpPrefix(ip_address1, /*prefix_length=*/31); + expected_capsule.address_assign_capsule().assigned_addresses.push_back( + assigned_address1); + quiche::QuicheIpAddress ip_address2; + ip_address2.FromString("2001:db8:1234:5678::"); + PrefixWithId assigned_address2; + assigned_address2.request_id = 1; + assigned_address2.ip_prefix = + quiche::QuicheIpPrefix(ip_address2, /*prefix_length=*/64); + expected_capsule.address_assign_capsule().assigned_addresses.push_back( + assigned_address2); + { + EXPECT_CALL(visitor_, OnCapsule(expected_capsule)); + ASSERT_TRUE(capsule_parser_.IngestCapsuleFragment(capsule_fragment)); + } + ValidateParserIsEmpty(); + TestSerialization(expected_capsule, capsule_fragment); +} + +TEST_F(CapsuleTest, AddressRequestCapsule) { + std::string capsule_fragment = absl::HexStringToBytes( + "9ECA6A01" // ADDRESS_REQUEST capsule type + "1A" // capsule length = 26 + // first requested address + "00" // request ID = 0 + "04" // IP version = 4 + "C000022A" // 192.0.2.42 + "1F" // prefix length = 31 + // second requested address + "01" // request ID = 1 + "06" // IP version = 6 + "20010db8123456780000000000000000" // 2001:db8:1234:5678:: + "40" // prefix length = 64 + ); + Capsule expected_capsule = Capsule::AddressRequest(); + quiche::QuicheIpAddress ip_address1; + ip_address1.FromString("192.0.2.42"); + PrefixWithId requested_address1; + requested_address1.request_id = 0; + requested_address1.ip_prefix = + quiche::QuicheIpPrefix(ip_address1, /*prefix_length=*/31); + expected_capsule.address_request_capsule().requested_addresses.push_back( + requested_address1); + quiche::QuicheIpAddress ip_address2; + ip_address2.FromString("2001:db8:1234:5678::"); + PrefixWithId requested_address2; + requested_address2.request_id = 1; + requested_address2.ip_prefix = + quiche::QuicheIpPrefix(ip_address2, /*prefix_length=*/64); + expected_capsule.address_request_capsule().requested_addresses.push_back( + requested_address2); + { + EXPECT_CALL(visitor_, OnCapsule(expected_capsule)); + ASSERT_TRUE(capsule_parser_.IngestCapsuleFragment(capsule_fragment)); + } + ValidateParserIsEmpty(); + TestSerialization(expected_capsule, capsule_fragment); +} + +TEST_F(CapsuleTest, RouteAdvertisementCapsule) { + std::string capsule_fragment = absl::HexStringToBytes( + "9ECA6A02" // ROUTE_ADVERTISEMENT capsule type + "2C" // capsule length = 44 + // first IP address range + "04" // IP version = 4 + "C0000218" // 192.0.2.24 + "C000022A" // 192.0.2.42 + "00" // ip protocol = 0 + // second IP address range + "06" // IP version = 6 + "00000000000000000000000000000000" // :: + "ffffffffffffffffffffffffffffffff" // all ones IPv6 address + "01" // ip protocol = 1 (ICMP) + ); + Capsule expected_capsule = Capsule::RouteAdvertisement(); + IpAddressRange ip_address_range1; + ip_address_range1.start_ip_address.FromString("192.0.2.24"); + ip_address_range1.end_ip_address.FromString("192.0.2.42"); + ip_address_range1.ip_protocol = 0; + expected_capsule.route_advertisement_capsule().ip_address_ranges.push_back( + ip_address_range1); + IpAddressRange ip_address_range2; + ip_address_range2.start_ip_address.FromString("::"); + ip_address_range2.end_ip_address.FromString( + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); + ip_address_range2.ip_protocol = 1; + expected_capsule.route_advertisement_capsule().ip_address_ranges.push_back( + ip_address_range2); + { + EXPECT_CALL(visitor_, OnCapsule(expected_capsule)); + ASSERT_TRUE(capsule_parser_.IngestCapsuleFragment(capsule_fragment)); + } + ValidateParserIsEmpty(); + TestSerialization(expected_capsule, capsule_fragment); +} + TEST_F(CapsuleTest, UnknownCapsule) { std::string capsule_fragment = absl::HexStringToBytes( "33" // unknown capsule type of 0x33
diff --git a/quiche/quic/core/http/quic_spdy_stream.cc b/quiche/quic/core/http/quic_spdy_stream.cc index c2c269a..7b580be 100644 --- a/quiche/quic/core/http/quic_spdy_stream.cc +++ b/quiche/quic/core/http/quic_spdy_stream.cc
@@ -292,6 +292,10 @@ } } + if (connect_ip_visitor_ != nullptr) { + connect_ip_visitor_->OnHeadersWritten(); + } + return bytes_written; } @@ -1361,6 +1365,24 @@ capsule.close_web_transport_session_capsule().error_code, capsule.close_web_transport_session_capsule().error_message); } break; + case CapsuleType::ADDRESS_ASSIGN: + if (connect_ip_visitor_ == nullptr) { + return true; + } + return connect_ip_visitor_->OnAddressAssignCapsule( + capsule.address_assign_capsule()); + case CapsuleType::ADDRESS_REQUEST: + if (connect_ip_visitor_ == nullptr) { + return true; + } + return connect_ip_visitor_->OnAddressRequestCapsule( + capsule.address_request_capsule()); + case CapsuleType::ROUTE_ADVERTISEMENT: + if (connect_ip_visitor_ == nullptr) { + return true; + } + return connect_ip_visitor_->OnRouteAdvertisementCapsule( + capsule.route_advertisement_capsule()); } return true; } @@ -1441,6 +1463,43 @@ datagram_visitor_ = visitor; } +void QuicSpdyStream::RegisterConnectIpVisitor(ConnectIpVisitor* visitor) { + if (visitor == nullptr) { + QUIC_BUG(null connect - ip visitor) + << ENDPOINT << "Null connect-ip visitor for stream ID " << id(); + return; + } + QUIC_DLOG(INFO) << ENDPOINT + << "Registering CONNECT-IP visitor with stream ID " << id(); + + if (connect_ip_visitor_ != nullptr) { + QUIC_BUG(connect - ip double registration) + << ENDPOINT << "Attempted to doubly register CONNECT-IP with stream ID " + << id(); + return; + } + connect_ip_visitor_ = visitor; +} + +void QuicSpdyStream::UnregisterConnectIpVisitor() { + if (connect_ip_visitor_ == nullptr) { + QUIC_BUG(connect - ip visitor empty during unregistration) + << ENDPOINT << "Cannot unregister CONNECT-IP visitor for stream ID " + << id(); + return; + } + QUIC_DLOG(INFO) << ENDPOINT + << "Unregistering CONNECT-IP visitor for stream ID " << id(); + connect_ip_visitor_ = nullptr; +} + +void QuicSpdyStream::ReplaceConnectIpVisitor(ConnectIpVisitor* visitor) { + QUIC_BUG_IF(connect - ip unknown move, connect_ip_visitor_ == nullptr) + << "Attempted to move missing CONNECT-IP visitor on HTTP/3 stream ID " + << id(); + connect_ip_visitor_ = visitor; +} + void QuicSpdyStream::SetMaxDatagramTimeInQueue( QuicTime::Delta max_time_in_queue) { spdy_session_->SetMaxDatagramTimeInQueueForStreamId(id(), max_time_in_queue);
diff --git a/quiche/quic/core/http/quic_spdy_stream.h b/quiche/quic/core/http/quic_spdy_stream.h index 62b0eea..b5b6e9e 100644 --- a/quiche/quic/core/http/quic_spdy_stream.h +++ b/quiche/quic/core/http/quic_spdy_stream.h
@@ -279,6 +279,31 @@ // Mainly meant to be used by the visitors' move operators. void ReplaceHttp3DatagramVisitor(Http3DatagramVisitor* visitor); + class QUIC_EXPORT_PRIVATE ConnectIpVisitor { + public: + virtual ~ConnectIpVisitor() {} + + virtual bool OnAddressAssignCapsule( + const AddressAssignCapsule& capsule) = 0; + virtual bool OnAddressRequestCapsule( + const AddressRequestCapsule& capsule) = 0; + virtual bool OnRouteAdvertisementCapsule( + const RouteAdvertisementCapsule& capsule) = 0; + virtual void OnHeadersWritten() = 0; + }; + + // Registers |visitor| to receive CONNECT-IP capsules. |visitor| must be + // valid until a corresponding call to UnregisterConnectIpVisitor. + void RegisterConnectIpVisitor(ConnectIpVisitor* visitor); + + // Unregisters a CONNECT-IP visitor. Must only be called after a call to + // RegisterConnectIpVisitor. + void UnregisterConnectIpVisitor(); + + // Replaces the current CONNECT-IP visitor with a different visitor. + // Mainly meant to be used by the visitors' move operators. + void ReplaceConnectIpVisitor(ConnectIpVisitor* visitor); + // Sets max datagram time in queue. void SetMaxDatagramTimeInQueue(QuicTime::Delta max_time_in_queue); @@ -449,6 +474,8 @@ // HTTP/3 Datagram support. Http3DatagramVisitor* datagram_visitor_ = nullptr; + // CONNECT-IP support. + ConnectIpVisitor* connect_ip_visitor_ = nullptr; }; } // namespace quic
diff --git a/quiche/quic/core/http/quic_spdy_stream_test.cc b/quiche/quic/core/http/quic_spdy_stream_test.cc index 4695f25..c6cd92c 100644 --- a/quiche/quic/core/http/quic_spdy_stream_test.cc +++ b/quiche/quic/core/http/quic_spdy_stream_test.cc
@@ -15,6 +15,7 @@ #include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" #include "quiche/quic/core/crypto/null_encrypter.h" +#include "quiche/quic/core/http/capsule.h" #include "quiche/quic/core/http/http_encoder.h" #include "quiche/quic/core/http/quic_spdy_session.h" #include "quiche/quic/core/http/spdy_utils.h" @@ -36,6 +37,7 @@ #include "quiche/quic/test_tools/quic_spdy_stream_peer.h" #include "quiche/quic/test_tools/quic_stream_peer.h" #include "quiche/quic/test_tools/quic_test_utils.h" +#include "quiche/common/quiche_ip_address.h" #include "quiche/common/quiche_mem_slice_storage.h" #include "quiche/common/simple_buffer_allocator.h" @@ -3161,6 +3163,64 @@ EXPECT_GT(stream_->GetMaxDatagramSize(), 512u); } +TEST_P(QuicSpdyStreamTest, Capsules) { + if (!UsesHttp3()) { + return; + } + Initialize(kShouldProcessData); + session_->set_local_http_datagram_support(HttpDatagramSupport::kDraft09); + QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(), + HttpDatagramSupport::kDraft09); + SavingHttp3DatagramVisitor h3_datagram_visitor; + stream_->RegisterHttp3DatagramVisitor(&h3_datagram_visitor); + SavingConnectIpVisitor connect_ip_visitor; + stream_->RegisterConnectIpVisitor(&connect_ip_visitor); + headers_[":method"] = "CONNECT"; + headers_[":protocol"] = "fake-capsule-protocol"; + ProcessHeaders(/*fin=*/false, headers_); + // Datagram capsule. + std::string http_datagram_payload = {1, 2, 3, 4, 5, 6}; + stream_->OnCapsule(Capsule::DatagramWithoutContext(http_datagram_payload)); + EXPECT_THAT(h3_datagram_visitor.received_h3_datagrams(), + ElementsAre(SavingHttp3DatagramVisitor::SavedHttp3Datagram{ + stream_->id(), http_datagram_payload})); + // Address assign capsule. + PrefixWithId ip_prefix_with_id; + ip_prefix_with_id.request_id = 1; + quiche::QuicheIpAddress ip_address; + ip_address.FromString("::"); + ip_prefix_with_id.ip_prefix = + quiche::QuicheIpPrefix(ip_address, /*prefix_length=*/96); + Capsule address_assign_capsule = Capsule::AddressAssign(); + address_assign_capsule.address_assign_capsule().assigned_addresses.push_back( + ip_prefix_with_id); + stream_->OnCapsule(address_assign_capsule); + EXPECT_THAT(connect_ip_visitor.received_address_assign_capsules(), + ElementsAre(address_assign_capsule.address_assign_capsule())); + // Address request capsule. + Capsule address_request_capsule = Capsule::AddressRequest(); + address_request_capsule.address_request_capsule() + .requested_addresses.push_back(ip_prefix_with_id); + stream_->OnCapsule(address_request_capsule); + EXPECT_THAT(connect_ip_visitor.received_address_request_capsules(), + ElementsAre(address_request_capsule.address_request_capsule())); + // Route advertisement capsule. + Capsule route_advertisement_capsule = Capsule::RouteAdvertisement(); + IpAddressRange ip_address_range; + ip_address_range.start_ip_address.FromString("192.0.2.24"); + ip_address_range.end_ip_address.FromString("192.0.2.42"); + ip_address_range.ip_protocol = 0; + route_advertisement_capsule.route_advertisement_capsule() + .ip_address_ranges.push_back(ip_address_range); + stream_->OnCapsule(route_advertisement_capsule); + EXPECT_THAT( + connect_ip_visitor.received_route_advertisement_capsules(), + ElementsAre(route_advertisement_capsule.route_advertisement_capsule())); + // Cleanup. + stream_->UnregisterHttp3DatagramVisitor(); + stream_->UnregisterConnectIpVisitor(); +} + TEST_P(QuicSpdyStreamTest, QUIC_TEST_DISABLED_IN_CHROME(HeadersAccumulatorNullptr)) { if (!UsesHttp3()) {
diff --git a/quiche/quic/test_tools/quic_test_utils.h b/quiche/quic/test_tools/quic_test_utils.h index 7981335..9808498 100644 --- a/quiche/quic/test_tools/quic_test_utils.h +++ b/quiche/quic/test_tools/quic_test_utils.h
@@ -2100,6 +2100,46 @@ std::vector<SavedHttp3Datagram> received_h3_datagrams_; }; +// Implementation of ConnectIpVisitor which saves all received capsules. +class SavingConnectIpVisitor : public QuicSpdyStream::ConnectIpVisitor { + public: + const std::vector<AddressAssignCapsule>& received_address_assign_capsules() + const { + return received_address_assign_capsules_; + } + const std::vector<AddressRequestCapsule>& received_address_request_capsules() + const { + return received_address_request_capsules_; + } + const std::vector<RouteAdvertisementCapsule>& + received_route_advertisement_capsules() const { + return received_route_advertisement_capsules_; + } + bool headers_written() const { return headers_written_; } + + // From QuicSpdyStream::ConnectIpVisitor. + bool OnAddressAssignCapsule(const AddressAssignCapsule& capsule) override { + received_address_assign_capsules_.push_back(capsule); + return true; + } + bool OnAddressRequestCapsule(const AddressRequestCapsule& capsule) override { + received_address_request_capsules_.push_back(capsule); + return true; + } + bool OnRouteAdvertisementCapsule( + const RouteAdvertisementCapsule& capsule) override { + received_route_advertisement_capsules_.push_back(capsule); + return true; + } + void OnHeadersWritten() override { headers_written_ = true; } + + private: + std::vector<AddressAssignCapsule> received_address_assign_capsules_; + std::vector<AddressRequestCapsule> received_address_request_capsules_; + std::vector<RouteAdvertisementCapsule> received_route_advertisement_capsules_; + bool headers_written_ = false; +}; + inline std::string EscapeTestParamName(absl::string_view name) { std::string result(name); // Escape all characters that are not allowed by gtest ([a-zA-Z0-9_]).