diff --git a/CODEOWNERS b/CODEOWNERS index 6f341bef43b95..caa706307f859 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -197,6 +197,7 @@ extensions/filters/http/oauth2 @derekargueta @snowp # DNS resolution /*/extensions/network/dns_resolver/cares @yanavlasov @mattklein123 /*/extensions/network/dns_resolver/apple @yanavlasov @mattklein123 +/*/extensions/network/dns_resolver/getaddrinfo @alyssawilk @mattklein123 # compression code /*/extensions/filters/http/decompressor @kbaichoo @mattklein123 /*/extensions/filters/http/compressor @kbaichoo @mattklein123 diff --git a/api/BUILD b/api/BUILD index b9370c57a3493..f63b87a6610ad 100644 --- a/api/BUILD +++ b/api/BUILD @@ -238,6 +238,7 @@ proto_library( "//envoy/extensions/matching/input_matchers/ip/v3:pkg", "//envoy/extensions/network/dns_resolver/apple/v3:pkg", "//envoy/extensions/network/dns_resolver/cares/v3:pkg", + "//envoy/extensions/network/dns_resolver/getaddrinfo/v3:pkg", "//envoy/extensions/network/socket_interface/v3:pkg", "//envoy/extensions/quic/crypto_stream/v3:pkg", "//envoy/extensions/quic/proof_source/v3:pkg", diff --git a/api/envoy/extensions/network/dns_resolver/getaddrinfo/v3/BUILD b/api/envoy/extensions/network/dns_resolver/getaddrinfo/v3/BUILD new file mode 100644 index 0000000000000..ee92fb652582e --- /dev/null +++ b/api/envoy/extensions/network/dns_resolver/getaddrinfo/v3/BUILD @@ -0,0 +1,9 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = ["@com_github_cncf_udpa//udpa/annotations:pkg"], +) diff --git a/api/envoy/extensions/network/dns_resolver/getaddrinfo/v3/getaddrinfo_dns_resolver.proto b/api/envoy/extensions/network/dns_resolver/getaddrinfo/v3/getaddrinfo_dns_resolver.proto new file mode 100644 index 0000000000000..0ffde4bee97b6 --- /dev/null +++ b/api/envoy/extensions/network/dns_resolver/getaddrinfo/v3/getaddrinfo_dns_resolver.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +package envoy.extensions.network.dns_resolver.getaddrinfo.v3; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.network.dns_resolver.getaddrinfo.v3"; +option java_outer_classname = "GetaddrinfoDnsResolverProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/network/dns_resolver/getaddrinfo/v3;getaddrinfov3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: getaddrinfo DNS resolver] +// [#extension: envoy.network.dns_resolver.getaddrinfo] + +// Configuration for getaddrinfo DNS resolver. This resolver will use the system's getaddrinfo() +// function to resolve hosts. +// +// .. attention:: +// +// This resolver uses a single background thread to do resolutions. As such, it is not currently +// advised for use in situations requiring a high resolution rate. A thread pool can be added +// in the future if needed. +// +// .. attention:: +// +// Resolutions currently use a hard coded TTL of 60s because the getaddrinfo() API does not +// provide the actual TTL. Configuration for this can be added in the future if needed. +message GetAddrInfoDnsResolverConfig { +} diff --git a/api/versioning/BUILD b/api/versioning/BUILD index a873cfc24c737..e394bb9198234 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -180,6 +180,7 @@ proto_library( "//envoy/extensions/matching/input_matchers/ip/v3:pkg", "//envoy/extensions/network/dns_resolver/apple/v3:pkg", "//envoy/extensions/network/dns_resolver/cares/v3:pkg", + "//envoy/extensions/network/dns_resolver/getaddrinfo/v3:pkg", "//envoy/extensions/network/socket_interface/v3:pkg", "//envoy/extensions/quic/crypto_stream/v3:pkg", "//envoy/extensions/quic/proof_source/v3:pkg", diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 70002bc7054e8..bf78925dff503 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -232,6 +232,8 @@ new_features: added :ref:`include_unroutable_families` to the Apple DNS resolver. change: | added support for multiple addresses. This is most valuable when used in conjunction with :ref:`ALL ` enabling full happy eyeballs support for Envoy (see detailed documentation :ref:`here ` but will also result in trying multiple addresses for resolvers doing only IPv4 or IPv6. This behavioral change can be temporarily disabled by setting runtime guard ``envoy.restart_features.remove_runtime_singleton`` to false. + change: | + added :ref:`GetAddrInfoDnsResolverConfig `, a new DNS resolver that uses the system's getaddrinfo() function to resolve DNS. This was primarily added for use on Android but can also be used in other situations in which the system resolver is desired. - area: dubbo_proxy change: | added :ref:`dynamic routes discovery ` support to the dubbo proxy. diff --git a/docs/root/intro/arch_overview/upstream/dns_resolution.rst b/docs/root/intro/arch_overview/upstream/dns_resolution.rst index b421b8439eae7..338b8cdf75237 100644 --- a/docs/root/intro/arch_overview/upstream/dns_resolution.rst +++ b/docs/root/intro/arch_overview/upstream/dns_resolution.rst @@ -13,12 +13,14 @@ Envoy uses `c-ares `_ as a third party DNS res On Apple OSes Envoy additionally offers resolution using Apple specific APIs via the ``envoy.restart_features.use_apple_api_for_dns_lookups`` runtime feature. -Envoy provides DNS resolution through extensions, and contains 2 built-in extensions: +Envoy provides DNS resolution through extensions, and contains 3 built-in extensions: 1) c-ares: :ref:`CaresDnsResolverConfig` 2) Apple (iOS/macOS only): :ref:`AppleDnsResolverConfig` +3) getaddrinfo: :ref:`GetAddrInfoDnsResolverConfig ` + For an example of a built-in DNS typed configuration see the :ref:`HTTP filter configuration documentation `. The Apple-based DNS Resolver emits the following stats rooted in the ``dns.apple`` stats tree: diff --git a/envoy/api/os_sys_calls.h b/envoy/api/os_sys_calls.h index 9642b84ab8f0f..28a591307443b 100644 --- a/envoy/api/os_sys_calls.h +++ b/envoy/api/os_sys_calls.h @@ -282,6 +282,17 @@ class OsSysCalls { alternate_getifaddrs_ = alternate_getifaddrs; } + /** + * @see man getaddrinfo + */ + virtual SysCallIntResult getaddrinfo(const char* node, const char* service, const addrinfo* hints, + addrinfo** res) PURE; + + /** + * @see man freeaddrinfo + */ + virtual void freeaddrinfo(addrinfo* res) PURE; + protected: absl::optional alternate_getifaddrs_{}; }; diff --git a/source/common/api/posix/os_sys_calls_impl.cc b/source/common/api/posix/os_sys_calls_impl.cc index 6b3c032b1d35a..3afac06a4c685 100644 --- a/source/common/api/posix/os_sys_calls_impl.cc +++ b/source/common/api/posix/os_sys_calls_impl.cc @@ -404,5 +404,13 @@ SysCallIntResult OsSysCallsImpl::getifaddrs([[maybe_unused]] InterfaceAddressVec #endif } +SysCallIntResult OsSysCallsImpl::getaddrinfo(const char* node, const char* service, + const addrinfo* hints, addrinfo** res) { + const int rc = ::getaddrinfo(node, service, hints, res); + return {rc, errno}; +} + +void OsSysCallsImpl::freeaddrinfo(addrinfo* res) { ::freeaddrinfo(res); } + } // namespace Api } // namespace Envoy diff --git a/source/common/api/posix/os_sys_calls_impl.h b/source/common/api/posix/os_sys_calls_impl.h index 3ec82659163e6..61a79c8aab063 100644 --- a/source/common/api/posix/os_sys_calls_impl.h +++ b/source/common/api/posix/os_sys_calls_impl.h @@ -62,6 +62,9 @@ class OsSysCallsImpl : public OsSysCalls { SysCallBoolResult socketTcpInfo(os_fd_t sockfd, EnvoyTcpInfo* tcp_info) override; bool supportsGetifaddrs() const override; SysCallIntResult getifaddrs(InterfaceAddressVector& interfaces) override; + SysCallIntResult getaddrinfo(const char* node, const char* service, const addrinfo* hints, + addrinfo** res) override; + void freeaddrinfo(addrinfo* res) override; }; using OsSysCallsSingleton = ThreadSafeSingleton; diff --git a/source/common/api/win32/os_sys_calls_impl.cc b/source/common/api/win32/os_sys_calls_impl.cc index 53e7d7df007a2..1a630b5d9c6b6 100644 --- a/source/common/api/win32/os_sys_calls_impl.cc +++ b/source/common/api/win32/os_sys_calls_impl.cc @@ -458,5 +458,13 @@ SysCallIntResult OsSysCallsImpl::getifaddrs([[maybe_unused]] InterfaceAddressVec PANIC("not implemented"); } +SysCallIntResult OsSysCallsImpl::getaddrinfo(const char* node, const char* service, + const addrinfo* hints, addrinfo** res) { + const int rc = ::getaddrinfo(node, service, hints, res); + return {rc, errno}; +} + +void OsSysCallsImpl::freeaddrinfo(addrinfo* res) { ::freeaddrinfo(res); } + } // namespace Api } // namespace Envoy diff --git a/source/common/api/win32/os_sys_calls_impl.h b/source/common/api/win32/os_sys_calls_impl.h index e2ca62d9c4e09..44bc4993709f8 100644 --- a/source/common/api/win32/os_sys_calls_impl.h +++ b/source/common/api/win32/os_sys_calls_impl.h @@ -64,6 +64,9 @@ class OsSysCallsImpl : public OsSysCalls { SysCallBoolResult socketTcpInfo(os_fd_t sockfd, EnvoyTcpInfo* tcp_info) override; bool supportsGetifaddrs() const override; SysCallIntResult getifaddrs(InterfaceAddressVector&) override; + SysCallIntResult getaddrinfo(const char* node, const char* service, const addrinfo* hints, + addrinfo** res) override; + void freeaddrinfo(addrinfo* res) override; }; using OsSysCallsSingleton = ThreadSafeSingleton; diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 12d64d11b49aa..ff8aba0efdf95 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -339,9 +339,10 @@ EXTENSIONS = { # c-ares DNS resolver extension is recommended to be enabled to maintain the legacy DNS resolving behavior. "envoy.network.dns_resolver.cares": "//source/extensions/network/dns_resolver/cares:config", - # apple DNS resolver extension is only needed in MacOS build plus one want to use apple library for DNS resolving. "envoy.network.dns_resolver.apple": "//source/extensions/network/dns_resolver/apple:config", + # getaddrinfo DNS resolver extension can be used when the system resolver is desired (e.g., Android) + "envoy.network.dns_resolver.getaddrinfo": "//source/extensions/network/dns_resolver/getaddrinfo:config", # # Custom matchers diff --git a/source/extensions/extensions_metadata.yaml b/source/extensions/extensions_metadata.yaml index 1752f4d02285b..b797c43d032ac 100644 --- a/source/extensions/extensions_metadata.yaml +++ b/source/extensions/extensions_metadata.yaml @@ -1096,6 +1096,13 @@ envoy.network.dns_resolver.apple: status: stable type_urls: - envoy.extensions.network.dns_resolver.apple.v3.AppleDnsResolverConfig +envoy.network.dns_resolver.getaddrinfo: + categories: + - envoy.network.dns_resolver + security_posture: robust_to_untrusted_downstream_and_upstream + status: stable + type_urls: + - envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig envoy.rbac.matchers.upstream_ip_port: categories: - envoy.rbac.matchers diff --git a/source/extensions/network/dns_resolver/getaddrinfo/BUILD b/source/extensions/network/dns_resolver/getaddrinfo/BUILD new file mode 100644 index 0000000000000..e2b26df7941cf --- /dev/null +++ b/source/extensions/network/dns_resolver/getaddrinfo/BUILD @@ -0,0 +1,20 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_extension( + name = "config", + srcs = ["getaddrinfo.cc"], + hdrs = ["getaddrinfo.h"], + deps = [ + "//envoy/network:dns_resolver_interface", + "//envoy/registry", + "@envoy_api//envoy/extensions/network/dns_resolver/getaddrinfo/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.cc b/source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.cc new file mode 100644 index 0000000000000..0607f309ee23e --- /dev/null +++ b/source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.cc @@ -0,0 +1,241 @@ +#include "source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.h" + +#include "envoy/extensions/network/dns_resolver/getaddrinfo/v3/getaddrinfo_dns_resolver.pb.h" +#include "envoy/network/dns_resolver.h" + +#include "source/common/api/os_sys_calls_impl.h" +#include "source/common/network/address_impl.h" + +namespace Envoy { +namespace Network { + +// This resolver uses getaddrinfo() on a dedicated resolution thread. Thus, it is only suitable +// currently for relatively low rate resolutions. In the future a thread pool could be added if +// desired. +class GetAddrInfoDnsResolver : public DnsResolver, public Logger::Loggable { +public: + GetAddrInfoDnsResolver(Event::Dispatcher& dispatcher, Api::Api& api) + : dispatcher_(dispatcher), + resolver_thread_(api.threadFactory().createThread([this] { resolveThreadRoutine(); })) {} + + ~GetAddrInfoDnsResolver() override { + { + absl::MutexLock guard(&mutex_); + shutting_down_ = true; + } + + resolver_thread_->join(); + } + + // DnsResolver + ActiveDnsQuery* resolve(const std::string& dns_name, DnsLookupFamily dns_lookup_family, + ResolveCb callback) override { + ENVOY_LOG(debug, "adding new query [{}] to pending queries", dns_name); + auto new_query = std::make_unique(dns_name, dns_lookup_family, callback); + absl::MutexLock guard(&mutex_); + pending_queries_.emplace_back(std::move(new_query)); + return pending_queries_.back().get(); + } + + void resetNetworking() override {} + +private: + class PendingQuery : public ActiveDnsQuery { + public: + PendingQuery(const std::string& dns_name, DnsLookupFamily dns_lookup_family, ResolveCb callback) + : dns_name_(dns_name), dns_lookup_family_(dns_lookup_family), callback_(callback) {} + + void cancel(CancelReason) override { + ENVOY_LOG(debug, "cancelling query [{}]", dns_name_); + cancelled_ = true; + } + + const std::string dns_name_; + const DnsLookupFamily dns_lookup_family_; + ResolveCb callback_; + bool cancelled_{false}; + }; + // Must be a shared_ptr for passing around via post. + using PendingQuerySharedPtr = std::shared_ptr; + + // RAII wrapper to free the `addrinfo`. + class AddrInfoWrapper : NonCopyable { + public: + AddrInfoWrapper(addrinfo* info) : info_(info) {} + ~AddrInfoWrapper() { + if (info_ != nullptr) { + Api::OsSysCallsSingleton::get().freeaddrinfo(info_); + } + } + const addrinfo* get() { return info_; } + + private: + addrinfo* info_; + }; + + // Parse a getaddrinfo() response and determine the final address list. We could potentially avoid + // adding v4 or v6 addresses if we know they will never be used. Right now the final filtering is + // done below and this code is kept simple. + std::pair> + processResponse(const PendingQuery& query, const addrinfo* addrinfo_result) { + std::list v4_results; + std::list v6_results; + for (auto ai = addrinfo_result; ai != nullptr; ai = ai->ai_next) { + if (ai->ai_family == AF_INET) { + sockaddr_in address; + memset(&address, 0, sizeof(address)); + address.sin_family = AF_INET; + address.sin_port = 0; + address.sin_addr = reinterpret_cast(ai->ai_addr)->sin_addr; + + v4_results.emplace_back( + DnsResponse(std::make_shared(&address), DEFAULT_TTL)); + } else if (ai->ai_family == AF_INET6) { + sockaddr_in6 address; + memset(&address, 0, sizeof(address)); + address.sin6_family = AF_INET6; + address.sin6_port = 0; + address.sin6_addr = reinterpret_cast(ai->ai_addr)->sin6_addr; + v6_results.emplace_back( + DnsResponse(std::make_shared(address), DEFAULT_TTL)); + } + } + + std::list final_results; + switch (query.dns_lookup_family_) { + case DnsLookupFamily::All: { + final_results = std::move(v4_results); + final_results.splice(final_results.begin(), v6_results); + break; + } + case DnsLookupFamily::V4Only: { + final_results = std::move(v4_results); + break; + } + case DnsLookupFamily::V6Only: { + final_results = std::move(v6_results); + break; + } + case DnsLookupFamily::V4Preferred: { + if (!v4_results.empty()) { + final_results = std::move(v4_results); + } else { + final_results = std::move(v6_results); + } + break; + } + case DnsLookupFamily::Auto: { + // This is effectively V6Preferred. + if (!v6_results.empty()) { + final_results = std::move(v6_results); + } else { + final_results = std::move(v4_results); + } + break; + } + } + + ENVOY_LOG(debug, "getaddrinfo resolution complete for host '{}': {}", query.dns_name_, + accumulateToString(final_results, [](const auto& dns_response) { + return dns_response.addrInfo().address_->asString(); + })); + + return std::make_pair(ResolutionStatus::Success, final_results); + } + + // Background thread which wakes up and does resolutions. + void resolveThreadRoutine() { + ENVOY_LOG(debug, "starting getaddrinfo resolver thread"); + + while (true) { + PendingQuerySharedPtr next_query; + { + absl::MutexLock guard(&mutex_); + auto condition = [this]() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_) { + return shutting_down_ || !pending_queries_.empty(); + }; + mutex_.Await(absl::Condition(&condition)); + if (shutting_down_) { + break; + } + + next_query = std::move(pending_queries_.front()); + pending_queries_.pop_front(); + } + + ENVOY_LOG(debug, "popped pending query [{}]", next_query->dns_name_); + + // For mock testing make sure the getaddrinfo() response is freed prior to the post. + std::pair> response; + { + addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_flags = AI_ADDRCONFIG; + hints.ai_family = AF_UNSPEC; + // If we don't specify a socket type, every address will appear twice, once + // for SOCK_STREAM and one for SOCK_DGRAM. Since we do not return the family + // anyway, just pick one. + hints.ai_socktype = SOCK_STREAM; + addrinfo* addrinfo_result_do_not_use = nullptr; + auto rc = Api::OsSysCallsSingleton::get().getaddrinfo( + next_query->dns_name_.c_str(), nullptr, &hints, &addrinfo_result_do_not_use); + auto addrinfo_wrapper = AddrInfoWrapper(addrinfo_result_do_not_use); + if (rc.return_value_ == 0) { + response = processResponse(*next_query, addrinfo_wrapper.get()); + } else { + // TODO(mattklein123): Handle some errors differently such as `EAI_NODATA`. + ENVOY_LOG(debug, "getaddrinfo failed with rc={} errno={}", gai_strerror(rc.return_value_), + errorDetails(rc.errno_)); + response = std::make_pair(ResolutionStatus::Failure, std::list()); + } + } + + dispatcher_.post( + [finished_query = std::move(next_query), response = std::move(response)]() mutable { + if (finished_query->cancelled_) { + ENVOY_LOG(debug, "dropping cancelled query [{}]", finished_query->dns_name_); + } else { + finished_query->callback_(response.first, std::move(response.second)); + } + }); + } + + ENVOY_LOG(debug, "getaddrinfo resolver thread exiting"); + } + + // getaddrinfo() doesn't provide TTL so use a hard coded default. This can be made configurable + // later if needed. + static constexpr std::chrono::seconds DEFAULT_TTL = std::chrono::seconds(60); + + Event::Dispatcher& dispatcher_; + absl::Mutex mutex_; + std::list pending_queries_ ABSL_GUARDED_BY(mutex_); + bool shutting_down_ ABSL_GUARDED_BY(mutex_){}; + // The resolver thread must be initialized last so that the above members are already fully + // initialized. + const Thread::ThreadPtr resolver_thread_; +}; + +// getaddrinfo DNS resolver factory +class GetAddrInfoDnsResolverFactory : public DnsResolverFactory, + public Logger::Loggable { +public: + std::string name() const override { return {"envoy.network.dns_resolver.getaddrinfo"}; } + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return ProtobufTypes::MessagePtr{new envoy::extensions::network::dns_resolver::getaddrinfo::v3:: + GetAddrInfoDnsResolverConfig()}; + } + + DnsResolverSharedPtr + createDnsResolver(Event::Dispatcher& dispatcher, Api::Api& api, + const envoy::config::core::v3::TypedExtensionConfig&) const override { + return std::make_shared(dispatcher, api); + } +}; + +// Register the CaresDnsResolverFactory +REGISTER_FACTORY(GetAddrInfoDnsResolverFactory, DnsResolverFactory); + +} // namespace Network +} // namespace Envoy diff --git a/source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.h b/source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.h new file mode 100644 index 0000000000000..b724274367490 --- /dev/null +++ b/source/extensions/network/dns_resolver/getaddrinfo/getaddrinfo.h @@ -0,0 +1,11 @@ +#pragma once + +#include "envoy/registry/registry.h" + +namespace Envoy { +namespace Network { + +DECLARE_FACTORY(GetAddrInfoDnsResolverFactory); + +} // namespace Network +} // namespace Envoy diff --git a/test/extensions/filters/http/dynamic_forward_proxy/BUILD b/test/extensions/filters/http/dynamic_forward_proxy/BUILD index 1dcafd08831a5..d97269f4a82ae 100644 --- a/test/extensions/filters/http/dynamic_forward_proxy/BUILD +++ b/test/extensions/filters/http/dynamic_forward_proxy/BUILD @@ -56,6 +56,7 @@ envoy_extension_cc_test( "//source/extensions/clusters/dynamic_forward_proxy:cluster", "//source/extensions/filters/http/dynamic_forward_proxy:config", "//source/extensions/key_value/file_based:config_lib", + "//source/extensions/network/dns_resolver/getaddrinfo:config", "//test/integration:http_integration_lib", "//test/integration/filters:stream_info_to_headers_filter_lib", "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", diff --git a/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_integration_test.cc b/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_integration_test.cc index 1bd6af2d265a9..b35df16e359e5 100644 --- a/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_integration_test.cc +++ b/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_integration_test.cc @@ -32,10 +32,12 @@ class ProxyFilterIntegrationTest : public testing::TestWithParambody()); } + void requestWithBodyTest(const std::string& typed_dns_resolver_config = "") { + int64_t original_usec = dispatcher_->timeSource().monotonicTime().time_since_epoch().count(); + + config_helper_.prependFilter(fmt::format(R"EOF( + name: stream-info-to-headers-filter +)EOF")); + + initializeWithArgs(1024, 1024, "", typed_dns_resolver_config); + codec_client_ = makeHttpConnection(lookupPort("http")); + default_request_headers_.setHost( + fmt::format("localhost:{}", fake_upstreams_[0]->localAddress()->ip()->port())); + + auto response = sendRequestAndWaitForResponse(default_request_headers_, 1024, + default_response_headers_, 1024); + checkSimpleRequestSuccess(1024, 1024, response.get()); + testConnectionTiming(response, false, original_usec); + EXPECT_EQ(1, test_server_->counter("dns_cache.foo.dns_query_attempt")->value()); + EXPECT_EQ(1, test_server_->counter("dns_cache.foo.host_added")->value()); + + // Now send another request. This should hit the DNS cache. + response = sendRequestAndWaitForResponse(default_request_headers_, 512, + default_response_headers_, 512); + checkSimpleRequestSuccess(512, 512, response.get()); + testConnectionTiming(response, true, original_usec); + EXPECT_EQ(1, test_server_->counter("dns_cache.foo.dns_query_attempt")->value()); + EXPECT_EQ(1, test_server_->counter("dns_cache.foo.host_added")->value()); + // Make sure dns timings are tracked for cache-hits. + ASSERT_FALSE(response->headers().get(Http::LowerCaseString("dns_start")).empty()); + ASSERT_FALSE(response->headers().get(Http::LowerCaseString("dns_end")).empty()); + + const Extensions::TransportSockets::Tls::SslHandshakerImpl* ssl_socket = + dynamic_cast( + fake_upstream_connection_->connection().ssl().get()); + EXPECT_STREQ("localhost", SSL_get_servername(ssl_socket->ssl(), TLSEXT_NAMETYPE_host_name)); + } + + void requestWithUnknownDomainTest(const std::string& typed_dns_resolver_config = "") { + useAccessLog("%RESPONSE_CODE_DETAILS%"); + initializeWithArgs(1024, 1024, "", typed_dns_resolver_config); + codec_client_ = makeHttpConnection(lookupPort("http")); + default_request_headers_.setHost("doesnotexist.example.com"); + + auto response = codec_client_->makeHeaderOnlyRequest(default_request_headers_); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_EQ("503", response->headers().getStatusValue()); + EXPECT_THAT(waitForAccessLog(access_log_name_), HasSubstr("dns_resolution_failure")); + + response = codec_client_->makeHeaderOnlyRequest(default_request_headers_); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_EQ("503", response->headers().getStatusValue()); + EXPECT_THAT(waitForAccessLog(access_log_name_, 1), HasSubstr("dns_resolution_failure")); + } + bool upstream_tls_{}; std::string upstream_cert_name_{"upstreamlocalhost"}; CdsHelper cds_helper_; @@ -225,65 +280,35 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, ProxyFilterIntegrationTest, // A basic test where we pause a request to lookup localhost, and then do another request which // should hit the TLS cache. -TEST_P(ProxyFilterIntegrationTest, RequestWithBody) { - int64_t original_usec = dispatcher_->timeSource().monotonicTime().time_since_epoch().count(); - - config_helper_.prependFilter(fmt::format(R"EOF( - name: stream-info-to-headers-filter -)EOF")); - - initializeWithArgs(); - codec_client_ = makeHttpConnection(lookupPort("http")); - const Http::TestRequestHeaderMapImpl request_headers{ - {":method", "POST"}, - {":path", "/test/long/url"}, - {":scheme", "http"}, - {":authority", - fmt::format("localhost:{}", fake_upstreams_[0]->localAddress()->ip()->port())}}; - - auto response = - sendRequestAndWaitForResponse(request_headers, 1024, default_response_headers_, 1024); - checkSimpleRequestSuccess(1024, 1024, response.get()); - testConnectionTiming(response, false, original_usec); - EXPECT_EQ(1, test_server_->counter("dns_cache.foo.dns_query_attempt")->value()); - EXPECT_EQ(1, test_server_->counter("dns_cache.foo.host_added")->value()); - - // Now send another request. This should hit the DNS cache. - response = sendRequestAndWaitForResponse(request_headers, 512, default_response_headers_, 512); - checkSimpleRequestSuccess(512, 512, response.get()); - testConnectionTiming(response, true, original_usec); - EXPECT_EQ(1, test_server_->counter("dns_cache.foo.dns_query_attempt")->value()); - EXPECT_EQ(1, test_server_->counter("dns_cache.foo.host_added")->value()); - // Make sure dns timings are tracked for cache-hits. - ASSERT_FALSE(response->headers().get(Http::LowerCaseString("dns_start")).empty()); - ASSERT_FALSE(response->headers().get(Http::LowerCaseString("dns_end")).empty()); +TEST_P(ProxyFilterIntegrationTest, RequestWithBody) { requestWithBodyTest(); } + +// Do a sanity check using the getaddrinfo() resolver. +TEST_P(ProxyFilterIntegrationTest, RequestWithBodyGetAddrInfoResolver) { + // getaddrinfo() does not reliably return v6 addresses depending on the environment. For now + // just run this on v4 which is most likely to succeed. In v6 only environments this test won't + // run at all but should still be covered in public CI. + if (GetParam() != Network::Address::IpVersion::v4) { + return; + } - const Extensions::TransportSockets::Tls::SslHandshakerImpl* ssl_socket = - dynamic_cast( - fake_upstream_connection_->connection().ssl().get()); - EXPECT_STREQ("localhost", SSL_get_servername(ssl_socket->ssl(), TLSEXT_NAMETYPE_host_name)); + requestWithBodyTest(R"EOF( + typed_dns_resolver_config: + name: envoy.network.dns_resolver.getaddrinfo + typed_config: + "@type": type.googleapis.com/envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig)EOF"); } // Currently if the first DNS resolution fails, the filter will continue with // a null address. Make sure this mode fails gracefully. -TEST_P(ProxyFilterIntegrationTest, RequestWithUnknownDomain) { - useAccessLog("%RESPONSE_CODE_DETAILS%"); - initializeWithArgs(); - codec_client_ = makeHttpConnection(lookupPort("http")); - const Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, - {":path", "/test/long/url"}, - {":scheme", "http"}, - {":authority", "doesnotexist.example.com"}}; - - auto response = codec_client_->makeHeaderOnlyRequest(default_request_headers_); - ASSERT_TRUE(response->waitForEndStream()); - EXPECT_EQ("503", response->headers().getStatusValue()); - EXPECT_THAT(waitForAccessLog(access_log_name_), HasSubstr("dns_resolution_failure")); - - response = codec_client_->makeHeaderOnlyRequest(default_request_headers_); - ASSERT_TRUE(response->waitForEndStream()); - EXPECT_EQ("503", response->headers().getStatusValue()); - EXPECT_THAT(waitForAccessLog(access_log_name_, 1), HasSubstr("dns_resolution_failure")); +TEST_P(ProxyFilterIntegrationTest, RequestWithUnknownDomain) { requestWithUnknownDomainTest(); } + +// Do a sanity check using the getaddrinfo() resolver. +TEST_P(ProxyFilterIntegrationTest, RequestWithUnknownDomainGetAddrInfoResolver) { + requestWithUnknownDomainTest(R"EOF( + typed_dns_resolver_config: + name: envoy.network.dns_resolver.getaddrinfo + typed_config: + "@type": type.googleapis.com/envoy.extensions.network.dns_resolver.getaddrinfo.v3.GetAddrInfoDnsResolverConfig)EOF"); } TEST_P(ProxyFilterIntegrationTest, RequestWithUnknownDomainAndNoCaching) { diff --git a/test/extensions/network/dns_resolver/getaddrinfo/BUILD b/test/extensions/network/dns_resolver/getaddrinfo/BUILD new file mode 100644 index 0000000000000..883e567cf24a9 --- /dev/null +++ b/test/extensions/network/dns_resolver/getaddrinfo/BUILD @@ -0,0 +1,24 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "getaddrinfo_test", + srcs = ["getaddrinfo_test.cc"], + extension_names = ["envoy.network.dns_resolver.getaddrinfo"], + deps = [ + "//source/extensions/network/dns_resolver/getaddrinfo:config", + "//test/mocks/api:api_mocks", + "//test/test_common:threadsafe_singleton_injector_lib", + "@envoy_api//envoy/extensions/network/dns_resolver/getaddrinfo/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/network/dns_resolver/getaddrinfo/getaddrinfo_test.cc b/test/extensions/network/dns_resolver/getaddrinfo/getaddrinfo_test.cc new file mode 100644 index 0000000000000..2be27db4c7ef5 --- /dev/null +++ b/test/extensions/network/dns_resolver/getaddrinfo/getaddrinfo_test.cc @@ -0,0 +1,292 @@ +#include "envoy/extensions/network/dns_resolver/getaddrinfo/v3/getaddrinfo_dns_resolver.pb.h" + +#include "source/common/network/dns_resolver/dns_factory_util.h" +#include "source/common/network/utility.h" + +#include "test/mocks/api/mocks.h" +#include "test/test_common/threadsafe_singleton_injector.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +using testing::NiceMock; +using testing::Return; + +namespace Envoy { +namespace Network { +namespace { + +class GetAddrInfoDnsImplTest : public testing::Test { +public: + GetAddrInfoDnsImplTest() + : api_(Api::createApiForTest()), dispatcher_(api_->allocateDispatcher("test_thread")) { + envoy::config::core::v3::TypedExtensionConfig typed_dns_resolver_config; + envoy::extensions::network::dns_resolver::getaddrinfo::v3::GetAddrInfoDnsResolverConfig + getaddrinfo; + typed_dns_resolver_config.mutable_typed_config()->PackFrom(getaddrinfo); + typed_dns_resolver_config.set_name(std::string("envoy.network.dns_resolver.getaddrinfo")); + + Network::DnsResolverFactory& dns_resolver_factory = + createDnsResolverFactoryFromTypedConfig(typed_dns_resolver_config); + resolver_ = + dns_resolver_factory.createDnsResolver(*dispatcher_, *api_, typed_dns_resolver_config); + + // NOP for coverage. + resolver_->resetNetworking(); + } + + void setupFakeGai(std::vector addresses = { + Utility::getCanonicalIpv4LoopbackAddress(), + Utility::getIpv6LoopbackAddress()}) { + EXPECT_CALL(os_sys_calls_, getaddrinfo(_, _, _, _)) + .WillOnce(Invoke([addresses](const char*, const char*, const addrinfo*, addrinfo** res) { + *res = makeGaiResponse(addresses); + return Api::SysCallIntResult{0, 0}; + })); + EXPECT_CALL(os_sys_calls_, freeaddrinfo(_)).WillOnce(Invoke([](addrinfo* res) { + freeGaiResponse(res); + })); + } + + static addrinfo* makeGaiResponse(std::vector addresses) { + auto gai_response = reinterpret_cast(malloc(sizeof(addrinfo))); + auto next_ai = gai_response; + + for (size_t i = 0; i < addresses.size(); i++) { + memset(next_ai, 0, sizeof(addrinfo)); + auto address = addresses[i]; + + if (address->ip()->ipv4() != nullptr) { + next_ai->ai_family = AF_INET; + } else { + next_ai->ai_family = AF_INET6; + } + + sockaddr_storage* storage = + reinterpret_cast(malloc(sizeof(sockaddr_storage))); + next_ai->ai_addr = reinterpret_cast(storage); + memcpy(next_ai->ai_addr, address->sockAddr(), address->sockAddrLen()); + + if (i != addresses.size() - 1) { + auto new_ai = reinterpret_cast(malloc(sizeof(addrinfo))); + next_ai->ai_next = new_ai; + next_ai = new_ai; + } + } + + return gai_response; + } + + static void freeGaiResponse(addrinfo* response) { + for (auto ai = response; ai != nullptr;) { + free(ai->ai_addr); + auto next_ai = ai->ai_next; + free(ai); + ai = next_ai; + } + } + + void verifyRealGaiResponse(DnsResolver::ResolutionStatus status, + std::list&& response) { + // Since we use AF_UNSPEC, depending on the CI environment we might get either 1 or 2 + // addresses. + EXPECT_EQ(status, DnsResolver::ResolutionStatus::Success); + EXPECT_TRUE(response.size() == 1 || response.size() == 2); + EXPECT_TRUE("127.0.0.1:0" == response.front().addrInfo().address_->asString() || + "[::1]:0" == response.front().addrInfo().address_->asString()); + } + + Api::ApiPtr api_; + Event::DispatcherPtr dispatcher_; + DnsResolverSharedPtr resolver_; + NiceMock os_sys_calls_; +}; + +TEST_F(GetAddrInfoDnsImplTest, LocalhostResolve) { + resolver_->resolve( + "localhost", DnsLookupFamily::All, + [this](DnsResolver::ResolutionStatus status, std::list&& response) { + verifyRealGaiResponse(status, std::move(response)); + dispatcher_->exit(); + }); + + dispatcher_->run(Event::Dispatcher::RunType::RunUntilExit); +} + +TEST_F(GetAddrInfoDnsImplTest, Cancel) { + auto query = + resolver_->resolve("localhost", DnsLookupFamily::All, + [](DnsResolver::ResolutionStatus, std::list&&) { FAIL(); }); + + query->cancel(ActiveDnsQuery::CancelReason::QueryAbandoned); + + resolver_->resolve( + "localhost", DnsLookupFamily::All, + [this](DnsResolver::ResolutionStatus status, std::list&& response) { + verifyRealGaiResponse(status, std::move(response)); + dispatcher_->exit(); + }); + + dispatcher_->run(Event::Dispatcher::RunType::RunUntilExit); +} + +TEST_F(GetAddrInfoDnsImplTest, Failure) { + TestThreadsafeSingletonInjector os_calls(&os_sys_calls_); + + EXPECT_CALL(os_sys_calls_, getaddrinfo(_, _, _, _)) + .WillOnce(Return(Api::SysCallIntResult{EAI_AGAIN, 0})); + resolver_->resolve( + "localhost", DnsLookupFamily::All, + [this](DnsResolver::ResolutionStatus status, std::list&& response) { + EXPECT_EQ(status, DnsResolver::ResolutionStatus::Failure); + EXPECT_TRUE(response.empty()); + + dispatcher_->exit(); + }); + + dispatcher_->run(Event::Dispatcher::RunType::RunUntilExit); +} + +TEST_F(GetAddrInfoDnsImplTest, All) { + TestThreadsafeSingletonInjector os_calls(&os_sys_calls_); + setupFakeGai(); + + resolver_->resolve( + "localhost", DnsLookupFamily::All, + [this](DnsResolver::ResolutionStatus status, std::list&& response) { + EXPECT_EQ(status, DnsResolver::ResolutionStatus::Success); + EXPECT_EQ(2, response.size()); + EXPECT_EQ("[[::1]:0, 127.0.0.1:0]", + accumulateToString(response, [](const auto& dns_response) { + return dns_response.addrInfo().address_->asString(); + })); + + dispatcher_->exit(); + }); + + dispatcher_->run(Event::Dispatcher::RunType::RunUntilExit); +} + +TEST_F(GetAddrInfoDnsImplTest, V4Only) { + TestThreadsafeSingletonInjector os_calls(&os_sys_calls_); + setupFakeGai(); + + resolver_->resolve( + "localhost", DnsLookupFamily::V4Only, + [this](DnsResolver::ResolutionStatus status, std::list&& response) { + EXPECT_EQ(status, DnsResolver::ResolutionStatus::Success); + EXPECT_EQ(1, response.size()); + EXPECT_EQ("[127.0.0.1:0]", + accumulateToString(response, [](const auto& dns_response) { + return dns_response.addrInfo().address_->asString(); + })); + + dispatcher_->exit(); + }); + + dispatcher_->run(Event::Dispatcher::RunType::RunUntilExit); +} + +TEST_F(GetAddrInfoDnsImplTest, V6Only) { + TestThreadsafeSingletonInjector os_calls(&os_sys_calls_); + setupFakeGai(); + + resolver_->resolve( + "localhost", DnsLookupFamily::V6Only, + [this](DnsResolver::ResolutionStatus status, std::list&& response) { + EXPECT_EQ(status, DnsResolver::ResolutionStatus::Success); + EXPECT_EQ(1, response.size()); + EXPECT_EQ("[[::1]:0]", + accumulateToString(response, [](const auto& dns_response) { + return dns_response.addrInfo().address_->asString(); + })); + + dispatcher_->exit(); + }); + + dispatcher_->run(Event::Dispatcher::RunType::RunUntilExit); +} + +TEST_F(GetAddrInfoDnsImplTest, V4Preferred) { + TestThreadsafeSingletonInjector os_calls(&os_sys_calls_); + setupFakeGai(); + + resolver_->resolve( + "localhost", DnsLookupFamily::V4Preferred, + [this](DnsResolver::ResolutionStatus status, std::list&& response) { + EXPECT_EQ(status, DnsResolver::ResolutionStatus::Success); + EXPECT_EQ(1, response.size()); + EXPECT_EQ("[127.0.0.1:0]", + accumulateToString(response, [](const auto& dns_response) { + return dns_response.addrInfo().address_->asString(); + })); + + dispatcher_->exit(); + }); + + dispatcher_->run(Event::Dispatcher::RunType::RunUntilExit); +} + +TEST_F(GetAddrInfoDnsImplTest, V4PreferredNoV4) { + TestThreadsafeSingletonInjector os_calls(&os_sys_calls_); + setupFakeGai({Utility::getIpv6LoopbackAddress()}); + + resolver_->resolve( + "localhost", DnsLookupFamily::V4Preferred, + [this](DnsResolver::ResolutionStatus status, std::list&& response) { + EXPECT_EQ(status, DnsResolver::ResolutionStatus::Success); + EXPECT_EQ(1, response.size()); + EXPECT_EQ("[[::1]:0]", + accumulateToString(response, [](const auto& dns_response) { + return dns_response.addrInfo().address_->asString(); + })); + + dispatcher_->exit(); + }); + + dispatcher_->run(Event::Dispatcher::RunType::RunUntilExit); +} + +TEST_F(GetAddrInfoDnsImplTest, Auto) { + TestThreadsafeSingletonInjector os_calls(&os_sys_calls_); + setupFakeGai(); + + resolver_->resolve( + "localhost", DnsLookupFamily::Auto, + [this](DnsResolver::ResolutionStatus status, std::list&& response) { + EXPECT_EQ(status, DnsResolver::ResolutionStatus::Success); + EXPECT_EQ(1, response.size()); + EXPECT_EQ("[[::1]:0]", + accumulateToString(response, [](const auto& dns_response) { + return dns_response.addrInfo().address_->asString(); + })); + + dispatcher_->exit(); + }); + + dispatcher_->run(Event::Dispatcher::RunType::RunUntilExit); +} + +TEST_F(GetAddrInfoDnsImplTest, AutoNoV6) { + TestThreadsafeSingletonInjector os_calls(&os_sys_calls_); + setupFakeGai({Utility::getCanonicalIpv4LoopbackAddress()}); + + resolver_->resolve( + "localhost", DnsLookupFamily::Auto, + [this](DnsResolver::ResolutionStatus status, std::list&& response) { + EXPECT_EQ(status, DnsResolver::ResolutionStatus::Success); + EXPECT_EQ(1, response.size()); + EXPECT_EQ("[127.0.0.1:0]", + accumulateToString(response, [](const auto& dns_response) { + return dns_response.addrInfo().address_->asString(); + })); + + dispatcher_->exit(); + }); + + dispatcher_->run(Event::Dispatcher::RunType::RunUntilExit); +} + +} // namespace +} // namespace Network +} // namespace Envoy diff --git a/test/mocks/api/mocks.h b/test/mocks/api/mocks.h index 551e61d0802c5..7d136a0784237 100644 --- a/test/mocks/api/mocks.h +++ b/test/mocks/api/mocks.h @@ -127,6 +127,9 @@ class MockOsSysCalls : public OsSysCallsImpl { MOCK_METHOD(bool, supportsGetifaddrs, (), (const)); MOCK_METHOD(void, setAlternateGetifaddrs, (AlternateGetifaddrs alternate_getifaddrs)); MOCK_METHOD(SysCallIntResult, getifaddrs, (InterfaceAddressVector & interfaces)); + MOCK_METHOD(SysCallIntResult, getaddrinfo, + (const char* node, const char* service, const addrinfo* hints, addrinfo** res)); + MOCK_METHOD(void, freeaddrinfo, (addrinfo * res)); // Map from (sockfd,level,optname) to boolean socket option. using SockOptKey = std::tuple; diff --git a/test/per_file_coverage.sh b/test/per_file_coverage.sh index d807b05a0b66b..71cda7ad7013f 100755 --- a/test/per_file_coverage.sh +++ b/test/per_file_coverage.sh @@ -69,6 +69,7 @@ declare -a KNOWN_LOW_COVERAGE=( "source/extensions/health_checkers/redis:95.7" "source/extensions/io_socket:96.2" "source/extensions/io_socket/user_space:96.2" +"source/extensions/network/dns_resolver/getaddrinfo:96.3" "source/extensions/rate_limit_descriptors:95.5" "source/extensions/rate_limit_descriptors/expr:95.5" "source/extensions/stat_sinks/common:96.4"