diff --git a/build_autogenerated.yaml b/build_autogenerated.yaml index 7e5cd1cdb03..9174423b818 100644 --- a/build_autogenerated.yaml +++ b/build_autogenerated.yaml @@ -9866,7 +9866,8 @@ targets: gtest: true build: test language: c++ - headers: [] + headers: + - test/core/util/scoped_env_var.h src: - test/core/client_channel/http_proxy_mapper_test.cc deps: diff --git a/include/grpc/impl/channel_arg_names.h b/include/grpc/impl/channel_arg_names.h index 565339a8728..def07be248d 100644 --- a/include/grpc/impl/channel_arg_names.h +++ b/include/grpc/impl/channel_arg_names.h @@ -328,6 +328,16 @@ /** Channel arg to set http proxy per channel. If set, the channel arg * value will be preferred over the environment variable settings. */ #define GRPC_ARG_HTTP_PROXY "grpc.http_proxy" +/** Specifies an HTTP proxy to use for individual addresses. + * The proxy must be specified as an IP address, not a DNS name. + * If set, the channel arg value will be preferred over the environment + * variable settings. */ +#define GRPC_ARG_ADDRESS_HTTP_PROXY "grpc.address_http_proxy" +/** Comma separated list of addresses or address ranges that are behind the + * address HTTP proxy. + */ +#define GRPC_ARG_ADDRESS_HTTP_PROXY_ENABLED_ADDRESSES \ + "grpc.address_http_proxy_enabled_addresses" /** If set to non zero, surfaces the user agent string to the server. User agent is surfaced by default. */ #define GRPC_ARG_SURFACE_USER_AGENT "grpc.surface_user_agent" diff --git a/src/core/ext/filters/client_channel/http_proxy_mapper.cc b/src/core/ext/filters/client_channel/http_proxy_mapper.cc index 1ef7b68b302..787b6b03591 100644 --- a/src/core/ext/filters/client_channel/http_proxy_mapper.cc +++ b/src/core/ext/filters/client_channel/http_proxy_mapper.cc @@ -26,7 +26,6 @@ #include #include #include -#include #include "absl/status/status.h" #include "absl/status/statusor.h" @@ -58,15 +57,10 @@ namespace grpc_core { namespace { -bool ServerInCIDRRange(absl::string_view server_host, - absl::string_view no_proxy_entry) { - auto server_address = StringToSockaddr(server_host, 0); - if (!server_address.ok()) { - return false; - } +bool ServerInCIDRRange(const grpc_resolved_address& server_address, + absl::string_view cidr_range) { std::pair possible_cidr = - absl::StrSplit(no_proxy_entry, absl::MaxSplits('/', 2), - absl::SkipEmpty()); + absl::StrSplit(cidr_range, absl::MaxSplits('/', 1), absl::SkipEmpty()); if (possible_cidr.first.empty() || possible_cidr.second.empty()) { return false; } @@ -77,12 +71,34 @@ bool ServerInCIDRRange(absl::string_view server_host, uint32_t mask_bits = 0; if (absl::SimpleAtoi(possible_cidr.second, &mask_bits)) { grpc_sockaddr_mask_bits(&*proxy_address, mask_bits); - return grpc_sockaddr_match_subnet(&*server_address, &*proxy_address, + return grpc_sockaddr_match_subnet(&server_address, &*proxy_address, mask_bits); } return false; } +bool ExactMatchOrSubdomain(absl::string_view host_name, + absl::string_view host_name_or_domain) { + return absl::EndsWithIgnoreCase(host_name, host_name_or_domain); +} + +// Parses the list of host names, addresses or subnet masks and returns true if +// the target address or host matches any value. +bool AddressIncluded( + const absl::optional& target_address, + absl::string_view host_name, absl::string_view addresses_and_subnets) { + for (absl::string_view entry : + absl::StrSplit(addresses_and_subnets, ',', absl::SkipEmpty())) { + absl::string_view sanitized_entry = absl::StripAsciiWhitespace(entry); + if (ExactMatchOrSubdomain(host_name, sanitized_entry) || + (target_address.has_value() && + ServerInCIDRRange(*target_address, sanitized_entry))) { + return true; + } + } + return false; +} + /// /// Parses the 'https_proxy' env var (fallback on 'http_proxy') and returns the /// proxy hostname to resolve or nullopt on error. Also sets 'user_cred' to user @@ -157,6 +173,33 @@ std::string MaybeAddDefaultPort(absl::string_view target) { return std::string(target); } +absl::optional GetChannelArgOrEnvVarValue( + const ChannelArgs& args, absl::string_view channel_arg, + const char* env_var) { + auto arg_value = args.GetOwnedString(channel_arg); + if (arg_value.has_value()) { + return arg_value; + } + return GetEnv(env_var); +} + +absl::optional GetAddressProxyServer( + const ChannelArgs& args) { + auto address_value = GetChannelArgOrEnvVarValue( + args, GRPC_ARG_ADDRESS_HTTP_PROXY, HttpProxyMapper::kAddressProxyEnvVar); + if (!address_value.has_value()) { + return absl::nullopt; + } + auto address = StringToSockaddr(*address_value); + if (!address.ok()) { + gpr_log(GPR_ERROR, "cannot parse value of '%s' env var. Error: %s", + HttpProxyMapper::kAddressProxyEnvVar, + address.status().ToString().c_str()); + return absl::nullopt; + } + return *address; +} + } // namespace absl::optional HttpProxyMapper::MapName( @@ -191,7 +234,6 @@ absl::optional HttpProxyMapper::MapName( no_proxy_str = GetEnv("no_proxy"); } if (no_proxy_str.has_value()) { - bool use_proxy = true; std::string server_host; std::string server_port; if (!SplitHostPort(absl::StripPrefix(uri->path(), "/"), &server_host, @@ -201,19 +243,15 @@ absl::optional HttpProxyMapper::MapName( "host '%s'", std::string(server_uri).c_str()); } else { - std::vector no_proxy_hosts = - absl::StrSplit(*no_proxy_str, ',', absl::SkipEmpty()); - for (const auto& no_proxy_entry : no_proxy_hosts) { - auto entry = absl::StripAsciiWhitespace(no_proxy_entry); - if (absl::EndsWithIgnoreCase(server_host, entry) || - ServerInCIDRRange(server_host, entry)) { - gpr_log(GPR_INFO, "not using proxy for host in no_proxy list '%s'", - std::string(server_uri).c_str()); - use_proxy = false; - break; - } + auto address = StringToSockaddr(server_host, 0); + if (AddressIncluded(address.ok() + ? absl::optional(*address) + : absl::nullopt, + server_host, *no_proxy_str)) { + gpr_log(GPR_INFO, "not using proxy for host in no_proxy list '%s'", + std::string(server_uri).c_str()); + return absl::nullopt; } - if (!use_proxy) return absl::nullopt; } } *args = args->Set(GRPC_ARG_HTTP_CONNECT_SERVER, @@ -229,6 +267,35 @@ absl::optional HttpProxyMapper::MapName( return name_to_resolve; } +absl::optional HttpProxyMapper::MapAddress( + const grpc_resolved_address& address, ChannelArgs* args) { + auto proxy_address = GetAddressProxyServer(*args); + if (!proxy_address.has_value()) { + return absl::nullopt; + } + auto address_string = grpc_sockaddr_to_string(&address, true); + if (!address_string.ok()) { + gpr_log(GPR_ERROR, "Unable to convert address to string: %s", + std::string(address_string.status().message()).c_str()); + return absl::nullopt; + } + std::string host_name, port; + if (!SplitHostPort(*address_string, &host_name, &port)) { + gpr_log(GPR_ERROR, "Address %s cannot be split in host and port", + address_string->c_str()); + return absl::nullopt; + } + auto enabled_addresses = GetChannelArgOrEnvVarValue( + *args, GRPC_ARG_ADDRESS_HTTP_PROXY_ENABLED_ADDRESSES, + kAddressProxyEnabledAddressesEnvVar); + if (!enabled_addresses.has_value() || + !AddressIncluded(address, host_name, *enabled_addresses)) { + return absl::nullopt; + } + *args = args->Set(GRPC_ARG_HTTP_CONNECT_SERVER, *address_string); + return proxy_address; +} + void RegisterHttpProxyMapper(CoreConfiguration::Builder* builder) { builder->proxy_mapper_registry()->Register( true /* at_start */, diff --git a/src/core/ext/filters/client_channel/http_proxy_mapper.h b/src/core/ext/filters/client_channel/http_proxy_mapper.h index 3e955df59d2..446fca98d8a 100644 --- a/src/core/ext/filters/client_channel/http_proxy_mapper.h +++ b/src/core/ext/filters/client_channel/http_proxy_mapper.h @@ -35,14 +35,15 @@ namespace grpc_core { class HttpProxyMapper : public ProxyMapperInterface { public: + static constexpr char const* kAddressProxyEnvVar = "GRPC_ADDRESS_HTTP_PROXY"; + static constexpr char const* kAddressProxyEnabledAddressesEnvVar = + "GRPC_ADDRESS_HTTP_PROXY_ENABLED_ADDRESSES"; + absl::optional MapName(absl::string_view server_uri, ChannelArgs* args) override; absl::optional MapAddress( - const grpc_resolved_address& /*address*/, - ChannelArgs* /*args*/) override { - return absl::nullopt; - } + const grpc_resolved_address& address, ChannelArgs* args) override; }; void RegisterHttpProxyMapper(CoreConfiguration::Builder* builder); diff --git a/test/core/client_channel/BUILD b/test/core/client_channel/BUILD index 478a320fde8..48c8d7295d2 100644 --- a/test/core/client_channel/BUILD +++ b/test/core/client_channel/BUILD @@ -43,6 +43,7 @@ grpc_cc_test( "//:grpc", "//src/core:channel_args", "//test/core/util:grpc_test_util", + "//test/core/util:scoped_env_var", ], ) diff --git a/test/core/client_channel/http_proxy_mapper_test.cc b/test/core/client_channel/http_proxy_mapper_test.cc index 798c3a0321b..8ec584a75ab 100644 --- a/test/core/client_channel/http_proxy_mapper_test.cc +++ b/test/core/client_channel/http_proxy_mapper_test.cc @@ -18,31 +18,49 @@ #include "src/core/ext/filters/client_channel/http_proxy_mapper.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_format.h" #include "absl/types/optional.h" +#include "gmock/gmock.h" #include "gtest/gtest.h" #include +#include "src/core/lib/address_utils/parse_address.h" +#include "src/core/lib/address_utils/sockaddr_utils.h" #include "src/core/lib/channel/channel_args.h" -#include "src/core/lib/gprpp/env.h" #include "src/core/lib/transport/http_connect_handshaker.h" +#include "test/core/util/scoped_env_var.h" #include "test/core/util/test_config.h" namespace grpc_core { namespace testing { namespace { -class ScopedSetEnv { - public: - explicit ScopedSetEnv(const char* value) { SetEnv("no_proxy", value); } - ScopedSetEnv(const ScopedSetEnv&) = delete; - ScopedSetEnv& operator=(const ScopedSetEnv&) = delete; - ~ScopedSetEnv() { UnsetEnv("no_proxy"); } -}; +const char* kNoProxyVarName = "no_proxy"; + +MATCHER_P(AddressEq, address, absl::StrFormat("is address %s", address)) { + if (!arg.has_value()) { + *result_listener << "is empty"; + return false; + } + auto address_string = grpc_sockaddr_to_string(&arg.value(), true); + if (!address_string.ok()) { + *result_listener << "unable to convert address to string: " + << address_string.status(); + return false; + } + if (*address_string != address) { + *result_listener << "value: " << *address_string; + return false; + } + return true; +} // Test that an empty no_proxy works as expected, i.e., proxy is used. TEST(NoProxyTest, EmptyList) { - ScopedSetEnv no_proxy(""); + ScopedEnvVar no_proxy(kNoProxyVarName, ""); auto args = ChannelArgs().Set(GRPC_ARG_HTTP_PROXY, "http://proxy.google.com"); EXPECT_EQ(HttpProxyMapper().MapName("dns:///test.google.com:443", &args), "proxy.google.com"); @@ -52,7 +70,7 @@ TEST(NoProxyTest, EmptyList) { // Test basic usage of 'no_proxy' to avoid using proxy for certain domain names. TEST(NoProxyTest, Basic) { - ScopedSetEnv no_proxy("google.com"); + ScopedEnvVar no_proxy(kNoProxyVarName, "google.com"); auto args = ChannelArgs().Set(GRPC_ARG_HTTP_PROXY, "http://proxy.google.com"); EXPECT_EQ(HttpProxyMapper().MapName("dns:///test.google.com:443", &args), absl::nullopt); @@ -61,7 +79,7 @@ TEST(NoProxyTest, Basic) { // Test empty entries in 'no_proxy' list. TEST(NoProxyTest, EmptyEntries) { - ScopedSetEnv no_proxy("foo.com,,google.com,,"); + ScopedEnvVar no_proxy(kNoProxyVarName, "foo.com,,google.com,,"); auto args = ChannelArgs().Set(GRPC_ARG_HTTP_PROXY, "http://proxy.google.com"); EXPECT_EQ(HttpProxyMapper().MapName("dns:///test.google.com:443", &args), absl::nullopt); @@ -70,7 +88,7 @@ TEST(NoProxyTest, EmptyEntries) { // Test entries with CIDR blocks (Class A) in 'no_proxy' list. TEST(NoProxyTest, CIDRClassAEntries) { - ScopedSetEnv no_proxy("foo.com,192.168.0.255/8"); + ScopedEnvVar no_proxy(kNoProxyVarName, "foo.com,192.168.0.255/8"); auto args = ChannelArgs().Set(GRPC_ARG_HTTP_PROXY, "http://proxy.google.com"); // address matching no_proxy cidr block EXPECT_EQ(HttpProxyMapper().MapName("dns:///192.0.1.1:443", &args), @@ -84,7 +102,7 @@ TEST(NoProxyTest, CIDRClassAEntries) { // Test entries with CIDR blocks (Class B) in 'no_proxy' list. TEST(NoProxyTest, CIDRClassBEntries) { - ScopedSetEnv no_proxy("foo.com,192.168.0.255/16"); + ScopedEnvVar no_proxy(kNoProxyVarName, "foo.com,192.168.0.255/16"); auto args = ChannelArgs().Set(GRPC_ARG_HTTP_PROXY, "http://proxy.google.com"); // address matching no_proxy cidr block EXPECT_EQ(HttpProxyMapper().MapName("dns:///192.168.1.5:443", &args), @@ -98,7 +116,7 @@ TEST(NoProxyTest, CIDRClassBEntries) { // Test entries with CIDR blocks (Class C) in 'no_proxy' list. TEST(NoProxyTest, CIDRClassCEntries) { - ScopedSetEnv no_proxy("foo.com,192.168.0.255/24"); + ScopedEnvVar no_proxy(kNoProxyVarName, "foo.com,192.168.0.255/24"); auto args = ChannelArgs().Set(GRPC_ARG_HTTP_PROXY, "http://proxy.google.com"); // address matching no_proxy cidr block EXPECT_EQ(HttpProxyMapper().MapName("dns:///192.168.0.5:443", &args), @@ -112,7 +130,7 @@ TEST(NoProxyTest, CIDRClassCEntries) { // Test entries with CIDR blocks (exact match) in 'no_proxy' list. TEST(NoProxyTest, CIDREntriesExactMatch) { - ScopedSetEnv no_proxy("foo.com,192.168.0.4/32"); + ScopedEnvVar no_proxy(kNoProxyVarName, "foo.com,192.168.0.4/32"); auto args = ChannelArgs().Set(GRPC_ARG_HTTP_PROXY, "http://proxy.google.com"); // address matching no_proxy cidr block EXPECT_EQ(HttpProxyMapper().MapName("dns:///192.168.0.4:443", &args), @@ -126,7 +144,7 @@ TEST(NoProxyTest, CIDREntriesExactMatch) { // Test entries with IPv6 CIDR blocks in 'no_proxy' list. TEST(NoProxyTest, CIDREntriesIPv6ExactMatch) { - ScopedSetEnv no_proxy("foo.com,2002:db8:a::45/64"); + ScopedEnvVar no_proxy(kNoProxyVarName, "foo.com,2002:db8:a::45/64"); auto args = ChannelArgs().Set(GRPC_ARG_HTTP_PROXY, "http://proxy.google.com"); // address matching no_proxy cidr block EXPECT_EQ(HttpProxyMapper().MapName( @@ -143,7 +161,7 @@ TEST(NoProxyTest, CIDREntriesIPv6ExactMatch) { // Test entries with whitespaced CIDR blocks in 'no_proxy' list. TEST(NoProxyTest, WhitespacedEntries) { - ScopedSetEnv no_proxy("foo.com, 192.168.0.255/24"); + ScopedEnvVar no_proxy(kNoProxyVarName, "foo.com, 192.168.0.255/24"); auto args = ChannelArgs().Set(GRPC_ARG_HTTP_PROXY, "http://proxy.google.com"); // address matching no_proxy cidr block EXPECT_EQ(HttpProxyMapper().MapName("dns:///192.168.0.5:443", &args), @@ -157,12 +175,84 @@ TEST(NoProxyTest, WhitespacedEntries) { // Test entries with invalid CIDR blocks in 'no_proxy' list. TEST(NoProxyTest, InvalidCIDREntries) { - ScopedSetEnv no_proxy("foo.com, 192.168.0.255/33"); + ScopedEnvVar no_proxy(kNoProxyVarName, "foo.com, 192.168.0.255/33"); auto args = ChannelArgs().Set(GRPC_ARG_HTTP_PROXY, "http://proxy.google.com"); EXPECT_EQ(HttpProxyMapper().MapName("dns:///192.168.1.0:443", &args), "proxy.google.com"); EXPECT_EQ(args.GetString(GRPC_ARG_HTTP_CONNECT_SERVER), "192.168.1.0:443"); } + +TEST(ProxyForAddressTest, ChannelArgPreferred) { + ScopedEnvVar address_proxy(HttpProxyMapper::kAddressProxyEnvVar, + "192.168.0.100:2020"); + auto args = ChannelArgs() + .Set(GRPC_ARG_ADDRESS_HTTP_PROXY, "192.168.0.101:2020") + .Set(GRPC_ARG_ADDRESS_HTTP_PROXY_ENABLED_ADDRESSES, + "255.255.255.255/0"); + auto address = StringToSockaddr("192.168.0.1:3333"); + ASSERT_TRUE(address.ok()) << address.status(); + EXPECT_THAT(HttpProxyMapper().MapAddress(*address, &args), + AddressEq("192.168.0.101:2020")); + EXPECT_EQ(args.GetString(GRPC_ARG_HTTP_CONNECT_SERVER), "192.168.0.1:3333"); +} + +TEST(ProxyForAddressTest, AddressesNotIncluded) { + ScopedEnvVar address_proxy(HttpProxyMapper::kAddressProxyEnvVar, + "192.168.0.100:2020"); + ScopedEnvVar address_proxy_enabled( + HttpProxyMapper::kAddressProxyEnabledAddressesEnvVar, + " 192.168.0.0/24 , 192.168.1.1 , 2001:db8:1::0/48 , 2001:db8:2::5"); + // v4 address + auto address = StringToSockaddr("192.168.2.1:3333"); + ASSERT_TRUE(address.ok()) << address.status(); + ChannelArgs args; + EXPECT_EQ(HttpProxyMapper().MapAddress(*address, &args), absl::nullopt); + EXPECT_EQ(args.GetString(GRPC_ARG_HTTP_CONNECT_SERVER), absl::nullopt); + // v6 address + address = StringToSockaddr("[2001:db8:2::1]:3000"); + ASSERT_TRUE(address.ok()) << address.status(); + args = ChannelArgs(); + EXPECT_EQ(HttpProxyMapper().MapAddress(*address, &args), absl::nullopt); + EXPECT_EQ(args.GetString(GRPC_ARG_HTTP_CONNECT_SERVER), absl::nullopt); +} + +TEST(ProxyForAddressTest, BadProxy) { + auto args = ChannelArgs().Set(GRPC_ARG_HTTP_PROXY, "192.168.0.0.100:2020"); + auto address = StringToSockaddr("192.168.0.1:3333"); + ASSERT_TRUE(address.ok()) << address.status(); + EXPECT_EQ(HttpProxyMapper().MapAddress(*address, &args), absl::nullopt); + EXPECT_EQ(args.GetString(GRPC_ARG_HTTP_CONNECT_SERVER), absl::nullopt); +} + +class IncludedAddressesTest + : public ::testing::TestWithParam {}; + +INSTANTIATE_TEST_CASE_P(IncludedAddresses, IncludedAddressesTest, + ::testing::Values( + // IP v6 address in a proxied subnet + "[2001:db8:1::1]:2020", + // IP v6 address that is proxied + "[2001:db8:2::5]:2020", + // Proxied IP v4 address + "192.168.1.1:3333", + // IP v4 address in proxied subnet + "192.168.0.1:3333")); + +TEST_P(IncludedAddressesTest, AddressIncluded) { + ScopedEnvVar address_proxy(HttpProxyMapper::kAddressProxyEnvVar, + "[2001:db8::1111]:2020"); + ScopedEnvVar address_proxy_enabled( + HttpProxyMapper::kAddressProxyEnabledAddressesEnvVar, + // Whitespaces added to test that they are ignored as expected + " 192.168.0.0/24 , 192.168.1.1 , 2001:db8:1::0/48 , 2001:db8:2::5"); + auto address = StringToSockaddr(GetParam()); + ASSERT_TRUE(address.ok()) << GetParam() << ": " << address.status(); + ChannelArgs args; + EXPECT_THAT(HttpProxyMapper().MapAddress(*address, &args), + AddressEq("[2001:db8::1111]:2020")); + EXPECT_EQ(args.GetString(GRPC_ARG_HTTP_CONNECT_SERVER), GetParam()); +} + } // namespace } // namespace testing } // namespace grpc_core