Implement draft-ietf-masque-connect-ip-dns-06 This is intended for IETF interop testing purposes. It does not actually act on the capsules on the client, it only logs them. I've confirmed that our server can correctly exchange info with our client. Gemini wrote this code but I reviewed it. PiperOrigin-RevId: 959335757
diff --git a/build/source_list.bzl b/build/source_list.bzl index f7f628d..a94ca22 100644 --- a/build/source_list.bzl +++ b/build/source_list.bzl
@@ -1017,6 +1017,11 @@ "balsa/simple_buffer.cc", "balsa/standard_header_map.cc", ] +masque_test_hdrs = [ +] +masque_test_srcs = [ + "quic/masque/masque_utils_test.cc", +] masque_private_tokens_hdrs = [ "quic/masque/private_tokens.h", ]
diff --git a/build/source_list.gni b/build/source_list.gni index 43668e7..3e66444 100644 --- a/build/source_list.gni +++ b/build/source_list.gni
@@ -1017,6 +1017,12 @@ "src/quiche/balsa/simple_buffer.cc", "src/quiche/balsa/standard_header_map.cc", ] +masque_test_hdrs = [ + +] +masque_test_srcs = [ + "src/quiche/quic/masque/masque_utils_test.cc", +] masque_private_tokens_hdrs = [ "src/quiche/quic/masque/private_tokens.h", ]
diff --git a/build/source_list.json b/build/source_list.json index be9d62c..17e3ed6 100644 --- a/build/source_list.json +++ b/build/source_list.json
@@ -1016,6 +1016,12 @@ "quiche/balsa/simple_buffer.cc", "quiche/balsa/standard_header_map.cc" ], + "masque_test_hdrs": [ + + ], + "masque_test_srcs": [ + "quiche/quic/masque/masque_utils_test.cc" + ], "masque_private_tokens_hdrs": [ "quiche/quic/masque/private_tokens.h" ],
diff --git a/quiche/quic/masque/masque_client_bin.cc b/quiche/quic/masque/masque_client_bin.cc index 0dfc958..7ed0e27 100644 --- a/quiche/quic/masque/masque_client_bin.cc +++ b/quiche/quic/masque/masque_client_bin.cc
@@ -159,6 +159,14 @@ // Consider installing routes. return true; } + bool OnDnsAssignCapsule(const DnsAssignCapsule& capsule) override { + QUIC_LOG(INFO) << "Received capsule " << capsule.ToString(); + return true; + } + bool OnPref64Capsule(const Pref64Capsule& capsule) override { + QUIC_LOG(INFO) << "Received capsule " << capsule.ToString(); + return true; + } // QuicSocketEventListener void OnSocketEvent(QuicEventLoop* /*event_loop*/, QuicUdpSocketFd fd,
diff --git a/quiche/quic/masque/masque_client_session.cc b/quiche/quic/masque/masque_client_session.cc index 98f3bdd..fcf7cae 100644 --- a/quiche/quic/masque/masque_client_session.cc +++ b/quiche/quic/masque/masque_client_session.cc
@@ -363,6 +363,30 @@ << DatagramStatusToString(message_status); } +void MasqueClientSession::SendDnsAssignCapsule( + const DnsAssignCapsule& capsule, + MasqueClientSession::EncapsulatedIpSession* encapsulated_ip_session) { + const ConnectIpClientState* connect_ip = + GetOrCreateConnectIpClientState(encapsulated_ip_session); + if (connect_ip == nullptr) { + QUIC_LOG(ERROR) << "Failed to get CONNECT-IP request for DNS_ASSIGN"; + return; + } + WriteDnsAssignCapsule(connect_ip->stream(), capsule); +} + +void MasqueClientSession::SendPref64Capsule( + const Pref64Capsule& capsule, + MasqueClientSession::EncapsulatedIpSession* encapsulated_ip_session) { + const ConnectIpClientState* connect_ip = + GetOrCreateConnectIpClientState(encapsulated_ip_session); + if (connect_ip == nullptr) { + QUIC_LOG(ERROR) << "Failed to get CONNECT-IP request for PREF64"; + return; + } + WritePref64Capsule(connect_ip->stream(), capsule); +} + void MasqueClientSession::SendEthernetFrame( absl::string_view frame, MasqueClientSession::EncapsulatedEthernetSession* encapsulated_ethernet_session) { @@ -930,6 +954,25 @@ << " IP bytes to connection for stream ID " << stream_id; } +void MasqueClientSession::ConnectIpClientState::OnUnknownCapsule( + QuicStreamId /*stream_id*/, const quiche::UnknownCapsule& capsule) { + if (capsule.type == kDnsAssignCapsuleType) { + DnsAssignCapsule dns_assign_capsule; + if (ParseDnsAssignCapsulePayload(capsule.payload, &dns_assign_capsule)) { + encapsulated_ip_session_->OnDnsAssignCapsule(dns_assign_capsule); + } else { + QUIC_LOG(ERROR) << "Failed to parse DNS_ASSIGN capsule"; + } + } else if (capsule.type == kPref64CapsuleType) { + Pref64Capsule pref64_capsule; + if (ParsePref64CapsulePayload(capsule.payload, &pref64_capsule)) { + encapsulated_ip_session_->OnPref64Capsule(pref64_capsule); + } else { + QUIC_LOG(ERROR) << "Failed to parse PREF64 capsule"; + } + } +} + bool MasqueClientSession::ConnectIpClientState::OnAddressAssignCapsule( const AddressAssignCapsule& capsule) { return encapsulated_ip_session_->OnAddressAssignCapsule(capsule);
diff --git a/quiche/quic/masque/masque_client_session.h b/quiche/quic/masque/masque_client_session.h index acf1f9f..f3aac5e 100644 --- a/quiche/quic/masque/masque_client_session.h +++ b/quiche/quic/masque/masque_client_session.h
@@ -92,6 +92,12 @@ const quiche::AddressRequestCapsule& capsule) = 0; virtual bool OnRouteAdvertisementCapsule( const quiche::RouteAdvertisementCapsule& capsule) = 0; + virtual bool OnDnsAssignCapsule(const DnsAssignCapsule& /*capsule*/) { + return true; + } + virtual bool OnPref64Capsule(const Pref64Capsule& /*capsule*/) { + return true; + } }; // CONNECT-ETHERNET. @@ -158,6 +164,14 @@ void SendIpPacket(absl::string_view packet, EncapsulatedIpSession* encapsulated_ip_session); + // Send DNS_ASSIGN capsule over CONNECT-IP stream. + void SendDnsAssignCapsule(const DnsAssignCapsule& capsule, + EncapsulatedIpSession* encapsulated_ip_session); + + // Send PREF64 capsule over CONNECT-IP stream. + void SendPref64Capsule(const Pref64Capsule& capsule, + EncapsulatedIpSession* encapsulated_ip_session); + // Send encapsulated Ethernet frame. |frame| contains the Ethernet // header and payload. void SendEthernetFrame( @@ -302,8 +316,8 @@ // From QuicSpdyStream::Http3DatagramVisitor. void OnHttp3Datagram(QuicStreamId stream_id, absl::string_view payload) override; - void OnUnknownCapsule(QuicStreamId /*stream_id*/, - const quiche::UnknownCapsule& /*capsule*/) override {} + void OnUnknownCapsule(QuicStreamId stream_id, + const quiche::UnknownCapsule& capsule) override; // From QuicSpdyStream::ConnectIpVisitor. bool OnAddressAssignCapsule(
diff --git a/quiche/quic/masque/masque_encapsulated_client.cc b/quiche/quic/masque/masque_encapsulated_client.cc index addffd9..09e40ed 100644 --- a/quiche/quic/masque/masque_encapsulated_client.cc +++ b/quiche/quic/masque/masque_encapsulated_client.cc
@@ -337,6 +337,17 @@ QuicDefaultClient::session()); } +void MasqueEncapsulatedClient::SendDnsAssignCapsule( + const DnsAssignCapsule& capsule) { + masque_client_->masque_client_session()->SendDnsAssignCapsule( + capsule, masque_encapsulated_client_session()); +} + +void MasqueEncapsulatedClient::SendPref64Capsule(const Pref64Capsule& capsule) { + masque_client_->masque_client_session()->SendPref64Capsule( + capsule, masque_encapsulated_client_session()); +} + QuicByteCount MaxPacketSizeForEncapsulatedConnections( MasqueClient* underlying_masque_client) { QuicByteCount max_packet_size =
diff --git a/quiche/quic/masque/masque_encapsulated_client.h b/quiche/quic/masque/masque_encapsulated_client.h index 46a40b9..883c2fc 100644 --- a/quiche/quic/masque/masque_encapsulated_client.h +++ b/quiche/quic/masque/masque_encapsulated_client.h
@@ -53,6 +53,12 @@ // Client session for this client. MasqueEncapsulatedClientSession* masque_encapsulated_client_session(); + // Send DNS_ASSIGN capsule over CONNECT-IP stream. + void SendDnsAssignCapsule(const DnsAssignCapsule& capsule); + + // Send PREF64 capsule over CONNECT-IP stream. + void SendPref64Capsule(const Pref64Capsule& capsule); + private: // Constructor for when this client is both encapsulated and underlying. MasqueEncapsulatedClient(QuicSocketAddress server_address,
diff --git a/quiche/quic/masque/masque_encapsulated_client_session.cc b/quiche/quic/masque/masque_encapsulated_client_session.cc index 3bc8256..25d14c6 100644 --- a/quiche/quic/masque/masque_encapsulated_client_session.cc +++ b/quiche/quic/masque/masque_encapsulated_client_session.cc
@@ -268,4 +268,16 @@ return true; } +bool MasqueEncapsulatedClientSession::OnDnsAssignCapsule( + const DnsAssignCapsule& capsule) { + QUIC_LOG(INFO) << "Received capsule " << capsule.ToString(); + return true; +} + +bool MasqueEncapsulatedClientSession::OnPref64Capsule( + const Pref64Capsule& capsule) { + QUIC_LOG(INFO) << "Received capsule " << capsule.ToString(); + return true; +} + } // namespace quic
diff --git a/quiche/quic/masque/masque_encapsulated_client_session.h b/quiche/quic/masque/masque_encapsulated_client_session.h index f80b7dd..8485160 100644 --- a/quiche/quic/masque/masque_encapsulated_client_session.h +++ b/quiche/quic/masque/masque_encapsulated_client_session.h
@@ -79,6 +79,8 @@ const quiche::AddressRequestCapsule& capsule) override; bool OnRouteAdvertisementCapsule( const quiche::RouteAdvertisementCapsule& capsule) override; + bool OnDnsAssignCapsule(const DnsAssignCapsule& capsule) override; + bool OnPref64Capsule(const Pref64Capsule& capsule) override; // From QuicSession. void OnConnectionClosed(const QuicConnectionCloseFrame& frame,
diff --git a/quiche/quic/masque/masque_server_backend.h b/quiche/quic/masque/masque_server_backend.h index 7735f2b..2714771 100644 --- a/quiche/quic/masque/masque_server_backend.h +++ b/quiche/quic/masque/masque_server_backend.h
@@ -8,6 +8,7 @@ #include <cstdint> #include <list> #include <memory> +#include <optional> #include <string> #include <vector> @@ -94,6 +95,23 @@ return concealed_auth_on_all_requests_; } + // Configured DNS_ASSIGN capsule to send to CONNECT-IP clients upon + // connection. + void SetDnsAssignCapsule(const DnsAssignCapsule& capsule) { + dns_assign_capsule_ = capsule; + } + const std::optional<DnsAssignCapsule>& dns_assign_capsule() const { + return dns_assign_capsule_; + } + + // Configured PREF64 capsule to send to CONNECT-IP clients upon connection. + void SetPref64Capsule(const Pref64Capsule& capsule) { + pref64_capsule_ = capsule; + } + const std::optional<Pref64Capsule>& pref64_capsule() const { + return pref64_capsule_; + } + private: // Handle MASQUE request. bool MaybeHandleMasqueRequest( @@ -115,6 +133,8 @@ }; std::list<ConcealedAuthCredential> concealed_auth_credentials_; bool concealed_auth_on_all_requests_ = false; + std::optional<DnsAssignCapsule> dns_assign_capsule_; + std::optional<Pref64Capsule> pref64_capsule_; }; } // namespace quic
diff --git a/quiche/quic/masque/masque_server_bin.cc b/quiche/quic/masque/masque_server_bin.cc index 7a99bdb..d12b638 100644 --- a/quiche/quic/masque/masque_server_bin.cc +++ b/quiche/quic/masque/masque_server_bin.cc
@@ -9,9 +9,13 @@ #include <cstdint> #include <memory> +#include <optional> #include <string> +#include <utility> #include <vector> +#include "absl/strings/str_split.h" +#include "absl/strings/string_view.h" #include "quiche/quic/masque/masque_server.h" #include "quiche/quic/masque/masque_server_backend.h" #include "quiche/quic/masque/masque_utils.h" @@ -51,6 +55,38 @@ "If set to true, enable concealed auth on all requests (such as GET) " "instead of just MASQUE."); +DEFINE_QUICHE_COMMAND_LINE_FLAG( + std::string, dns_server_v4, "", + "IPv4 address of the DNS server to send in the DNS_ASSIGN capsule " + "formatted to CONNECT-IP clients. E.g., \"8.8.8.8\"."); + +DEFINE_QUICHE_COMMAND_LINE_FLAG( + std::string, dns_server_v6, "", + "IPv6 address of the DNS server to send in the DNS_ASSIGN capsule " + "formatted to CONNECT-IP clients. E.g., \"2001:4860:4860::8888\"."); + +DEFINE_QUICHE_COMMAND_LINE_FLAG( + int32_t, dns_priority, 1, + "Service priority of the DNS server (must be non-zero per specification)."); + +DEFINE_QUICHE_COMMAND_LINE_FLAG( + std::string, dns_auth_domain, "", + "Authentication domain name of the DNS server to send in the DNS_ASSIGN " + "capsule (empty if unencrypted DNS on port 53). E.g., \"dns.google\"."); + +DEFINE_QUICHE_COMMAND_LINE_FLAG( + std::string, dns_service_parameters, "", + "Service parameters applying to the DNS nameserver. E.g., \"alpn=h2,h3\"."); + +DEFINE_QUICHE_COMMAND_LINE_FLAG( + std::string, dns_internal_domains, "", + "Internal domains authoritative for this DNS server (comma separated). " + "An empty string or \"/root\" indicates the DNS root."); + +DEFINE_QUICHE_COMMAND_LINE_FLAG( + std::string, dns_search_domains, "", + "Search domains authoritative for this DNS server (comma separated)."); + int main(int argc, char* argv[]) { const char* usage = "Usage: masque_server [options]"; std::vector<std::string> non_option_args = @@ -77,6 +113,90 @@ backend->SetConcealedAuthOnAllRequests( quiche::GetQuicheCommandLineFlag(FLAGS_concealed_auth_on_all_requests)); + std::string dns_v4_str = + quiche::GetQuicheCommandLineFlag(FLAGS_dns_server_v4); + std::string dns_v6_str = + quiche::GetQuicheCommandLineFlag(FLAGS_dns_server_v6); + if (!dns_v4_str.empty() || !dns_v6_str.empty()) { + quic::DnsAssignCapsule dns_capsule; + quic::DnsConfiguration config; + quic::DnsNameserver ns; + int32_t priority = quiche::GetQuicheCommandLineFlag(FLAGS_dns_priority); + if (priority <= 0 || priority > 65535) { + QUIC_LOG(ERROR) << "Invalid dns_priority: " << priority << "; using 1."; + ns.service_priority = 1; + } else { + ns.service_priority = static_cast<uint16_t>(priority); + } + if (!dns_v4_str.empty()) { + for (absl::string_view ip_piece : absl::StrSplit(dns_v4_str, ',')) { + quic::QuicIpAddress ip; + if (ip.FromString(std::string(ip_piece)) && ip.IsIPv4()) { + ns.ipv4_addresses.push_back(ip); + } else { + QUIC_LOG(ERROR) << "Invalid IPv4 address in --dns_server_v4: " + << ip_piece; + } + } + } + if (!dns_v6_str.empty()) { + for (absl::string_view ip_piece : absl::StrSplit(dns_v6_str, ',')) { + quic::QuicIpAddress ip; + if (ip.FromString(std::string(ip_piece)) && ip.IsIPv6()) { + ns.ipv6_addresses.push_back(ip); + } else { + QUIC_LOG(ERROR) << "Invalid IPv6 address in --dns_server_v6: " + << ip_piece; + } + } + } + ns.authentication_domain_name.domain_name = + quiche::GetQuicheCommandLineFlag(FLAGS_dns_auth_domain); + std::string service_params_str = + quiche::GetQuicheCommandLineFlag(FLAGS_dns_service_parameters); + if (!service_params_str.empty()) { + std::optional<std::string> encoded_params = + quic::EncodeSvcParams(service_params_str); + if (!encoded_params) { + QUIC_LOG(ERROR) + << "Invalid SVCB presentation format in --dns_service_parameters: " + << service_params_str; + } else { + ns.service_parameters = *std::move(encoded_params); + } + } + config.nameservers.push_back(std::move(ns)); + + std::string internal_doms = + quiche::GetQuicheCommandLineFlag(FLAGS_dns_internal_domains); + if (!internal_doms.empty()) { + for (absl::string_view dom : absl::StrSplit(internal_doms, ',')) { + if (dom == "/root") { + config.internal_domains.push_back(quic::DnsDomain{""}); + } else { + config.internal_domains.push_back(quic::DnsDomain{std::string(dom)}); + } + } + } else { + config.internal_domains.push_back(quic::DnsDomain{""}); + } + + std::string search_doms = + quiche::GetQuicheCommandLineFlag(FLAGS_dns_search_domains); + if (!search_doms.empty()) { + for (absl::string_view dom : absl::StrSplit(search_doms, ',')) { + if (!dom.empty()) { + config.search_domains.push_back(quic::DnsDomain{std::string(dom)}); + } + } + } + + dns_capsule.dns_configurations.push_back(std::move(config)); + backend->SetDnsAssignCapsule(dns_capsule); + QUIC_LOG(INFO) << "Configured " << dns_capsule.ToString() + << " to send to CONNECT-IP clients."; + } + auto server = std::make_unique<quic::MasqueServer>(masque_mode, backend.get());
diff --git a/quiche/quic/masque/masque_server_session.cc b/quiche/quic/masque/masque_server_session.cc index cbed99a..5b3b7b0 100644 --- a/quiche/quic/masque/masque_server_session.cc +++ b/quiche/quic/masque/masque_server_session.cc
@@ -1210,12 +1210,43 @@ } } +void MasqueServerSession::ConnectIpServerState::OnUnknownCapsule( + QuicStreamId /*stream_id*/, const quiche::UnknownCapsule& capsule) { + if (capsule.type == kDnsAssignCapsuleType) { + DnsAssignCapsule dns_assign_capsule; + if (ParseDnsAssignCapsulePayload(capsule.payload, &dns_assign_capsule)) { + OnDnsAssignCapsule(dns_assign_capsule); + } else { + QUIC_LOG(ERROR) << "Failed to parse DNS_ASSIGN capsule"; + } + } else if (capsule.type == kPref64CapsuleType) { + Pref64Capsule pref64_capsule; + if (ParsePref64CapsulePayload(capsule.payload, &pref64_capsule)) { + OnPref64Capsule(pref64_capsule); + } else { + QUIC_LOG(ERROR) << "Failed to parse PREF64 capsule"; + } + } +} + bool MasqueServerSession::ConnectIpServerState::OnAddressAssignCapsule( const AddressAssignCapsule& capsule) { QUIC_DLOG(INFO) << "Ignoring received capsule " << capsule.ToString(); return true; } +bool MasqueServerSession::ConnectIpServerState::OnDnsAssignCapsule( + const DnsAssignCapsule& capsule) { + QUIC_LOG(INFO) << "Ignoring received capsule " << capsule.ToString(); + return true; +} + +bool MasqueServerSession::ConnectIpServerState::OnPref64Capsule( + const Pref64Capsule& capsule) { + QUIC_LOG(INFO) << "Ignoring received capsule " << capsule.ToString(); + return true; +} + bool MasqueServerSession::ConnectIpServerState::OnAddressRequestCapsule( const AddressRequestCapsule& capsule) { QUIC_DLOG(INFO) << "Ignoring received capsule " << capsule.ToString(); @@ -1245,6 +1276,29 @@ route_advertisement.route_advertisement_capsule().ip_address_ranges.push_back( default_route); stream()->WriteCapsule(route_advertisement); + + if (masque_session_->masque_server_backend()->dns_assign_capsule()) { + WriteDnsAssignCapsule( + stream(), + *masque_session_->masque_server_backend()->dns_assign_capsule()); + } + if (masque_session_->masque_server_backend()->pref64_capsule()) { + WritePref64Capsule( + stream(), *masque_session_->masque_server_backend()->pref64_capsule()); + } +} + +void MasqueServerSession::SendDnsAssignCapsule( + const DnsAssignCapsule& capsule) { + for (auto& state : connect_ip_server_states_) { + WriteDnsAssignCapsule(state.stream(), capsule); + } +} + +void MasqueServerSession::SendPref64Capsule(const Pref64Capsule& capsule) { + for (auto& state : connect_ip_server_states_) { + WritePref64Capsule(state.stream(), capsule); + } } // Connect Ethernet
diff --git a/quiche/quic/masque/masque_server_session.h b/quiche/quic/masque/masque_server_session.h index d6b79c3..ab9b666 100644 --- a/quiche/quic/masque/masque_server_session.h +++ b/quiche/quic/masque/masque_server_session.h
@@ -83,6 +83,15 @@ QuicSocketEventMask events) override; QuicEventLoop* event_loop() const { return event_loop_; } + MasqueServerBackend* masque_server_backend() const { + return masque_server_backend_; + } + + // Send DNS_ASSIGN capsule to all active CONNECT-IP client streams. + void SendDnsAssignCapsule(const DnsAssignCapsule& capsule); + + // Send PREF64 capsule to all active CONNECT-IP client streams. + void SendPref64Capsule(const Pref64Capsule& capsule); private: bool HandleConnectUdpSocketEvent(QuicUdpSocketFd fd, @@ -181,8 +190,8 @@ // From QuicSpdyStream::Http3DatagramVisitor. void OnHttp3Datagram(QuicStreamId stream_id, absl::string_view payload) override; - void OnUnknownCapsule(QuicStreamId /*stream_id*/, - const quiche::UnknownCapsule& /*capsule*/) override {} + void OnUnknownCapsule(QuicStreamId stream_id, + const quiche::UnknownCapsule& capsule) override; // From QuicSpdyStream::ConnectIpVisitor. bool OnAddressAssignCapsule( @@ -191,6 +200,8 @@ const quiche::AddressRequestCapsule& capsule) override; bool OnRouteAdvertisementCapsule( const quiche::RouteAdvertisementCapsule& capsule) override; + virtual bool OnDnsAssignCapsule(const DnsAssignCapsule& capsule); + virtual bool OnPref64Capsule(const Pref64Capsule& capsule); void OnHeadersWritten() override; private:
diff --git a/quiche/quic/masque/masque_utils.cc b/quiche/quic/masque/masque_utils.cc index 18661c1..8f86552 100644 --- a/quiche/quic/masque/masque_utils.cc +++ b/quiche/quic/masque/masque_utils.cc
@@ -6,17 +6,28 @@ #include <cstdint> #include <cstring> +#include <optional> #include <ostream> #include <string> #include <utility> +#include <vector> +#include "absl/container/btree_map.h" #include "absl/strings/escaping.h" +#include "absl/strings/match.h" +#include "absl/strings/numbers.h" #include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" +#include "absl/strings/str_split.h" #include "absl/strings/string_view.h" +#include "quiche/quic/core/http/quic_spdy_stream.h" +#include "quiche/quic/core/quic_data_reader.h" #include "quiche/quic/core/quic_data_writer.h" #include "quiche/quic/core/quic_versions.h" +#include "quiche/quic/platform/api/quic_bug_tracker.h" #include "quiche/quic/platform/api/quic_ip_address.h" #include "quiche/quic/platform/api/quic_logging.h" +#include "quiche/common/capsule.h" #include "quiche/common/platform/api/quiche_command_line_flags.h" #include "quiche/common/platform/api/quiche_logging.h" @@ -284,4 +295,624 @@ std::string(1, 0x00), signature_input); } +namespace { + +bool ParseDnsDomain(QuicDataReader& reader, DnsDomain* domain) { + if (domain == nullptr) { + return false; + } + absl::string_view piece; + if (!reader.ReadStringPieceVarInt62(&piece)) { + return false; + } + domain->domain_name = std::string(piece); + return true; +} + +size_t ComputeDnsDomainLength(const DnsDomain& domain) { + return QuicDataWriter::GetVarInt62Len(domain.domain_name.size()) + + domain.domain_name.size(); +} + +size_t ComputeDnsNameserverLength(const DnsNameserver& ns) { + size_t total = sizeof(ns.service_priority); + total += QuicDataWriter::GetVarInt62Len(ns.ipv4_addresses.size()); + total += ns.ipv4_addresses.size() * QuicIpAddress::kIPv4AddressSize; + total += QuicDataWriter::GetVarInt62Len(ns.ipv6_addresses.size()); + total += ns.ipv6_addresses.size() * QuicIpAddress::kIPv6AddressSize; + total += ComputeDnsDomainLength(ns.authentication_domain_name); + total += QuicDataWriter::GetVarInt62Len(ns.service_parameters.size()); + total += ns.service_parameters.size(); + return total; +} + +size_t ComputeDnsConfigurationLength(const DnsConfiguration& config) { + size_t total = QuicDataWriter::GetVarInt62Len(config.nameservers.size()); + for (const DnsNameserver& ns : config.nameservers) { + total += ComputeDnsNameserverLength(ns); + } + total += QuicDataWriter::GetVarInt62Len(config.internal_domains.size()); + for (const DnsDomain& domain : config.internal_domains) { + total += ComputeDnsDomainLength(domain); + } + total += QuicDataWriter::GetVarInt62Len(config.search_domains.size()); + for (const DnsDomain& domain : config.search_domains) { + total += ComputeDnsDomainLength(domain); + } + return total; +} + +} // namespace + +bool ParseDnsAssignCapsulePayload(absl::string_view payload, + DnsAssignCapsule* capsule) { + if (capsule == nullptr) { + return false; + } + capsule->dns_configurations.clear(); + QuicDataReader reader(payload); + while (!reader.IsDoneReading()) { + DnsConfiguration config; + uint64_t nameserver_count = 0; + if (!reader.ReadVarInt62(&nameserver_count)) { + return false; + } + for (uint64_t i = 0; i < nameserver_count; ++i) { + DnsNameserver ns; + if (!reader.ReadUInt16(&ns.service_priority)) { + return false; + } + if (ns.service_priority == 0) { + return false; + } + uint64_t ipv4_count = 0; + if (!reader.ReadVarInt62(&ipv4_count)) { + return false; + } + for (uint64_t j = 0; j < ipv4_count; ++j) { + absl::string_view ip_bytes; + if (!reader.ReadStringPiece(&ip_bytes, + QuicIpAddress::kIPv4AddressSize)) { + return false; + } + QuicIpAddress ip; + if (!ip.FromPackedString(ip_bytes.data(), + QuicIpAddress::kIPv4AddressSize)) { + return false; + } + ns.ipv4_addresses.push_back(ip); + } + uint64_t ipv6_count = 0; + if (!reader.ReadVarInt62(&ipv6_count)) { + return false; + } + for (uint64_t j = 0; j < ipv6_count; ++j) { + absl::string_view ip_bytes; + if (!reader.ReadStringPiece(&ip_bytes, + QuicIpAddress::kIPv6AddressSize)) { + return false; + } + QuicIpAddress ip; + if (!ip.FromPackedString(ip_bytes.data(), + QuicIpAddress::kIPv6AddressSize)) { + return false; + } + ns.ipv6_addresses.push_back(ip); + } + if (!ParseDnsDomain(reader, &ns.authentication_domain_name)) { + return false; + } + absl::string_view sp_piece; + if (!reader.ReadStringPieceVarInt62(&sp_piece)) { + return false; + } + if (!sp_piece.empty() && !DecodeSvcParams(sp_piece).has_value()) { + return false; + } + ns.service_parameters = std::string(sp_piece); + config.nameservers.push_back(std::move(ns)); + } + + uint64_t internal_domain_count = 0; + if (!reader.ReadVarInt62(&internal_domain_count)) { + return false; + } + for (uint64_t i = 0; i < internal_domain_count; ++i) { + DnsDomain domain; + if (!ParseDnsDomain(reader, &domain)) { + return false; + } + config.internal_domains.push_back(std::move(domain)); + } + + uint64_t search_domain_count = 0; + if (!reader.ReadVarInt62(&search_domain_count)) { + return false; + } + for (uint64_t i = 0; i < search_domain_count; ++i) { + DnsDomain domain; + if (!ParseDnsDomain(reader, &domain)) { + return false; + } + config.search_domains.push_back(std::move(domain)); + } + + capsule->dns_configurations.push_back(std::move(config)); + } + return true; +} + +bool ParsePref64CapsulePayload(absl::string_view payload, + Pref64Capsule* capsule) { + if (capsule == nullptr) { + return false; + } + capsule->nat64_prefixes.clear(); + if (payload.size() % 13 != 0) { + return false; + } + QuicDataReader reader(payload); + while (!reader.IsDoneReading()) { + Pref64Prefix prefix; + if (!reader.ReadUInt8(&prefix.prefix_length)) { + return false; + } + if (prefix.prefix_length != 32 && prefix.prefix_length != 40 && + prefix.prefix_length != 48 && prefix.prefix_length != 56 && + prefix.prefix_length != 64 && prefix.prefix_length != 96) { + return false; + } + absl::string_view prefix_bytes; + if (!reader.ReadStringPiece(&prefix_bytes, 12)) { + return false; + } + char full_ipv6_bytes[16] = {}; + memcpy(full_ipv6_bytes, prefix_bytes.data(), 12); + if (!prefix.prefix.FromPackedString(full_ipv6_bytes, 16)) { + return false; + } + capsule->nat64_prefixes.push_back(std::move(prefix)); + } + return true; +} + +std::string SerializeDnsAssignCapsulePayload(const DnsAssignCapsule& capsule) { + size_t total_len = 0; + for (const DnsConfiguration& config : capsule.dns_configurations) { + total_len += ComputeDnsConfigurationLength(config); + } + std::string buffer(total_len, '\0'); + QuicDataWriter writer(buffer.size(), buffer.data()); + for (const DnsConfiguration& config : capsule.dns_configurations) { + if (!writer.WriteVarInt62(config.nameservers.size())) { + QUIC_BUG(dns_assign_serial_error) << "Failed writing nameservers count"; + return ""; + } + for (const DnsNameserver& ns : config.nameservers) { + if (!writer.WriteUInt16(ns.service_priority) || + !writer.WriteVarInt62(ns.ipv4_addresses.size())) { + QUIC_BUG(dns_assign_serial_error) + << "Failed writing priority or ipv4 count"; + return ""; + } + for (const QuicIpAddress& ip : ns.ipv4_addresses) { + if (!ip.IsIPv4()) { + QUIC_BUG(dns_assign_serial_error) << "Not an IPv4 address"; + return ""; + } + std::string packed = ip.ToPackedString(); + if (packed.size() != QuicIpAddress::kIPv4AddressSize || + !writer.WriteStringPiece(packed)) { + QUIC_BUG(dns_assign_serial_error) << "Failed writing ipv4"; + return ""; + } + } + if (!writer.WriteVarInt62(ns.ipv6_addresses.size())) { + QUIC_BUG(dns_assign_serial_error) << "Failed writing ipv6 count"; + return ""; + } + for (const QuicIpAddress& ip : ns.ipv6_addresses) { + if (!ip.IsIPv6()) { + QUIC_BUG(dns_assign_serial_error) << "Not an IPv6 address"; + return ""; + } + std::string packed = ip.ToPackedString(); + if (packed.size() != QuicIpAddress::kIPv6AddressSize || + !writer.WriteStringPiece(packed)) { + QUIC_BUG(dns_assign_serial_error) << "Failed writing ipv6"; + return ""; + } + } + if (!ns.service_parameters.empty() && + !DecodeSvcParams(ns.service_parameters).has_value()) { + QUIC_BUG(dns_assign_serial_error) + << "Invalid service parameters wire format"; + return ""; + } + if (!writer.WriteStringPieceVarInt62( + ns.authentication_domain_name.domain_name) || + !writer.WriteStringPieceVarInt62(ns.service_parameters)) { + QUIC_BUG(dns_assign_serial_error) + << "Failed writing domain name / service params"; + return ""; + } + } + if (!writer.WriteVarInt62(config.internal_domains.size())) { + QUIC_BUG(dns_assign_serial_error) + << "Failed writing internal domains count"; + return ""; + } + for (const DnsDomain& domain : config.internal_domains) { + if (!writer.WriteStringPieceVarInt62(domain.domain_name)) { + QUIC_BUG(dns_assign_serial_error) << "Failed writing internal domain"; + return ""; + } + } + if (!writer.WriteVarInt62(config.search_domains.size())) { + QUIC_BUG(dns_assign_serial_error) + << "Failed writing search domains count"; + return ""; + } + for (const DnsDomain& domain : config.search_domains) { + if (!writer.WriteStringPieceVarInt62(domain.domain_name)) { + QUIC_BUG(dns_assign_serial_error) << "Failed writing search domain"; + return ""; + } + } + } + if (writer.remaining() != 0) { + QUIC_BUG(dns_assign_serial_error) + << "Remaining length after serialization: " << writer.remaining(); + return ""; + } + return buffer; +} + +std::string SerializePref64CapsulePayload(const Pref64Capsule& capsule) { + size_t total_len = capsule.nat64_prefixes.size() * 13; + std::string buffer(total_len, '\0'); + QuicDataWriter writer(buffer.size(), buffer.data()); + for (const Pref64Prefix& prefix : capsule.nat64_prefixes) { + if (!writer.WriteUInt8(prefix.prefix_length) || !prefix.prefix.IsIPv6()) { + QUIC_BUG(pref64_serial_error) + << "Failed writing prefix length / not ipv6"; + return ""; + } + std::string packed = prefix.prefix.ToPackedString(); + if (packed.size() != 16 || + !writer.WriteStringPiece(absl::string_view(packed.data(), 12))) { + QUIC_BUG(pref64_serial_error) << "Failed writing prefix data"; + return ""; + } + } + if (writer.remaining() != 0) { + QUIC_BUG(pref64_serial_error) + << "Remaining length after serialization: " << writer.remaining(); + return ""; + } + return buffer; +} + +namespace { +constexpr uint16_t kSvcParamKeyAlpn = 1; +constexpr uint16_t kSvcParamKeyNoDefaultAlpn = 2; +constexpr uint16_t kSvcParamKeyPort = 3; +constexpr uint16_t kSvcParamKeyIpv4Hint = 4; +constexpr uint16_t kSvcParamKeyIpv6Hint = 6; +constexpr uint16_t kSvcParamKeyDohPath = 7; +constexpr uint16_t kSvcParamKeyOhttp = 8; +} // namespace + +std::optional<std::string> EncodeSvcParams(absl::string_view presentation) { + absl::btree_map<uint16_t, std::string> params; + for (absl::string_view token : absl::StrSplit( + presentation, absl::ByAnyChar(" \t\r\n"), absl::SkipWhitespace())) { + size_t equals_pos = token.find('='); + absl::string_view key_str = token.substr(0, equals_pos); + absl::string_view value_str = (equals_pos == absl::string_view::npos) + ? "" + : token.substr(equals_pos + 1); + uint16_t key = 0; + std::string wire_val; + if (key_str == "alpn") { + key = kSvcParamKeyAlpn; + for (absl::string_view alpn : absl::StrSplit(value_str, ',')) { + if (alpn.empty() || alpn.size() > 255) { + return std::nullopt; + } + wire_val.push_back(static_cast<char>(alpn.size())); + wire_val.append(alpn.data(), alpn.size()); + } + if (wire_val.empty()) { + return std::nullopt; + } + } else if (key_str == "no-default-alpn") { + key = kSvcParamKeyNoDefaultAlpn; + if (!value_str.empty()) { + return std::nullopt; + } + } else if (key_str == "port") { + key = kSvcParamKeyPort; + uint16_t port = 0; + if (!absl::SimpleAtoi(value_str, &port)) { + return std::nullopt; + } + char buf[2]; + QuicDataWriter writer(2, buf); + writer.WriteUInt16(port); + wire_val = std::string(buf, 2); + } else if (key_str == "ipv4hint") { + key = kSvcParamKeyIpv4Hint; + for (absl::string_view ip_str : absl::StrSplit(value_str, ',')) { + QuicIpAddress ip; + if (!ip.FromString(std::string(ip_str)) || !ip.IsIPv4()) { + return std::nullopt; + } + wire_val.append(ip.ToPackedString()); + } + if (wire_val.empty()) { + return std::nullopt; + } + } else if (key_str == "ipv6hint") { + key = kSvcParamKeyIpv6Hint; + for (absl::string_view ip_str : absl::StrSplit(value_str, ',')) { + QuicIpAddress ip; + if (!ip.FromString(std::string(ip_str)) || !ip.IsIPv6()) { + return std::nullopt; + } + wire_val.append(ip.ToPackedString()); + } + if (wire_val.empty()) { + return std::nullopt; + } + } else if (key_str == "dohpath") { + key = kSvcParamKeyDohPath; + if (value_str.empty()) { + return std::nullopt; + } + wire_val = std::string(value_str); + } else if (key_str == "ohttp") { + key = kSvcParamKeyOhttp; + if (!value_str.empty()) { + return std::nullopt; + } + } else if (absl::StartsWith(key_str, "key")) { + uint32_t key_num; + if (!absl::SimpleAtoi(key_str.substr(3), &key_num) || key_num > 65535) { + return std::nullopt; + } + key = static_cast<uint16_t>(key_num); + wire_val = std::string(value_str); + } else { + return std::nullopt; // Unknown key string + } + + if (!params.try_emplace(key, std::move(wire_val)).second) { + return std::nullopt; // Duplicate key not allowed per Section 2.2 of RFC + // 9460. + } + } + + size_t total_len = 0; + for (const auto& [key, val] : params) { + if (val.size() > 65535) { + return std::nullopt; + } + total_len += 4 + val.size(); // 2 bytes key + 2 bytes len + value bytes + } + std::string wire(total_len, '\0'); + QuicDataWriter writer(wire.size(), wire.data()); + for (const auto& [key, val] : params) { + if (!writer.WriteUInt16(key) || + !writer.WriteUInt16(static_cast<uint16_t>(val.size())) || + !writer.WriteStringPiece(val)) { + return std::nullopt; + } + } + return wire; +} + +std::optional<std::string> DecodeSvcParams(absl::string_view wire_format) { + if (wire_format.empty()) { + return ""; + } + QuicDataReader reader(wire_format); + std::vector<std::string> param_strings; + uint16_t last_key = 0; + bool first = true; + while (!reader.IsDoneReading()) { + uint16_t key = 0; + uint16_t len = 0; + if (!reader.ReadUInt16(&key) || !reader.ReadUInt16(&len)) { + return std::nullopt; + } + absl::string_view val; + if (!reader.ReadStringPiece(&val, len)) { + return std::nullopt; + } + if (!first && key <= last_key) { + return std::nullopt; // Keys must be strictly increasing per Section 2.2 + // of RFC 9460. + } + first = false; + last_key = key; + + if (key == kSvcParamKeyAlpn) { + std::vector<absl::string_view> alpns; + QuicDataReader alpn_reader(val); + while (!alpn_reader.IsDoneReading()) { + uint8_t alpn_len = 0; + absl::string_view alpn; + if (!alpn_reader.ReadUInt8(&alpn_len) || alpn_len == 0 || + !alpn_reader.ReadStringPiece(&alpn, alpn_len)) { + return std::nullopt; + } + alpns.push_back(alpn); + } + if (alpns.empty()) { + return std::nullopt; + } + param_strings.push_back(absl::StrCat("alpn=", absl::StrJoin(alpns, ","))); + } else if (key == kSvcParamKeyNoDefaultAlpn) { + if (!val.empty()) { + return std::nullopt; + } + param_strings.push_back("no-default-alpn"); + } else if (key == kSvcParamKeyPort) { + if (val.size() != 2) { + return std::nullopt; + } + QuicDataReader port_reader(val); + uint16_t port = 0; + if (!port_reader.ReadUInt16(&port)) { + return std::nullopt; + } + param_strings.push_back(absl::StrCat("port=", port)); + } else if (key == kSvcParamKeyIpv4Hint) { + if (val.empty() || val.size() % QuicIpAddress::kIPv4AddressSize != 0) { + return std::nullopt; + } + std::vector<std::string> ips; + QuicDataReader ip_reader(val); + while (!ip_reader.IsDoneReading()) { + absl::string_view ip_bytes; + if (!ip_reader.ReadStringPiece(&ip_bytes, + QuicIpAddress::kIPv4AddressSize)) { + return std::nullopt; + } + QuicIpAddress ip; + if (!ip.FromPackedString(ip_bytes.data(), + QuicIpAddress::kIPv4AddressSize)) { + return std::nullopt; + } + ips.push_back(ip.ToString()); + } + param_strings.push_back( + absl::StrCat("ipv4hint=", absl::StrJoin(ips, ","))); + } else if (key == kSvcParamKeyIpv6Hint) { + if (val.empty() || val.size() % QuicIpAddress::kIPv6AddressSize != 0) { + return std::nullopt; + } + std::vector<std::string> ips; + QuicDataReader ip_reader(val); + while (!ip_reader.IsDoneReading()) { + absl::string_view ip_bytes; + if (!ip_reader.ReadStringPiece(&ip_bytes, + QuicIpAddress::kIPv6AddressSize)) { + return std::nullopt; + } + QuicIpAddress ip; + if (!ip.FromPackedString(ip_bytes.data(), + QuicIpAddress::kIPv6AddressSize)) { + return std::nullopt; + } + ips.push_back(ip.ToString()); + } + param_strings.push_back( + absl::StrCat("ipv6hint=", absl::StrJoin(ips, ","))); + } else if (key == kSvcParamKeyDohPath) { + if (val.empty()) { + return std::nullopt; + } + param_strings.push_back(absl::StrCat("dohpath=", val)); + } else if (key == kSvcParamKeyOhttp) { + if (!val.empty()) { + return std::nullopt; + } + param_strings.push_back("ohttp"); + } else { + param_strings.push_back(absl::StrCat("key", key, "=", val)); + } + } + return absl::StrJoin(param_strings, " "); +} + +std::string DnsNameserver::ToString() const { + std::string rv = absl::StrCat("(priority:", service_priority); + for (const QuicIpAddress& ip : ipv4_addresses) { + absl::StrAppend(&rv, ",ipv4:", ip.ToString()); + } + for (const QuicIpAddress& ip : ipv6_addresses) { + absl::StrAppend(&rv, ",ipv6:", ip.ToString()); + } + if (!authentication_domain_name.domain_name.empty()) { + absl::StrAppend(&rv, + ",auth_domain:", authentication_domain_name.domain_name); + } + if (!service_parameters.empty()) { + std::optional<std::string> decoded = DecodeSvcParams(service_parameters); + absl::StrAppend(&rv, ",service_params:", + decoded.has_value() ? absl::CEscape(*decoded) + : absl::CEscape(service_parameters)); + } + absl::StrAppend(&rv, ")"); + return rv; +} + +std::string DnsConfiguration::ToString() const { + std::string rv = "{nameservers:["; + for (const DnsNameserver& ns : nameservers) { + absl::StrAppend(&rv, ns.ToString()); + } + absl::StrAppend(&rv, "],internal_domains:["); + bool first = true; + for (const DnsDomain& dom : internal_domains) { + if (!first) { + absl::StrAppend(&rv, ","); + } + first = false; + absl::StrAppend(&rv, dom.domain_name.empty() ? "/root" : dom.domain_name); + } + absl::StrAppend(&rv, "],search_domains:["); + first = true; + for (const DnsDomain& dom : search_domains) { + if (!first) { + absl::StrAppend(&rv, ","); + } + first = false; + absl::StrAppend(&rv, dom.domain_name); + } + absl::StrAppend(&rv, "]}"); + return rv; +} + +std::string DnsAssignCapsule::ToString() const { + std::string rv = "DNS_ASSIGN["; + for (const DnsConfiguration& config : dns_configurations) { + absl::StrAppend(&rv, config.ToString()); + } + absl::StrAppend(&rv, "]"); + return rv; +} + +std::string Pref64Prefix::ToString() const { + return absl::StrCat("(", prefix.ToString(), "/", + static_cast<int>(prefix_length), ")"); +} + +std::string Pref64Capsule::ToString() const { + std::string rv = "PREF64["; + for (const Pref64Prefix& prefix : nat64_prefixes) { + absl::StrAppend(&rv, prefix.ToString()); + } + absl::StrAppend(&rv, "]"); + return rv; +} + +void WriteDnsAssignCapsule(QuicSpdyStream* stream, + const DnsAssignCapsule& capsule, bool fin) { + QUICHE_CHECK(stream != nullptr); + std::string payload = SerializeDnsAssignCapsulePayload(capsule); + stream->WriteCapsule(quiche::Capsule::Unknown(kDnsAssignCapsuleType, payload), + fin); +} + +void WritePref64Capsule(QuicSpdyStream* stream, const Pref64Capsule& capsule, + bool fin) { + QUICHE_CHECK(stream != nullptr); + std::string payload = SerializePref64CapsulePayload(capsule); + stream->WriteCapsule(quiche::Capsule::Unknown(kPref64CapsuleType, payload), + fin); +} + } // namespace quic
diff --git a/quiche/quic/masque/masque_utils.h b/quiche/quic/masque/masque_utils.h index db1524d..bae1fb0 100644 --- a/quiche/quic/masque/masque_utils.h +++ b/quiche/quic/masque/masque_utils.h
@@ -7,19 +7,22 @@ #include <cstddef> #include <cstdint> +#include <optional> #include <ostream> #include <string> +#include <vector> #include "absl/strings/string_view.h" +#include "quiche/quic/core/http/quic_spdy_stream.h" #include "quiche/quic/core/quic_types.h" #include "quiche/quic/core/quic_versions.h" -#include "quiche/quic/platform/api/quic_export.h" #include "quiche/quic/platform/api/quic_ip_address.h" +#include "quiche/common/platform/api/quiche_export.h" namespace quic { // List of QUIC versions that support MASQUE. Currently restricted to IETF QUIC. -QUIC_NO_EXPORT ParsedQuicVersionVector MasqueSupportedVersions(); +QUICHE_EXPORT ParsedQuicVersionVector MasqueSupportedVersions(); inline constexpr QuicByteCount kMasqueIpPacketBufferSize = 1501; // Enough for a VLAN tag, but not Stacked VLANs. @@ -48,9 +51,9 @@ // <https://datatracker.ietf.org/doc/draft-ietf-masque-connect-udp-listen/> }; -QUIC_NO_EXPORT std::string MasqueModeToString(MasqueMode masque_mode); -QUIC_NO_EXPORT std::ostream& operator<<(std::ostream& os, - const MasqueMode& masque_mode); +QUICHE_EXPORT std::string MasqueModeToString(MasqueMode masque_mode); +QUICHE_EXPORT std::ostream& operator<<(std::ostream& os, + const MasqueMode& masque_mode); // Create a TUN interface, with the specified `client_address`. Requires root. int CreateTunInterface(const QuicIpAddress& client_address, bool server = true); @@ -67,16 +70,168 @@ "EXPORTER-HTTP-Concealed-Authentication"; // Returns the signature auth TLS key exporter context. -QUIC_NO_EXPORT std::string ComputeConcealedAuthContext( +QUICHE_EXPORT std::string ComputeConcealedAuthContext( uint16_t signature_scheme, absl::string_view key_id, absl::string_view public_key, absl::string_view scheme, absl::string_view host, uint16_t port, absl::string_view realm); // Returns the data covered by signature auth signatures, computed by // concatenating a fixed prefix from the specification and the signature input. -QUIC_NO_EXPORT std::string ConcealedAuthDataCoveredBySignature( +QUICHE_EXPORT std::string ConcealedAuthDataCoveredBySignature( absl::string_view signature_input); +// Capsule types for draft-ietf-masque-connect-ip-dns-06. +// Provisional values registered in Section 6 (Table 1). +inline constexpr uint64_t kDnsAssignCapsuleType = 0x1ACE79EC; +inline constexpr uint64_t kPref64CapsuleType = 0x274C0FBC; + +// Represents a Domain structure (Section 3.1), which carries a domain name +// in format of an IDNA A-label. +struct QUICHE_EXPORT DnsDomain { + // Fully Qualified Domain Name in DNS presentation format (ASCII/A-label). + std::string domain_name; + + bool operator==(const DnsDomain& other) const { + return domain_name == other.domain_name; + } +}; + +// Represents a Nameserver structure (Section 3.2), detailing how to reach a +// particular DNS resolver using unencrypted or encrypted transports. +struct QUICHE_EXPORT DnsNameserver { + // The priority of this nameserver compared to others + // (Section 2.4.1 of [SVCB]). + // Note: MUST NOT be set to 0 per Section 3.2 of the CONNECT-IP DNS draft. + uint16_t service_priority = 0; + // Sequence of IPv4 addresses that can be used to reach this nameserver. + std::vector<QuicIpAddress> ipv4_addresses; + // Sequence of IPv6 addresses that can be used to reach this nameserver. + std::vector<QuicIpAddress> ipv6_addresses; + // Domain representing the domain name of the nameserver. This may be empty + // if the nameserver only supports unencrypted DNS over port 53. + DnsDomain authentication_domain_name; + // Set of service parameters applying to this nameserver encoded using the + // wire format specified in Section 2.2 of [SVCB] (RFC 9460). + // Use EncodeSvcParams() / DecodeSvcParams() to convert between presentation + // format and wire format. + std::string service_parameters; + + bool operator==(const DnsNameserver& other) const { + return service_priority == other.service_priority && + ipv4_addresses == other.ipv4_addresses && + ipv6_addresses == other.ipv6_addresses && + authentication_domain_name == other.authentication_domain_name && + service_parameters == other.service_parameters; + } + std::string ToString() const; +}; + +// Encodes service parameters from DNS presentation format (e.g., +// "alpn=h2,h3 dohpath=/dns-query{?dns}") to RFC 9460 Section 2.2 SVCB wire +// format. Returns std::nullopt if parsing fails. +QUICHE_EXPORT std::optional<std::string> EncodeSvcParams( + absl::string_view presentation); + +// Decodes service parameters from RFC 9460 Section 2.2 SVCB wire format to +// DNS presentation format. Returns std::nullopt if parsing fails. +QUICHE_EXPORT std::optional<std::string> DecodeSvcParams( + absl::string_view wire_format); + +// Represents a DNS Configuration structure (Section 3.3) describing a set of +// nameservers responsible for resolving specific internal and search domains. +struct QUICHE_EXPORT DnsConfiguration { + // Series of Nameserver structures representing how to reach the resolvers. + std::vector<DnsNameserver> nameservers; + // Series of Domain structures representing internal domain names and their + // subdomains that the nameservers are authoritative for. An empty string + // indicates the DNS root (authoritative for all domain names). + std::vector<DnsDomain> internal_domains; + // Series of Domain structures representing DNS search domains. + std::vector<DnsDomain> search_domains; + + bool operator==(const DnsConfiguration& other) const { + return nameservers == other.nameservers && + internal_domains == other.internal_domains && + search_domains == other.search_domains; + } + std::string ToString() const; +}; + +// Represents a DNS_ASSIGN capsule (Section 3.4) allowing an endpoint to send +// one or more DNS configurations to its peer over a CONNECT-IP stream. +struct QUICHE_EXPORT DnsAssignCapsule { + // Multiple DNS configurations may be included if different DNS servers are + // responsible for separate internal domains. + std::vector<DnsConfiguration> dns_configurations; + + bool operator==(const DnsAssignCapsule& other) const { + return dns_configurations == other.dns_configurations; + } + std::string ToString() const; +}; + +// Represents an individual NAT64 prefix (Section 4.1) used for IPv6/IPv4 +// address synthesis in IPv6-only environments. +struct QUICHE_EXPORT Pref64Prefix { + // Length of the NAT64 prefix in bits. Valid values are 32, 40, 48, 56, 64, + // and 96. + uint8_t prefix_length = 0; + // The highest 96 bits (12 bytes) of the IPv6 prefix. Stored as an IPv6 + // address where the trailing 32 bits are zeroed. + QuicIpAddress prefix; + + bool operator==(const Pref64Prefix& other) const { + return prefix_length == other.prefix_length && prefix == other.prefix; + } + std::string ToString() const; +}; + +// Represents a PREF64 capsule (Section 4.1) conveying zero or more NAT64 +// prefixes. An empty PREF64 capsule informs that NAT64 prefixes are not +// available. +struct QUICHE_EXPORT Pref64Capsule { + std::vector<Pref64Prefix> nat64_prefixes; + + bool operator==(const Pref64Capsule& other) const { + return nat64_prefixes == other.nat64_prefixes; + } + std::string ToString() const; +}; + +// Parses the payload of a DNS_ASSIGN capsule into |capsule|. +// Returns true if parsing succeeds, false otherwise (e.g., malformed format, +// priority is 0, or trailing bytes remain). +QUICHE_EXPORT bool ParseDnsAssignCapsulePayload(absl::string_view payload, + DnsAssignCapsule* capsule); + +// Parses the payload of a PREF64 capsule into |capsule|. +// Returns true on success, false if malformed (e.g., payload length is not a +// multiple of 13 bytes or prefix_length is invalid). +QUICHE_EXPORT bool ParsePref64CapsulePayload(absl::string_view payload, + Pref64Capsule* capsule); + +// Serializes the payload of |capsule| into wire format bytes for a DNS_ASSIGN +// capsule. +QUICHE_EXPORT std::string SerializeDnsAssignCapsulePayload( + const DnsAssignCapsule& capsule); + +// Serializes the payload of |capsule| into wire format bytes for a PREF64 +// capsule. +QUICHE_EXPORT std::string SerializePref64CapsulePayload( + const Pref64Capsule& capsule); + +// Serializes and writes a DNS_ASSIGN capsule to |stream|. |fin| indicates +// whether to close the stream write side after sending. +QUICHE_EXPORT void WriteDnsAssignCapsule(QuicSpdyStream* stream, + const DnsAssignCapsule& capsule, + bool fin = false); + +// Serializes and writes a PREF64 capsule to |stream|. |fin| indicates whether +// to close the stream write side after sending. +QUICHE_EXPORT void WritePref64Capsule(QuicSpdyStream* stream, + const Pref64Capsule& capsule, + bool fin = false); + } // namespace quic #endif // QUICHE_QUIC_MASQUE_MASQUE_UTILS_H_
diff --git a/quiche/quic/masque/masque_utils_test.cc b/quiche/quic/masque/masque_utils_test.cc new file mode 100644 index 0000000..351a0f9 --- /dev/null +++ b/quiche/quic/masque/masque_utils_test.cc
@@ -0,0 +1,223 @@ +// Copyright 2026 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 "quiche/quic/masque/masque_utils.h" + +#include <optional> +#include <string> +#include <utility> +#include <vector> + +#include "absl/strings/string_view.h" +#include "quiche/quic/platform/api/quic_expect_bug.h" +#include "quiche/quic/platform/api/quic_ip_address.h" +#include "quiche/quic/platform/api/quic_test.h" + +namespace quic { +namespace test { +namespace { + +using ::testing::_; +using ::testing::Eq; +using ::testing::IsEmpty; +using ::testing::Not; +using ::testing::Optional; + +TEST(MasqueUtilsTest, DnsAssignCapsuleRoundTrip) { + DnsAssignCapsule original; + + DnsConfiguration config1; + DnsNameserver ns1; + ns1.service_priority = 10; + QuicIpAddress ipv4_1; + ASSERT_TRUE(ipv4_1.FromString("192.0.2.33")); + ns1.ipv4_addresses.push_back(ipv4_1); + QuicIpAddress ipv6_1; + ASSERT_TRUE(ipv6_1.FromString("2001:db8::1")); + ns1.ipv6_addresses.push_back(ipv6_1); + ns1.authentication_domain_name.domain_name = "dns.corp.example"; + std::optional<std::string> encoded_params = + EncodeSvcParams("alpn=h2,h3\ndohpath=/dns-query{?dns}"); + ASSERT_THAT(encoded_params, Optional(_)); + ns1.service_parameters = *std::move(encoded_params); + config1.nameservers.push_back(ns1); + + DnsDomain int_dom1; + int_dom1.domain_name = "internal.corp.example"; + config1.internal_domains.push_back(int_dom1); + DnsDomain search_dom1; + search_dom1.domain_name = "corp.example"; + config1.search_domains.push_back(search_dom1); + + original.dns_configurations.push_back(config1); + + DnsConfiguration config2; + config2.internal_domains.push_back(DnsDomain{""}); // Root domain + original.dns_configurations.push_back(config2); + + std::string serialized = SerializeDnsAssignCapsulePayload(original); + ASSERT_FALSE(serialized.empty()); + + DnsAssignCapsule parsed; + ASSERT_TRUE(ParseDnsAssignCapsulePayload(serialized, &parsed)); + EXPECT_EQ(parsed, original); + EXPECT_EQ( + parsed.ToString(), + "DNS_ASSIGN[{nameservers:[(priority:10,ipv4:192.0.2.33,ipv6:2001:db8::1," + "auth_domain:dns.corp.example,service_params:alpn=h2,h3 dohpath=/dns-" + "query{?dns})],internal_domains:[internal.corp.example],search_domains:[" + "corp.example]}{nameservers:[],internal_domains:[/root],search_domains:[" + "]}]"); +} + +TEST(MasqueUtilsTest, DnsAssignCapsuleZeroPriorityIsMalformed) { + DnsAssignCapsule original; + DnsConfiguration config; + DnsNameserver ns; + ns.service_priority = 0; // Priority 0 is forbidden by spec. + QuicIpAddress ipv4; + ASSERT_TRUE(ipv4.FromString("192.0.2.1")); + ns.ipv4_addresses.push_back(ipv4); + config.nameservers.push_back(ns); + original.dns_configurations.push_back(config); + + std::string serialized = SerializeDnsAssignCapsulePayload(original); + ASSERT_FALSE(serialized.empty()); + + DnsAssignCapsule parsed; + EXPECT_FALSE(ParseDnsAssignCapsulePayload(serialized, &parsed)); +} + +TEST(MasqueUtilsTest, DnsAssignCapsuleTrailingJunkIsMalformed) { + DnsAssignCapsule original; + DnsConfiguration config; + original.dns_configurations.push_back(config); + + std::string serialized = SerializeDnsAssignCapsulePayload(original); + serialized += "junk"; + + DnsAssignCapsule parsed; + EXPECT_FALSE(ParseDnsAssignCapsulePayload(serialized, &parsed)); +} + +TEST(MasqueUtilsTest, Pref64CapsuleRoundTrip) { + Pref64Capsule original; + + Pref64Prefix prefix1; + prefix1.prefix_length = 96; + ASSERT_TRUE(prefix1.prefix.FromString("64:ff9b::")); + original.nat64_prefixes.push_back(prefix1); + + Pref64Prefix prefix2; + prefix2.prefix_length = 64; + ASSERT_TRUE(prefix2.prefix.FromString("2001:db8:64::")); + original.nat64_prefixes.push_back(prefix2); + + std::string serialized = SerializePref64CapsulePayload(original); + ASSERT_EQ(serialized.size(), 26u); // 2 prefixes * 13 bytes each. + + Pref64Capsule parsed; + ASSERT_TRUE(ParsePref64CapsulePayload(serialized, &parsed)); + EXPECT_EQ(parsed, original); + EXPECT_EQ(parsed.ToString(), "PREF64[(64:ff9b::/96)(2001:db8:64::/64)]"); +} + +TEST(MasqueUtilsTest, Pref64CapsuleInvalidLengthIsMalformed) { + Pref64Capsule original; + Pref64Prefix prefix; + prefix.prefix_length = 96; + ASSERT_TRUE(prefix.prefix.FromString("64:ff9b::")); + original.nat64_prefixes.push_back(prefix); + + std::string serialized = SerializePref64CapsulePayload(original); + // Remove one byte so length is not a multiple of 13. + serialized.pop_back(); + + Pref64Capsule parsed; + EXPECT_FALSE(ParsePref64CapsulePayload(serialized, &parsed)); +} + +TEST(MasqueUtilsTest, Pref64CapsuleInvalidPrefixLengthIsMalformed) { + Pref64Capsule original; + Pref64Prefix prefix; + prefix.prefix_length = 60; // 60 is not in {32, 40, 48, 56, 64, 96}. + ASSERT_TRUE(prefix.prefix.FromString("64:ff9b::")); + original.nat64_prefixes.push_back(prefix); + + std::string serialized = SerializePref64CapsulePayload(original); + ASSERT_EQ(serialized.size(), 13u); + + Pref64Capsule parsed; + EXPECT_FALSE(ParsePref64CapsulePayload(serialized, &parsed)); +} + +TEST(MasqueUtilsTest, SvcParamsRoundTrip) { + EXPECT_THAT(EncodeSvcParams(""), Optional(Eq(""))); + EXPECT_THAT(DecodeSvcParams(""), Optional(Eq(""))); + + std::optional<std::string> encoded1 = + EncodeSvcParams("alpn=h2,h3 dohpath=/dns-query{?dns}"); + ASSERT_THAT(encoded1, Optional(Not(IsEmpty()))); + EXPECT_THAT(DecodeSvcParams(*encoded1), + Optional(Eq("alpn=h2,h3 dohpath=/dns-query{?dns}"))); + + // Test out-of-order presentation strings are sorted by SvcParamKey in wire + // format. + std::optional<std::string> encoded2 = + EncodeSvcParams("dohpath=/dns-query{?dns} alpn=h2,h3"); + ASSERT_THAT(encoded2, Optional(Not(IsEmpty()))); + EXPECT_EQ(*encoded1, *encoded2); + + // Test port, no-default-alpn, and ip hints. + std::optional<std::string> encoded3 = EncodeSvcParams( + "port=8080 no-default-alpn ipv4hint=192.0.2.1,10.0.0.1 " + "ipv6hint=2001:db8::1"); + ASSERT_THAT(encoded3, Optional(Not(IsEmpty()))); + EXPECT_THAT( + DecodeSvcParams(*encoded3), + Optional(Eq("no-default-alpn port=8080 ipv4hint=192.0.2.1,10.0.0.1 " + "ipv6hint=2001:db8::1"))); + + // Test malformed presentation format. + EXPECT_EQ(EncodeSvcParams("unknown_key_format"), std::nullopt); + EXPECT_EQ(EncodeSvcParams("port=not_an_int"), std::nullopt); + EXPECT_EQ(EncodeSvcParams("ipv4hint=not_an_ip"), std::nullopt); + EXPECT_EQ(EncodeSvcParams("alpn=h2 alpn=h3"), std::nullopt); // Duplicate key + + // Test malformed wire format (out-of-order keys). + char bad_wire_bytes[] = {0x00, 0x07, 0x00, 0x01, 'x', 0x00, + 0x01, 0x00, 0x02, 'h', '2'}; + EXPECT_EQ( + DecodeSvcParams(std::string(bad_wire_bytes, sizeof(bad_wire_bytes))), + std::nullopt); +} + +TEST(MasqueUtilsTest, DnsAssignCapsuleMalformedSvcParams) { + DnsAssignCapsule original; + DnsConfiguration config; + DnsNameserver ns; + ns.service_priority = 10; + QuicIpAddress ipv4; + ASSERT_TRUE(ipv4.FromString("192.0.2.1")); + ns.ipv4_addresses.push_back(ipv4); + char bad_wire_bytes[] = {0x00, 0x07, 0x00, 0x01, 'x', 0x00, + 0x01, 0x00, 0x02, 'h', '2'}; + ns.service_parameters = std::string(bad_wire_bytes, sizeof(bad_wire_bytes)); + config.nameservers.push_back(ns); + original.dns_configurations.push_back(config); + + std::string serialized; + EXPECT_QUIC_BUG(serialized = SerializeDnsAssignCapsulePayload(original), + "Invalid service parameters wire format"); + EXPECT_TRUE(serialized.empty()); +} + +TEST(MasqueUtilsTest, NullPointerParsing) { + EXPECT_FALSE(ParseDnsAssignCapsulePayload("any", nullptr)); + EXPECT_FALSE(ParsePref64CapsulePayload("any", nullptr)); +} + +} // namespace +} // namespace test +} // namespace quic