diff --git a/api/envoy/data/dns/v3/dns_table.proto b/api/envoy/data/dns/v3/dns_table.proto index 1816a304405e..73993187f69d 100644 --- a/api/envoy/data/dns/v3/dns_table.proto +++ b/api/envoy/data/dns/v3/dns_table.proto @@ -128,7 +128,15 @@ message DnsTable { option (udpa.annotations.versioning).previous_message_type = "envoy.data.dns.v2alpha.DnsTable.DnsVirtualDomain"; - // A domain name for which Envoy will respond to query requests + // A domain name for which Envoy will respond to query requests. + // Wildcard records are supported on the first label only, e.g. ``*.example.com`` or ``*.subdomain.example.com``. + // Names such as ``*example.com``, ``subdomain.*.example.com``, ``*subdomain.example.com``, etc + // are not valid wildcard names and asterisk will be interpreted as a literal ``*`` character. + // Wildcard records match subdomains on any levels, e.g. ``*.example.com`` will match + // ``foo.example.com``, ``bar.foo.example.com``, ``baz.bar.foo.example.com``, etc. In case there are multiple + // wildcard records, the longest wildcard match will be used, e.g. if there are wildcard records for + // ``*.example.com`` and ``*.foo.example.com`` and the query is for ``bar.foo.example.com``, the latter will be used. + // Specific records will always take precedence over wildcard records. string name = 1 [(validate.rules).string = {min_len: 1 well_known_regex: HTTP_HEADER_NAME}]; // The configuration containing the method to determine the address of this endpoint diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 8d48e712045a..3488a6e2c460 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -483,6 +483,11 @@ new_features: change: | Added :ref:`disable_id_token_set_cookie ` to disable setting the ID Token cookie. +- area: dns_filter + change: | + Added support for wildcard resolution in :ref:`inline_dns_table + ` + when DNS filter is working in server mode. deprecated: - area: tracing diff --git a/source/extensions/filters/udp/dns_filter/dns_filter.cc b/source/extensions/filters/udp/dns_filter/dns_filter.cc index 9b36ddb47c21..e971791fe4d1 100644 --- a/source/extensions/filters/udp/dns_filter/dns_filter.cc +++ b/source/extensions/filters/udp/dns_filter/dns_filter.cc @@ -39,9 +39,11 @@ DnsFilterEnvoyConfig::DnsFilterEnvoyConfig( for (const auto& virtual_domain : dns_table.virtual_domains()) { AddressConstPtrVec addrs{}; - const absl::string_view domain_name = virtual_domain.name(); - const absl::string_view suffix = Utils::getDomainSuffix(domain_name); - ENVOY_LOG(trace, "Loading configuration for domain: {}. Suffix: {}", domain_name, suffix); + const absl::string_view virtual_domain_name = + Utils::getVirtualDomainName(virtual_domain.name()); + const absl::string_view suffix = Utils::getDomainSuffix(virtual_domain_name); + ENVOY_LOG(trace, "Loading configuration for domain: {}. Suffix: {}", virtual_domain_name, + suffix); if (virtual_domain.endpoint().has_address_list()) { const auto& address_list = virtual_domain.endpoint().address_list().address(); @@ -69,7 +71,7 @@ DnsFilterEnvoyConfig::DnsFilterEnvoyConfig( if (virtual_domains != nullptr) { // The suffix already has a node in the trie - auto existing_endpoint_config = virtual_domains->find(domain_name); + auto existing_endpoint_config = virtual_domains->find(virtual_domain_name); if (existing_endpoint_config != virtual_domains->end()) { // Update the existing endpoint config with the new addresses @@ -79,11 +81,11 @@ DnsFilterEnvoyConfig::DnsFilterEnvoyConfig( } else { // Add a new endpoint config for the new domain endpoint_config.address_list = absl::make_optional(std::move(addrs)); - virtual_domains->emplace(std::string(domain_name), std::move(endpoint_config)); + virtual_domains->emplace(std::string(virtual_domain_name), std::move(endpoint_config)); } } else { endpoint_config.address_list = absl::make_optional(std::move(addrs)); - addEndpointToSuffix(suffix, domain_name, endpoint_config); + addEndpointToSuffix(suffix, virtual_domain_name, endpoint_config); } } @@ -147,12 +149,12 @@ DnsFilterEnvoyConfig::DnsFilterEnvoyConfig( // See if there's a suffix already configured auto virtual_domains = dns_lookup_trie_.find(suffix); if (virtual_domains == nullptr) { - addEndpointToSuffix(suffix, domain_name, endpoint_config); + addEndpointToSuffix(suffix, virtual_domain_name, endpoint_config); } else { // A domain can be redirected to one cluster. If it appears multiple times, the first // entry is the only one used - if (virtual_domains->find(domain_name) == virtual_domains->end()) { - virtual_domains->emplace(domain_name, std::move(endpoint_config)); + if (virtual_domains->find(virtual_domain_name) == virtual_domains->end()) { + virtual_domains->emplace(virtual_domain_name, std::move(endpoint_config)); } } } @@ -160,7 +162,7 @@ DnsFilterEnvoyConfig::DnsFilterEnvoyConfig( std::chrono::seconds ttl = virtual_domain.has_answer_ttl() ? std::chrono::seconds(virtual_domain.answer_ttl().seconds()) : DEFAULT_RESOLVER_TTL; - domain_ttl_.emplace(virtual_domain.name(), ttl); + domain_ttl_.emplace(virtual_domain_name, ttl); } forward_queries_ = config.has_client_config(); @@ -393,12 +395,20 @@ const DnsEndpointConfig* DnsFilter::getEndpointConfigForDomain(const absl::strin return nullptr; } - const auto iter = virtual_domains->find(domain); - if (iter == virtual_domains->end()) { - ENVOY_LOG(debug, "No endpoint configuration exists for [{}]", domain); - return nullptr; + // Try to find exact match at first and then look for possible wildcard match + // moving to the next label on each iteration. + size_t pos = 0; + while (pos != domain.npos) { + const auto iter = virtual_domains->find(domain.substr(pos)); + if (iter != virtual_domains->end()) { + return &(iter->second); + } + + pos = domain.find('.', pos + 1); } - return &(iter->second); + + ENVOY_LOG(debug, "No endpoint configuration exists for [{}]", domain); + return nullptr; } const DnsSrvRecord* DnsFilter::getServiceConfigForDomain(const absl::string_view domain) { diff --git a/source/extensions/filters/udp/dns_filter/dns_filter_utils.cc b/source/extensions/filters/udp/dns_filter/dns_filter_utils.cc index c37f6d77563d..ea013f0eaa38 100644 --- a/source/extensions/filters/udp/dns_filter/dns_filter_utils.cc +++ b/source/extensions/filters/udp/dns_filter/dns_filter_utils.cc @@ -89,6 +89,19 @@ absl::string_view getDomainSuffix(const absl::string_view name) { return name.substr(pos + 1); } +absl::string_view getVirtualDomainName(const absl::string_view domain_name) { + // We can use names started with '.' as wildcard records in virtual domain name config + // since these are not valid domain names and in this way we optimize for future search against + // them. We expect only names like *.foo.com as a valid wildcard records because any other + // wildcard usages are not considered as valid ones, i.e. **.foo.com, *foo.bar.com, foo*.bar.com + // are all invalid. + if (domain_name.starts_with("*.")) { + return domain_name.substr(1); + } + + return domain_name; +} + } // namespace Utils } // namespace DnsFilter } // namespace UdpFilters diff --git a/source/extensions/filters/udp/dns_filter/dns_filter_utils.h b/source/extensions/filters/udp/dns_filter/dns_filter_utils.h index 34b2091f2105..cc3f6447ba53 100644 --- a/source/extensions/filters/udp/dns_filter/dns_filter_utils.h +++ b/source/extensions/filters/udp/dns_filter/dns_filter_utils.h @@ -46,6 +46,13 @@ getAddressRecordType(const Network::Address::InstanceConstSharedPtr& ipaddr); */ absl::string_view getDomainSuffix(const absl::string_view name); +/** + * @brief For given domain name return virtual domain name that will be used for internal + * resolution. Valid wildcard names are sanitized and converted to format to store in + * virtual domain config. + */ +absl::string_view getVirtualDomainName(const absl::string_view domain_name); + } // namespace Utils } // namespace DnsFilter } // namespace UdpFilters diff --git a/test/extensions/filters/udp/dns_filter/dns_filter_integration_test.cc b/test/extensions/filters/udp/dns_filter/dns_filter_integration_test.cc index 5ac40051fdb4..9ed168b4b372 100644 --- a/test/extensions/filters/udp/dns_filter/dns_filter_integration_test.cc +++ b/test/extensions/filters/udp/dns_filter/dns_filter_integration_test.cc @@ -134,6 +134,13 @@ name: listener_0 - cluster_name: "cluster_0" weight: 20 priority: 10 + - name: "*.foo1.com" + endpoint: + address_list: + address: + - 10.10.0.1 + - 10.10.0.2 + - 10.10.0.3 )EOF", addr->ip()->addressAsString(), addr->ip()->addressAsString(), addr->ip()->port()); @@ -401,6 +408,24 @@ TEST_P(DnsFilterIntegrationTest, ClusterEndpointWithoutPortServiceRecordLookupTe EXPECT_EQ(endpoints, ports.size()); } + +TEST_P(DnsFilterIntegrationTest, WildcardLookupTest) { + setup(0); + const uint32_t port = lookupPort("listener_0"); + const auto listener_address = *Network::Utility::resolveUrl( + fmt::format("tcp://{}:{}", Network::Test::getLoopbackAddressUrlString(version_), port)); + + Network::UdpRecvData response; + std::string query = + Utils::buildQueryForDomain("wild.foo1.com", DNS_RECORD_TYPE_A, DNS_RECORD_CLASS_IN); + requestResponseWithListenerAddress(*listener_address, query, response); + + response_ctx_ = ResponseValidator::createResponseContext(response, counters_); + EXPECT_TRUE(response_ctx_->parse_status_); + + EXPECT_EQ(3, response_ctx_->answers_.size()); + EXPECT_EQ(DNS_RESPONSE_CODE_NO_ERROR, response_ctx_->getQueryResponseCode()); +} } // namespace } // namespace DnsFilter } // namespace UdpFilters diff --git a/test/extensions/filters/udp/dns_filter/dns_filter_test.cc b/test/extensions/filters/udp/dns_filter/dns_filter_test.cc index cd2adfa09bf4..d03cc3e177f0 100644 --- a/test/extensions/filters/udp/dns_filter/dns_filter_test.cc +++ b/test/extensions/filters/udp/dns_filter/dns_filter_test.cc @@ -2433,6 +2433,133 @@ stat_prefix: "my_prefix" EXPECT_EQ(1, config_->stats().known_domain_queries_.value()); } +TEST_F(DnsFilterTest, WildcardName) { + InSequence s; + + const std::string wildcard_virtual_domain = R"EOF( +stat_prefix: "my_prefix" +server_config: + inline_dns_table: + external_retry_count: 0 + virtual_domains: + - name: "*.foobaz.com" + endpoint: + address_list: + address: + - "10.0.0.1" +)EOF"; + setup(wildcard_virtual_domain); + + const std::list expected_address{"10.0.0.1"}; + const std::string domain("www.foobaz.com"); + + const std::string query = + Utils::buildQueryForDomain(domain, DNS_RECORD_TYPE_A, DNS_RECORD_CLASS_IN); + ASSERT_FALSE(query.empty()); + sendQueryFromClient("10.0.0.1:1000", query); + + response_ctx_ = ResponseValidator::createResponseContext(udp_response_, counters_); + EXPECT_TRUE(response_ctx_->parse_status_); + EXPECT_EQ(DNS_RESPONSE_CODE_NO_ERROR, response_ctx_->getQueryResponseCode()); + + for (const auto& answer : response_ctx_->answers_) { + EXPECT_EQ(answer.first, domain); + Utils::verifyAddress(expected_address, answer.second); + } + + // Validate stats + EXPECT_EQ(1, config_->stats().downstream_rx_queries_.value()); + EXPECT_EQ(1, config_->stats().known_domain_queries_.value()); +} + +TEST_F(DnsFilterTest, WildcardSubdomainPrevails) { + InSequence s; + + const std::string wildcard_with_subdomain_virtual_domain = R"EOF( +stat_prefix: "my_prefix" +server_config: + inline_dns_table: + external_retry_count: 0 + virtual_domains: + - name: "*.foo1.com" + endpoint: + address_list: + address: + - "10.0.0.1" + - name: "*.foo2.foo1.com" + endpoint: + address_list: + address: + - "10.0.0.2" +)EOF"; + setup(wildcard_with_subdomain_virtual_domain); + + const std::list expected_address{"10.0.0.2"}; + const std::string domain("www.foo2.foo1.com"); + + const std::string query = + Utils::buildQueryForDomain(domain, DNS_RECORD_TYPE_A, DNS_RECORD_CLASS_IN); + ASSERT_FALSE(query.empty()); + sendQueryFromClient("10.0.0.1:1000", query); + + response_ctx_ = ResponseValidator::createResponseContext(udp_response_, counters_); + EXPECT_TRUE(response_ctx_->parse_status_); + EXPECT_EQ(DNS_RESPONSE_CODE_NO_ERROR, response_ctx_->getQueryResponseCode()); + + for (const auto& answer : response_ctx_->answers_) { + EXPECT_EQ(answer.first, domain); + Utils::verifyAddress(expected_address, answer.second); + } + + // Validate stats + EXPECT_EQ(1, config_->stats().downstream_rx_queries_.value()); + EXPECT_EQ(1, config_->stats().known_domain_queries_.value()); +} + +TEST_F(DnsFilterTest, WildcardExactNamePrevails) { + InSequence s; + + const std::string wildcard_and_exact_name_virtual_domain = R"EOF( +stat_prefix: "my_prefix" +server_config: + inline_dns_table: + external_retry_count: 0 + virtual_domains: + - name: "*.foo1.com" + endpoint: + address_list: + address: + - "10.0.0.1" + - name: "bar.foo1.com" + endpoint: + address_list: + address: + - "10.0.0.2" +)EOF"; + setup(wildcard_and_exact_name_virtual_domain); + + const std::list expected_address{"10.0.0.2"}; + const std::string domain("bar.foo1.com"); + + const std::string query = + Utils::buildQueryForDomain(domain, DNS_RECORD_TYPE_A, DNS_RECORD_CLASS_IN); + ASSERT_FALSE(query.empty()); + sendQueryFromClient("10.0.0.1:1000", query); + + response_ctx_ = ResponseValidator::createResponseContext(udp_response_, counters_); + EXPECT_TRUE(response_ctx_->parse_status_); + EXPECT_EQ(DNS_RESPONSE_CODE_NO_ERROR, response_ctx_->getQueryResponseCode()); + + for (const auto& answer : response_ctx_->answers_) { + EXPECT_EQ(answer.first, domain); + Utils::verifyAddress(expected_address, answer.second); + } + + // Validate stats + EXPECT_EQ(1, config_->stats().downstream_rx_queries_.value()); + EXPECT_EQ(1, config_->stats().known_domain_queries_.value()); +} + } // namespace } // namespace DnsFilter } // namespace UdpFilters diff --git a/test/extensions/filters/udp/dns_filter/dns_filter_utils_test.cc b/test/extensions/filters/udp/dns_filter/dns_filter_utils_test.cc index 632b01df8567..868b15c4607d 100644 --- a/test/extensions/filters/udp/dns_filter/dns_filter_utils_test.cc +++ b/test/extensions/filters/udp/dns_filter/dns_filter_utils_test.cc @@ -108,6 +108,9 @@ TEST_F(DnsFilterUtilsTest, GetDomainSuffixTest) { {"_ldap._tcp.Default-First-Site-Name._sites.dc._msdcs.utelsystems.local", "utelsystems.local"}, {"primary.voip.subzero.com", "subzero.com"}, + {"*.voip.subzero.com", "subzero.com"}, + {"*.subzero.com", "subzero.com"}, + {".subzero.com", "subzero.com"}, {"subzero.com", "subzero.com"}, {"subzero", "subzero"}, {".com", "com"}, @@ -120,6 +123,29 @@ TEST_F(DnsFilterUtilsTest, GetDomainSuffixTest) { } } +TEST_F(DnsFilterUtilsTest, GetVirtualDomainName) { + struct DomainSuffixTestData { + const std::string domain; + const std::string expected_name; + } suffix_data[] = { + {"_ldap._tcp.Default-First-Site-Name._sites.dc._msdcs.utelsystems.local", + "_ldap._tcp.Default-First-Site-Name._sites.dc._msdcs.utelsystems.local"}, + {"primary.voip.subzero.com", "primary.voip.subzero.com"}, + {"*.subzero.com", ".subzero.com"}, + {"*www.subzero.com", "*www.subzero.com"}, + {"subzero", "subzero"}, + {".com", ".com"}, + {"*.", "."}, + {".", "."}, + {"", ""}, + }; + + for (auto& ptr : suffix_data) { + const absl::string_view result = Utils::getVirtualDomainName(ptr.domain); + EXPECT_EQ(ptr.expected_name, result); + } +} + } // namespace } // namespace Utils } // namespace DnsFilter