Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: add config to enable looking up local interface name for upstream connections #19758

Merged
merged 22 commits into from
Mar 16, 2022
Merged
5 changes: 5 additions & 0 deletions api/envoy/config/cluster/v3/cluster.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,11 @@ message UpstreamConnectionOptions {

// If set then set SO_KEEPALIVE on the socket to enable TCP Keepalives.
core.v3.TcpKeepalive tcp_keepalive = 1;

// If enabled, associates the interface name of the local address with the upstream connection.
// This can be used by extensions during processing of requests. The association mechanism is
// implementation specific. Defaults to false due to performance concerns.
bool set_local_interface_name_on_upstream_connections = 2;
junr03 marked this conversation as resolved.
Show resolved Hide resolved
}

message TrackClusterStats {
Expand Down
1 change: 1 addition & 0 deletions envoy/network/connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ class Connection : public Event::DeferredDeletable,
/**
* @return the connection info provider backing this connection.
*/
virtual ConnectionInfoSetter& connectionInfoSetter() PURE;
virtual const ConnectionInfoProvider& connectionInfoProvider() const PURE;
virtual ConnectionInfoProviderSharedPtr connectionInfoProviderSharedPtr() const PURE;

Expand Down
8 changes: 7 additions & 1 deletion envoy/network/socket.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,17 @@ class ConnectionInfoSetter : public ConnectionInfoProvider {
**/
virtual void setConnectionID(uint64_t id) PURE;

/**
* @param enable whether to enable or disable setting interface name. While having an interface
* name might be helpful for debugging, it might come at a performance cost.
*/
virtual void enableSettingInterfaceName(const bool enable) PURE;

/**
* @param interface_name the name of the network interface used by the local end of the
*connection.
junr03 marked this conversation as resolved.
Show resolved Hide resolved
**/
virtual void setInterfaceName(absl::string_view interface_name) PURE;
virtual void maybeSetInterfaceName(IoHandle& io_handle) PURE;

/**
* @param connection_info sets the downstream ssl connection.
Expand Down
5 changes: 5 additions & 0 deletions envoy/upstream/upstream.h
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,11 @@ class ClusterInfo {
*/
virtual bool warmHosts() const PURE;

/**
* @return true if this cluster is configured to set local interface name on upstream connections.
*/
virtual bool setLocalInterfaceNameOnUpstreamConnections() const PURE;

/**
* @return eds cluster service_name of the cluster.
*/
Expand Down
5 changes: 2 additions & 3 deletions source/common/network/connection_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -940,12 +940,11 @@ void ClientConnectionImpl::onConnected() {
stream_info_.upstreamInfo()->upstreamTiming().onUpstreamConnectComplete(dispatcher_.timeSource());
// There are no meaningful socket source address semantics for non-IP sockets, so skip.
if (socket_->connectionInfoProviderSharedPtr()->remoteAddress()->ip()) {
// interfaceName makes a syscall. Call once to minimize perf hit.
const auto maybe_interface_name = ioHandle().interfaceName();
socket_->connectionInfoProvider().maybeSetInterfaceName(ioHandle());
junr03 marked this conversation as resolved.
Show resolved Hide resolved
const auto maybe_interface_name = socket_->connectionInfoProvider().interfaceName();
if (maybe_interface_name.has_value()) {
ENVOY_CONN_LOG_EVENT(debug, "conn_interface", "connected on local interface '{}'", *this,
maybe_interface_name.value());
socket_->connectionInfoProvider().setInterfaceName(maybe_interface_name.value());
}
}
ConnectionImpl::onConnected();
Expand Down
3 changes: 3 additions & 0 deletions source/common/network/connection_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ class ConnectionImpl : public ConnectionImplBase, public TransportSocketCallback
void readDisable(bool disable) override;
void detectEarlyCloseWhenReadDisabled(bool value) override { detect_early_close_ = value; }
bool readEnabled() const override;
ConnectionInfoSetter& connectionInfoSetter() override {
return socket_->connectionInfoProvider();
}
const ConnectionInfoProvider& connectionInfoProvider() const override {
return socket_->connectionInfoProvider();
}
Expand Down
4 changes: 4 additions & 0 deletions source/common/network/happy_eyeballs_connection_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ bool HappyEyeballsConnectionImpl::readEnabled() const {
return connections_[0]->readEnabled();
}

ConnectionInfoSetter& HappyEyeballsConnectionImpl::connectionInfoSetter() {
return connections_[0]->connectionInfoSetter();
}

const ConnectionInfoProvider& HappyEyeballsConnectionImpl::connectionInfoProvider() const {
return connections_[0]->connectionInfoProvider();
}
Expand Down
2 changes: 2 additions & 0 deletions source/common/network/happy_eyeballs_connection_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ class HappyEyeballsConnectionImpl : public ClientConnection,
bool isHalfCloseEnabled() override;
std::string nextProtocol() const override;
// Note, this might change before connect finishes.
ConnectionInfoSetter& connectionInfoSetter() override;
// Note, this might change before connect finishes.
junr03 marked this conversation as resolved.
Show resolved Hide resolved
const ConnectionInfoProvider& connectionInfoProvider() const override;
// Note, this might change before connect finishes.
ConnectionInfoProviderSharedPtr connectionInfoProviderSharedPtr() const override;
Expand Down
10 changes: 8 additions & 2 deletions source/common/network/socket_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,13 @@ class ConnectionInfoSetterImpl : public ConnectionInfoSetter {
absl::optional<uint64_t> connectionID() const override { return connection_id_; }
void setConnectionID(uint64_t id) override { connection_id_ = id; }
absl::optional<absl::string_view> interfaceName() const override { return interface_name_; }
void setInterfaceName(absl::string_view interface_name) override {
interface_name_ = std::string(interface_name);
void enableSettingInterfaceName(const bool enable) override {
allow_syscall_for_interface_name_ = enable;
}
void maybeSetInterfaceName(IoHandle& io_handle) override {
if (allow_syscall_for_interface_name_) {
interface_name_ = io_handle.interfaceName();
}
}
Ssl::ConnectionInfoConstSharedPtr sslConnection() const override { return ssl_info_; }
void setSslConnection(const Ssl::ConnectionInfoConstSharedPtr& ssl_connection_info) override {
Expand All @@ -71,6 +76,7 @@ class ConnectionInfoSetterImpl : public ConnectionInfoSetter {
Address::InstanceConstSharedPtr direct_remote_address_;
std::string server_name_;
absl::optional<uint64_t> connection_id_;
bool allow_syscall_for_interface_name_{false};
absl::optional<std::string> interface_name_;
Ssl::ConnectionInfoConstSharedPtr ssl_info_;
std::string ja3_hash_;
Expand Down
3 changes: 3 additions & 0 deletions source/common/quic/client_connection_factory_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ std::unique_ptr<Network::ClientConnection> createQuicNetworkConnection(
Network::Address::InstanceConstSharedPtr server_addr,
Network::Address::InstanceConstSharedPtr local_addr, QuicStatNames& quic_stat_names,
OptRef<Http::AlternateProtocolsCache> rtt_cache, Stats::Scope& scope) {
// TODO: Quic should take into account the set_local_interface_name_on_upstream_connections config
// and call maybeSetInterfaceName based on that upon acquiring a local socket.
// Similar to what is done in ClientConnectionImpl::onConnected().
ASSERT(crypto_config != nullptr);
PersistentQuicInfoImpl* info_impl = reinterpret_cast<PersistentQuicInfoImpl*>(&info);
quic::ParsedQuicVersionVector quic_versions = quic::CurrentSupportedHttp3Versions();
Expand Down
5 changes: 5 additions & 0 deletions source/common/quic/quic_filter_manager_connection_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ class QuicFilterManagerConnectionImpl : public Network::ConnectionImplBase,
void readDisable(bool /*disable*/) override { ASSERT(false); }
void detectEarlyCloseWhenReadDisabled(bool /*value*/) override { ASSERT(false); }
bool readEnabled() const override { return true; }
Network::ConnectionInfoSetter& connectionInfoSetter() override {
ENVOY_BUG(network_connection_ && network_connection_->connectionSocket(),
"No connection socket.");
return network_connection_->connectionSocket()->connectionInfoProvider();
}
const Network::ConnectionInfoSetter& connectionInfoProvider() const override {
ENVOY_BUG(network_connection_ && network_connection_->connectionSocket(),
"No connection socket.");
Expand Down
11 changes: 11 additions & 0 deletions source/common/upstream/upstream_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,8 @@ Network::ClientConnectionPtr HostImpl::createConnection(
socket_factory.createTransportSocket(std::move(transport_socket_options)),
connection_options);

connection->connectionInfoSetter().enableSettingInterfaceName(
cluster.setLocalInterfaceNameOnUpstreamConnections());
connection->setBufferLimits(cluster.perConnectionBufferLimitBytes());
cluster.createNetworkFilterChain(*connection);
return connection;
Expand Down Expand Up @@ -843,13 +845,22 @@ ClusterInfoImpl::ClusterInfoImpl(
config.connection_pool_per_downstream_connection()),
warm_hosts_(!config.health_checks().empty() &&
common_lb_config_.ignore_new_hosts_until_first_hc()),
set_local_interface_name_on_upstream_connections_(
config.upstream_connection_options().set_local_interface_name_on_upstream_connections()),
cluster_type_(
config.has_cluster_type()
? absl::make_optional<envoy::config::cluster::v3::Cluster::CustomClusterType>(
config.cluster_type())
: absl::nullopt),
factory_context_(
std::make_unique<FactoryContextImpl>(*stats_scope_, runtime, factory_context)) {
#ifdef WIN32
if (set_local_interface_name_on_upstream_connections_) {
throw EnvoyException("set_local_interface_name_on_upstream_connections_ cannot be set to true "
"on Windows platforms");
}
#endif

if (config.has_max_requests_per_connection() &&
http_protocol_options_->common_http_protocol_options_.has_max_requests_per_connection()) {
throw EnvoyException("Only one of max_requests_per_connection from Cluster or "
Expand Down
4 changes: 4 additions & 0 deletions source/common/upstream/upstream_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,9 @@ class ClusterInfoImpl : public ClusterInfo,
return connection_pool_per_downstream_connection_;
}
bool warmHosts() const override { return warm_hosts_; }
bool setLocalInterfaceNameOnUpstreamConnections() const override {
return set_local_interface_name_on_upstream_connections_;
}
const absl::optional<envoy::config::core::v3::UpstreamHttpProtocolOptions>&
upstreamHttpProtocolOptions() const override {
return http_protocol_options_->upstream_http_protocol_options_;
Expand Down Expand Up @@ -821,6 +824,7 @@ class ClusterInfoImpl : public ClusterInfo,
const bool drain_connections_on_host_removal_;
const bool connection_pool_per_downstream_connection_;
const bool warm_hosts_;
const bool set_local_interface_name_on_upstream_connections_;
const absl::optional<envoy::config::core::v3::UpstreamHttpProtocolOptions>
upstream_http_protocol_options_;
absl::optional<std::string> eds_service_name_;
Expand Down
5 changes: 4 additions & 1 deletion source/server/api_listener_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,10 @@ class ApiListenerImplBase : public ApiListener,
IS_ENVOY_BUG("Unexpected function call");
}
bool readEnabled() const override { return true; }
const Network::ConnectionInfoSetter& connectionInfoProvider() const override {
Network::ConnectionInfoSetter& connectionInfoSetter() override {
return *connection_info_provider_;
}
const Network::ConnectionInfoProvider& connectionInfoProvider() const override {
return *connection_info_provider_;
}
Network::ConnectionInfoProviderSharedPtr connectionInfoProviderSharedPtr() const override {
Expand Down
4 changes: 2 additions & 2 deletions test/common/network/happy_eyeballs_connection_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -980,8 +980,8 @@ TEST_F(HappyEyeballsConnectionImplTest, NextProtocol) {
TEST_F(HappyEyeballsConnectionImplTest, AddressProvider) {
connectFirstAttempt();

const ConnectionInfoSetterImpl provider(std::make_shared<Address::Ipv4Instance>(80),
std::make_shared<Address::Ipv4Instance>(80));
ConnectionInfoSetterImpl provider(std::make_shared<Address::Ipv4Instance>(80),
std::make_shared<Address::Ipv4Instance>(80));
EXPECT_CALL(*created_connections_[0], connectionInfoProvider()).WillOnce(ReturnRef(provider));
impl_->connectionInfoProvider();
}
Expand Down
1 change: 1 addition & 0 deletions test/common/tcp/conn_pool_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ class TcpConnPoolImplDestructorTest : public Event::TestUsingSimulatedTime,

void prepareConn() {
connection_ = new StrictMock<Network::MockClientConnection>();
EXPECT_CALL(*connection_, connectionInfoSetter());
EXPECT_CALL(*connection_, transportFailureReason()).Times(AtLeast(0));
EXPECT_CALL(*connection_, setBufferLimits(0));
EXPECT_CALL(*connection_, detectEarlyCloseWhenReadDisabled(false));
Expand Down
30 changes: 29 additions & 1 deletion test/common/upstream/cluster_manager_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2018,7 +2018,7 @@ TEST_F(ClusterManagerImplTest, DynamicAddRemove) {
EXPECT_EQ(cp2, TcpPoolDataPeer::getPool(cluster_manager_->getThreadLocalCluster("fake_cluster")
->tcpConnPool(ResourcePriority::Default, nullptr)));

Network::MockClientConnection* connection = new Network::MockClientConnection();
Network::MockClientConnection* connection = new NiceMock<Network::MockClientConnection>();
ON_CALL(*cluster2->info_, features())
.WillByDefault(Return(ClusterInfo::Features::CLOSE_CONNECTIONS_ON_HOST_HEALTH_FAILURE));
EXPECT_CALL(factory_.tls_.dispatcher_, createClientConnection_(_, _, _, _))
Expand Down Expand Up @@ -5643,6 +5643,34 @@ TEST_F(ClusterManagerImplTest, CheckActiveStaticCluster) {
EnvoyException, "gRPC client cluster 'added_via_api' is not static");
}

#ifdef WIN32
TEST_F(ClusterManagerImplTest, LocalInterfaceNameForUpstreamConnectionThrowsInWin32) {
const std::string yaml = fmt::format(R"EOF(
static_resources:
clusters:
- name: cluster_1
connect_timeout: 0.25s
type: STATIC
lb_policy: ROUND_ROBIN
upstream_connection_options:
set_local_interface_name_on_upstream_connections: true
load_assignment:
cluster_name: cluster_1
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 5678
)EOF");

EXPECT_THROW_WITH_MESSAGE(create(parseBootstrapFromV3Yaml(yaml)), EnvoyException,
"set_local_interface_name_on_upstream_connections_ cannot be set to "
"true on Windows platforms");
}
#endif

class PreconnectTest : public ClusterManagerImplTest {
public:
void initialize(float ratio) {
Expand Down
3 changes: 2 additions & 1 deletion test/common/upstream/upstream_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1336,7 +1336,7 @@ TEST_F(HostImplTest, CreateConnection) {
auto connection = new testing::StrictMock<Network::MockClientConnection>();
EXPECT_CALL(*connection, setBufferLimits(0));
EXPECT_CALL(dispatcher, createClientConnection_(_, _, _, _)).WillOnce(Return(connection));

EXPECT_CALL(*connection, connectionInfoSetter());
Envoy::Upstream::Host::CreateConnectionData connection_data =
host->createConnection(dispatcher, options, transport_socket_options);
EXPECT_EQ(connection, connection_data.connection_.get());
Expand Down Expand Up @@ -1373,6 +1373,7 @@ TEST_F(HostImplTest, CreateConnectionHappyEyeballs) {
auto connection = new testing::StrictMock<Network::MockClientConnection>();
EXPECT_CALL(*connection, setBufferLimits(0));
EXPECT_CALL(*connection, addConnectionCallbacks(_));
EXPECT_CALL(*connection, connectionInfoSetter());
// The underlying connection should be created with the first address in the list.
EXPECT_CALL(dispatcher, createClientConnection_(address_list[0], _, _, _))
.WillOnce(Return(connection));
Expand Down
8 changes: 8 additions & 0 deletions test/integration/filters/stream_info_to_headers_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,17 @@ class StreamInfoToHeadersFilter : public Http::PassThroughFilter {
Http::LowerCaseString("alpn"),
decoder_callbacks_->streamInfo().upstreamInfo()->upstreamSslConnection()->alpn());
}

headers.addCopy(Http::LowerCaseString("num_streams"),
decoder_callbacks_->streamInfo().upstreamInfo()->upstreamNumStreams());

const auto maybe_local_interface_name =
decoder_callbacks_->streamInfo().upstreamInfo()->upstreamInterfaceName();
if (maybe_local_interface_name.has_value()) {
headers.addCopy(Http::LowerCaseString("local_interface_name"),
maybe_local_interface_name.value());
}

StreamInfo::UpstreamTiming& upstream_timing =
decoder_callbacks_->streamInfo().upstreamInfo()->upstreamTiming();
if (upstream_timing.upstream_connect_start_.has_value()) {
Expand Down
65 changes: 65 additions & 0 deletions test/integration/protocol_integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3628,6 +3628,71 @@ TEST_P(ProtocolIntegrationTest, HandleUpstreamSocketFail) {
test_server_.reset();
}

TEST_P(ProtocolIntegrationTest, NoLocalInterfaceNameForUpstreamConnection) {
config_helper_.prependFilter(R"EOF(
name: stream-info-to-headers-filter
typed_config:
"@type": type.googleapis.com/google.protobuf.Empty
)EOF");
initialize();

codec_client_ = makeHttpConnection(lookupPort("http"));

// Send a headers only request.
auto response = codec_client_->makeHeaderOnlyRequest(default_request_headers_);
waitForNextUpstreamRequest();

// Make sure that the body was injected to the request.
EXPECT_TRUE(upstream_request_->complete());

// Send a headers only response.
upstream_request_->encodeHeaders(default_response_headers_, true);
ASSERT_TRUE(response->waitForEndStream());

// Make sure that the local interface name was not populated. This is the runtime default.
EXPECT_TRUE(response->headers().get(Http::LowerCaseString("local_interface_name")).empty());
}

// WIN32 fails configuration and terminates the server.
#ifndef WIN32
TEST_P(ProtocolIntegrationTest, LocalInterfaceNameForUpstreamConnection) {
config_helper_.prependFilter(R"EOF(
name: stream-info-to-headers-filter
typed_config:
"@type": type.googleapis.com/google.protobuf.Empty
)EOF");

config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) {
bootstrap.mutable_static_resources()
->mutable_clusters(0)
->mutable_upstream_connection_options()
->set_set_local_interface_name_on_upstream_connections(true);
});
initialize();

codec_client_ = makeHttpConnection(lookupPort("http"));

// Send a headers only request.
auto response = codec_client_->makeHeaderOnlyRequest(default_request_headers_);
waitForNextUpstreamRequest();

// Make sure that the body was injected to the request.
EXPECT_TRUE(upstream_request_->complete());

// Send a headers only response.
upstream_request_->encodeHeaders(default_response_headers_, true);
ASSERT_TRUE(response->waitForEndStream());

// Make sure that the local interface name was populated due to runtime override.
// TODO: h3 upstreams don't have local interface name
if (GetParam().upstream_protocol == Http::CodecType::HTTP3) {
junr03 marked this conversation as resolved.
Show resolved Hide resolved
EXPECT_TRUE(response->headers().get(Http::LowerCaseString("local_interface_name")).empty());
} else {
EXPECT_FALSE(response->headers().get(Http::LowerCaseString("local_interface_name")).empty());
}
}
#endif

#ifdef NDEBUG
// These tests send invalid request and response header names which violate ASSERT while creating
// such request/response headers. So they can only be run in NDEBUG mode.
Expand Down
2 changes: 2 additions & 0 deletions test/mocks/network/connection.cc
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ void MockConnectionBase::runLowWatermarkCallbacks() {
}

template <class T> static void initializeMockConnection(T& connection) {
ON_CALL(connection, connectionInfoSetter())
.WillByDefault(ReturnRef(*connection.stream_info_.downstream_connection_info_provider_));
ON_CALL(connection, connectionInfoProvider())
.WillByDefault(ReturnPointee(connection.stream_info_.downstream_connection_info_provider_));
ON_CALL(connection, connectionInfoProviderSharedPtr())
Expand Down
1 change: 1 addition & 0 deletions test/mocks/network/connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class MockConnectionBase {
MOCK_METHOD(void, readDisable, (bool disable)); \
MOCK_METHOD(void, detectEarlyCloseWhenReadDisabled, (bool)); \
MOCK_METHOD(bool, readEnabled, (), (const)); \
MOCK_METHOD(ConnectionInfoSetter&, connectionInfoSetter, ()); \
MOCK_METHOD(const ConnectionInfoProvider&, connectionInfoProvider, (), (const)); \
MOCK_METHOD(ConnectionInfoProviderSharedPtr, connectionInfoProviderSharedPtr, (), (const)); \
MOCK_METHOD(absl::optional<Connection::UnixDomainSocketPeerCredentials>, \
Expand Down
1 change: 1 addition & 0 deletions test/mocks/upstream/cluster_info.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ class MockClusterInfo : public ClusterInfo {
MOCK_METHOD(bool, drainConnectionsOnHostRemoval, (), (const));
MOCK_METHOD(bool, connectionPoolPerDownstreamConnection, (), (const));
MOCK_METHOD(bool, warmHosts, (), (const));
MOCK_METHOD(bool, setLocalInterfaceNameOnUpstreamConnections, (), (const));
MOCK_METHOD(const absl::optional<envoy::config::core::v3::UpstreamHttpProtocolOptions>&,
upstreamHttpProtocolOptions, (), (const));
MOCK_METHOD(const absl::optional<envoy::config::core::v3::AlternateProtocolsCacheOptions>&,
Expand Down