Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion api/envoy/config/core/v3/address.proto
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ message ExtraSourceAddress {
SocketOptionsOverride socket_options = 2;
}

// [#next-free-field: 7]
// [#next-free-field: 8]
message BindConfig {
option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.core.BindConfig";

Expand Down Expand Up @@ -180,6 +180,24 @@ message BindConfig {
// <envoy_v3_api_msg_config.upstream.local_address_selector.v3.DefaultLocalAddressSelector>`).
// [#extension-category: envoy.upstream.local_address_selector]
TypedExtensionConfig local_address_selector = 6;

// If set to true, the :ref:`network_namespace_filepath
// <envoy_v3_api_field_config.core.v3.SocketAddress.network_namespace_filepath>` of every source
// address in this bind config is validated when the configuration is loaded, and the
// configuration is rejected if a referenced network namespace cannot be opened.
//
// By default no such validation is performed: a network namespace that does not exist when the
// configuration is loaded may be created later, before connections are actually established, so
// eager validation would wrongly reject such configurations. If a namespace is unavailable when
// a connection is created, the connection attempt fails gracefully.
//
// Listener addresses do not need an equivalent option: a listener creates and binds its socket
// in the configured network namespace when the listener configuration is loaded, so an
// unavailable namespace always rejects the listener configuration.
//
// .. attention::
// Network namespaces are only configurable on Linux. Otherwise, this field has no effect.
bool validate_network_namespaces = 7;
}

// Addresses specify either a logical or physical address and port, which are
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Added a new :ref:`validate_network_namespaces
<envoy_v3_api_field_config.core.v3.BindConfig.validate_network_namespaces>` option to
``BindConfig``. When set, the :ref:`network_namespace_filepath
<envoy_v3_api_field_config.core.v3.SocketAddress.network_namespace_filepath>` of every source
address in the bind config is validated at configuration load time, and the configuration is
rejected if a referenced Linux network namespace cannot be opened.
14 changes: 14 additions & 0 deletions source/common/network/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -800,5 +800,19 @@ ResolvedUdpSocketConfig::ResolvedUdpSocketConfig(
}
}

#if defined(__linux__)
absl::Status Utility::validateNetworkNamespace(absl::string_view netns) {
Api::OsSysCalls& posix = Api::OsSysCallsSingleton::get();
const std::string netns_path(netns);
auto open_result = posix.open(netns_path.c_str(), O_RDONLY);
if (open_result.return_value_ < 0) {
return absl::InvalidArgumentError(fmt::format("failed to open network namespace file {}: {}",
netns_path, errorDetails(open_result.errno_)));
}
posix.close(open_result.return_value_);
return absl::OkStatus();
}
#endif

} // namespace Network
} // namespace Envoy
11 changes: 11 additions & 0 deletions source/common/network/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,17 @@ class Utility {

return result;
}

/**
* Validates that a network namespace referenced by a filepath can be entered, i.e. that the file
* exists and can be opened. This is intended to be used at config-admission time so that a
* misconfigured (e.g. non-existent) network namespace is rejected rather than causing a failure
* deep in the connection/socket creation path.
*
* @param netns filepath referencing the network namespace to validate.
* @return OkStatus if the namespace file can be opened, an error status otherwise.
*/
static absl::Status validateNetworkNamespace(absl::string_view netns);
#endif

private:
Expand Down
25 changes: 25 additions & 0 deletions source/common/upstream/upstream_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
#include "source/common/network/resolver_impl.h"
#include "source/common/network/socket_option_factory.h"
#include "source/common/network/socket_option_impl.h"
#include "source/common/network/utility.h"
#include "source/common/protobuf/protobuf.h"
#include "source/common/protobuf/utility.h"
#include "source/common/router/config_impl.h"
Expand Down Expand Up @@ -248,6 +249,21 @@ buildClusterSocketOptions(const envoy::config::cluster::v3::Cluster& cluster_con
return cluster_options;
}

