[Proxy] Support for setting proxy for addresses (#34617)

pull/34737/head
Eugene Ostroukhov 1 year ago committed by GitHub
parent 74c1de6512
commit 1a76e7cb42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      build_autogenerated.yaml
  2. 10
      include/grpc/impl/channel_arg_names.h
  3. 113
      src/core/ext/filters/client_channel/http_proxy_mapper.cc
  4. 9
      src/core/ext/filters/client_channel/http_proxy_mapper.h
  5. 1
      test/core/client_channel/BUILD
  6. 126
      test/core/client_channel/http_proxy_mapper_test.cc

@ -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:

@ -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"

@ -26,7 +26,6 @@
#include <memory>
#include <string>
#include <utility>
#include <vector>
#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<absl::string_view, absl::string_view> 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<grpc_resolved_address>& 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<std::string> 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<grpc_resolved_address> 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<std::string> HttpProxyMapper::MapName(
@ -191,7 +234,6 @@ absl::optional<std::string> 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<std::string> HttpProxyMapper::MapName(
"host '%s'",
std::string(server_uri).c_str());
} else {
std::vector<absl::string_view> 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<grpc_resolved_address>(*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<std::string> HttpProxyMapper::MapName(
return name_to_resolve;
}
absl::optional<grpc_resolved_address> 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 */,

@ -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<std::string> MapName(absl::string_view server_uri,
ChannelArgs* args) override;
absl::optional<grpc_resolved_address> 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);

@ -43,6 +43,7 @@ grpc_cc_test(
"//:grpc",
"//src/core:channel_args",
"//test/core/util:grpc_test_util",
"//test/core/util:scoped_env_var",
],
)

@ -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 <grpc/impl/channel_arg_names.h>
#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<absl::string_view> {};
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

Loading…
Cancel
Save