| // 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 |