// Validates that an upstream bind source address with a configured network namespace can actually
// be entered. This is only done at config load time when the bind config opts in via
// `validate_network_namespaces`, so that a misconfigured (e.g. non-existent) network namespace is
// rejected here rather than causing a connection failure later.
absl::Status validateBindNetworkNamespace(const Network::Address::InstanceConstSharedPtr& address) {
#if defined(__linux__)
if (address != nullptr && address->networkNamespace().has_value()) {
return Network::Utility::validateNetworkNamespace(*address->networkNamespace());
}
#else
(void)address;
#endif
return absl::OkStatus();
}

absl::StatusOr<std::vector<::Envoy::Upstream::UpstreamLocalAddress>>
parseBindConfig(::Envoy::OptRef<const envoy::config::core::v3::BindConfig> bind_config,
const std::optional<std::string>& cluster_name,
Expand All @@ -264,6 +280,9 @@ parseBindConfig(::Envoy::OptRef<const envoy::config::core::v3::BindConfig> bind_
::Envoy::Network::Address::resolveProtoSocketAddress(bind_config->source_address());
RETURN_IF_NOT_OK_REF(address_or_error.status());
upstream_local_address.address_ = address_or_error.value();
if (bind_config->validate_network_namespaces()) {
RETURN_IF_NOT_OK(validateBindNetworkNamespace(upstream_local_address.address_));
}
}
upstream_local_address.socket_options_ = std::make_shared<Network::ConnectionSocket::Options>();

Expand All @@ -280,6 +299,9 @@ parseBindConfig(::Envoy::OptRef<const envoy::config::core::v3::BindConfig> bind_
::Envoy::Network::Address::resolveProtoSocketAddress(extra_source_address.address());
RETURN_IF_NOT_OK_REF(address_or_error.status());
extra_upstream_local_address.address_ = address_or_error.value();
if (bind_config->validate_network_namespaces()) {
RETURN_IF_NOT_OK(validateBindNetworkNamespace(extra_upstream_local_address.address_));
}

extra_upstream_local_address.socket_options_ =
std::make_shared<::Envoy::Network::ConnectionSocket::Options>();
Expand All @@ -304,6 +326,9 @@ parseBindConfig(::Envoy::OptRef<const envoy::config::core::v3::BindConfig> bind_
::Envoy::Network::Address::resolveProtoSocketAddress(additional_source_address);
RETURN_IF_NOT_OK_REF(address_or_error.status());
additional_upstream_local_address.address_ = address_or_error.value();
if (bind_config->validate_network_namespaces()) {
RETURN_IF_NOT_OK(validateBindNetworkNamespace(additional_upstream_local_address.address_));
}
additional_upstream_local_address.socket_options_ =
std::make_shared<::Envoy::Network::ConnectionSocket::Options>();
::Envoy::Network::Socket::appendOptions(additional_upstream_local_address.socket_options_,
Expand Down
27 changes: 27 additions & 0 deletions test/common/network/utility_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,33 @@ TEST_F(ExecInNetnsTest, FailtoReturnToOriginalNetns) {
},
"failed to restore original netns .*");
}

TEST_F(ExecInNetnsTest, ValidateNetworkNamespaceSuccess) {
testing::StrictMock<Api::MockOsSysCalls> os_syscalls;
TestThreadsafeSingletonInjector<Api::OsSysCallsImpl> os_calls(&os_syscalls);

EXPECT_CALL(os_syscalls, open(_, O_RDONLY))
.WillOnce(Invoke([](const char*, int) -> Api::SysCallIntResult { return {42, 0}; }));
EXPECT_CALL(os_syscalls, close(42)).WillOnce(Invoke([](int) -> Api::SysCallIntResult {
return {0, 0};
}));

EXPECT_TRUE(Utility::validateNetworkNamespace("/var/run/netns/ns1").ok());
}

TEST_F(ExecInNetnsTest, ValidateNetworkNamespaceOpenFail) {
testing::StrictMock<Api::MockOsSysCalls> os_syscalls;
TestThreadsafeSingletonInjector<Api::OsSysCallsImpl> os_calls(&os_syscalls);

// open() fails (e.g. the namespace does not exist). No close() is expected.
EXPECT_CALL(os_syscalls, open(_, O_RDONLY))
.WillOnce(Invoke([](const char*, int) -> Api::SysCallIntResult { return {-1, -1}; }));

auto status = Utility::validateNetworkNamespace("/var/run/netns/does_not_exist");
EXPECT_FALSE(status.ok());
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_TRUE(status.message().starts_with("failed to open network namespace file"));
}
#endif

} // namespace
Expand Down
59 changes: 59 additions & 0 deletions test/common/upstream/upstream_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3996,6 +3996,65 @@ TEST_F(StaticClusterImplTest, SourceAddressPriorityWitExtraSourceAddress) {
}
}

#if defined(__linux__)
// With validate_network_namespaces set, a cluster whose upstream bind config references a
// non-existent network namespace is rejected at config load time.
TEST_F(StaticClusterImplTest, UpstreamBindConfigInvalidNetworkNamespace) {
envoy::config::cluster::v3::Cluster config;
config.set_name("staticcluster");
config.mutable_connect_timeout();
config.mutable_upstream_bind_config()->set_validate_network_namespaces(true);
auto* source_address = config.mutable_upstream_bind_config()->mutable_source_address();
source_address->set_address("1.2.3.4");
source_address->set_port_value(0);
source_address->set_network_namespace_filepath("/run/netns/envoy_does_not_exist_test_ns");

Envoy::Upstream::ClusterFactoryContextImpl factory_context(server_context_, nullptr, nullptr,
false);
EXPECT_THROW_WITH_REGEX(std::shared_ptr<StaticClusterImpl> cluster =
createCluster(config, factory_context),
EnvoyException, "failed to open network namespace file");
}

// Without validate_network_namespaces, the invalid network namespace is not validated at config
// load time, so cluster creation succeeds (preserving the pre-existing behavior).
TEST_F(StaticClusterImplTest, UpstreamBindConfigInvalidNetworkNamespaceNotValidated) {
envoy::config::cluster::v3::Cluster config;
config.set_name("staticcluster");
config.mutable_connect_timeout();
auto* source_address = config.mutable_upstream_bind_config()->mutable_source_address();
source_address->set_address("1.2.3.4");
source_address->set_port_value(0);
source_address->set_network_namespace_filepath("/run/netns/envoy_does_not_exist_test_ns");

Envoy::Upstream::ClusterFactoryContextImpl factory_context(server_context_, nullptr, nullptr,
false);
EXPECT_NO_THROW(createCluster(config, factory_context));
}

// With validate_network_namespaces set, extra source addresses are validated as well.
TEST_F(StaticClusterImplTest, UpstreamBindConfigInvalidNetworkNamespaceExtraSourceAddress) {
envoy::config::cluster::v3::Cluster config;
config.set_name("staticcluster");
config.mutable_connect_timeout();
config.mutable_upstream_bind_config()->set_validate_network_namespaces(true);
auto* source_address = config.mutable_upstream_bind_config()->mutable_source_address();
source_address->set_address("1.2.3.4");
source_address->set_port_value(0);
auto* extra_source_address =
config.mutable_upstream_bind_config()->add_extra_source_addresses()->mutable_address();
extra_source_address->set_address("2001::1");
extra_source_address->set_port_value(0);
extra_source_address->set_network_namespace_filepath("/run/netns/envoy_does_not_exist_test_ns");

Envoy::Upstream::ClusterFactoryContextImpl factory_context(server_context_, nullptr, nullptr,
false);
EXPECT_THROW_WITH_REGEX(std::shared_ptr<StaticClusterImpl> cluster =
createCluster(config, factory_context),
EnvoyException, "failed to open network namespace file");
}
#endif

TEST_F(StaticClusterImplTest, SourceAddressPriorityWithDeprecatedAdditionalSourceAddress) {
envoy::config::cluster::v3::Cluster config;
config.set_name("staticcluster");
Expand Down