diff --git a/source/common/config/lds_json.cc b/source/common/config/lds_json.cc index e0acbbe6122b7..699231888fcc2 100644 --- a/source/common/config/lds_json.cc +++ b/source/common/config/lds_json.cc @@ -56,6 +56,7 @@ void LdsJson::translateListener(const Json::Object& json_listener, JSON_UTIL_SET_BOOL(json_listener, listener, use_original_dst); JSON_UTIL_SET_BOOL(json_listener, *listener.mutable_deprecated_v1(), bind_to_port); JSON_UTIL_SET_INTEGER(json_listener, listener, per_connection_buffer_limit_bytes); + JSON_UTIL_SET_BOOL(json_listener, listener, transparent); } } // namespace Config diff --git a/source/common/json/config_schemas.cc b/source/common/json/config_schemas.cc index 218890638ba8d..687468d6188ea 100644 --- a/source/common/json/config_schemas.cc +++ b/source/common/json/config_schemas.cc @@ -185,6 +185,7 @@ const std::string Json::Schema::LISTENER_SCHEMA(R"EOF( }, "drain_type": {"type" : "string", "enum" : ["default", "modify_only"]}, "ssl_context" : {"$ref" : "#/definitions/ssl_context"}, + "transparent" : {"type": "boolean"}, "bind_to_port" : {"type": "boolean"}, "use_proxy_proto" : {"type" : "boolean"}, "use_original_dst" : {"type" : "boolean"}, diff --git a/source/common/network/listen_socket_impl.h b/source/common/network/listen_socket_impl.h index 71effb8914803..f472ff337f628 100644 --- a/source/common/network/listen_socket_impl.h +++ b/source/common/network/listen_socket_impl.h @@ -81,7 +81,6 @@ class ConnectionSocketImpl : public SocketImpl, public ConnectionSocket { const Address::InstanceConstSharedPtr& remoteAddress() const override { return remote_address_; } void setLocalAddress(const Address::InstanceConstSharedPtr& local_address, bool restored) override { - ASSERT(!restored || *local_address != *local_address_); local_address_ = local_address; local_address_restored_ = restored; } diff --git a/source/common/network/socket_option_impl.cc b/source/common/network/socket_option_impl.cc index 43a7aca454258..056af399df927 100644 --- a/source/common/network/socket_option_impl.cc +++ b/source/common/network/socket_option_impl.cc @@ -10,6 +10,17 @@ namespace Envoy { namespace Network { bool SocketOptionImpl::setOption(Socket& socket, Socket::SocketState state) const { + if (transparent_.has_value()) { + const int should_transparent = transparent_.value() ? 1 : 0; + const int error = + setIpSocketOption(socket, ENVOY_SOCKET_IP_TRANSPARENT, ENVOY_SOCKET_IPV6_TRANSPARENT, + &should_transparent, sizeof(should_transparent)); + if (error != 0) { + ENVOY_LOG(warn, "Setting IP_TRANSPARENT on listener socket failed: {}", strerror(error)); + return false; + } + } + if (state == Socket::SocketState::PreBind) { if (freebind_.has_value()) { const int should_freebind = freebind_.value() ? 1 : 0; diff --git a/source/common/network/socket_option_impl.h b/source/common/network/socket_option_impl.h index 590dcc2e846e2..766e30ce9e2be 100644 --- a/source/common/network/socket_option_impl.h +++ b/source/common/network/socket_option_impl.h @@ -17,6 +17,18 @@ namespace Network { // on a platform, we can make this the empty value. This allows us to avoid proliferation of #ifdef. typedef absl::optional SocketOptionName; +#ifdef IP_TRANSPARENT +#define ENVOY_SOCKET_IP_TRANSPARENT Network::SocketOptionName(IP_TRANSPARENT) +#else +#define ENVOY_SOCKET_IP_TRANSPARENT Network::SocketOptionName() +#endif + +#ifdef IPV6_TRANSPARENT +#define ENVOY_SOCKET_IPV6_TRANSPARENT Network::SocketOptionName(IPV6_TRANSPARENT) +#else +#define ENVOY_SOCKET_IPV6_TRANSPARENT Network::SocketOptionName() +#endif + #ifdef IP_FREEBIND #define ENVOY_SOCKET_IP_FREEBIND Network::SocketOptionName(IP_FREEBIND) #else @@ -31,7 +43,8 @@ typedef absl::optional SocketOptionName; class SocketOptionImpl : public Socket::Option, Logger::Loggable { public: - SocketOptionImpl(absl::optional freebind) : freebind_(freebind) {} + SocketOptionImpl(absl::optional transparent, absl::optional freebind) + : transparent_(transparent), freebind_(freebind) {} // Socket::Option bool setOption(Socket& socket, Socket::SocketState state) const override; @@ -57,6 +70,7 @@ class SocketOptionImpl : public Socket::Option, Logger::Loggable transparent_; const absl::optional freebind_; }; diff --git a/source/common/upstream/upstream_impl.cc b/source/common/upstream/upstream_impl.cc index 2e948854626ad..de7a30e408f06 100644 --- a/source/common/upstream/upstream_impl.cc +++ b/source/common/upstream/upstream_impl.cc @@ -75,7 +75,9 @@ uint64_t parseFeatures(const envoy::api::v2::Cluster& config, class UpstreamSocketOption : public Network::SocketOptionImpl { public: UpstreamSocketOption(const ClusterInfo& cluster_info) - : Network::SocketOptionImpl(cluster_info.features() & ClusterInfo::Features::FREEBIND) {} + : Network::SocketOptionImpl({}, cluster_info.features() & ClusterInfo::Features::FREEBIND + ? true + : absl::optional{}) {} }; } // namespace diff --git a/source/extensions/filters/listener/original_dst/original_dst.cc b/source/extensions/filters/listener/original_dst/original_dst.cc index 9c0cfc391798e..0d343859e1162 100644 --- a/source/extensions/filters/listener/original_dst/original_dst.cc +++ b/source/extensions/filters/listener/original_dst/original_dst.cc @@ -26,7 +26,7 @@ Network::FilterStatus OriginalDstFilter::onAccept(Network::ListenerFilterCallbac // connections that are NOT redirected using iptables. If a connection was not redirected, // the address returned by getOriginalDst() matches the local address of the new socket. // In this case the listener handles the connection directly and does not hand it off. - if (original_local_address && (*original_local_address != local_address)) { + if (original_local_address) { // Restore the local address to the original one. socket.setLocalAddress(original_local_address, true); } diff --git a/source/server/BUILD b/source/server/BUILD index e0e47e1ea7635..293f396f4166e 100644 --- a/source/server/BUILD +++ b/source/server/BUILD @@ -201,6 +201,7 @@ envoy_cc_library( "//include/envoy/server:listener_manager_interface", "//include/envoy/server:transport_socket_config_interface", "//include/envoy/server:worker_interface", + "//source/common/api:os_sys_calls_lib", "//source/common/config:utility_lib", "//source/common/config:well_known_names", "//source/common/network:listen_socket_lib", diff --git a/source/server/listener_manager_impl.cc b/source/server/listener_manager_impl.cc index 16bd62b23e77b..4ff17b71581fd 100644 --- a/source/server/listener_manager_impl.cc +++ b/source/server/listener_manager_impl.cc @@ -3,6 +3,7 @@ #include "envoy/registry/registry.h" #include "envoy/server/transport_socket_config.h" +#include "common/api/os_sys_calls_impl.h" #include "common/common/assert.h" #include "common/common/fmt.h" #include "common/config/utility.h" @@ -111,7 +112,9 @@ ProdListenerComponentFactory::createDrainManager(envoy::api::v2::Listener::Drain class ListenerSocketOption : public Network::SocketOptionImpl { public: ListenerSocketOption(const envoy::api::v2::Listener& config) - : Network::SocketOptionImpl(config.freebind().value()) {} + : Network::SocketOptionImpl( + PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, transparent, absl::optional{}), + PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, freebind, absl::optional{})) {} }; ListenerImpl::ListenerImpl(const envoy::api::v2::Listener& config, ListenerManagerImpl& parent, @@ -136,9 +139,7 @@ ListenerImpl::ListenerImpl(const envoy::api::v2::Listener& config, ListenerManag ASSERT(config.filter_chains().size() >= 1); // Add listen socket options from the config. - if (config.has_freebind()) { - addListenSocketOption(std::make_unique(config)); - } + addListenSocketOption(std::make_unique(config)); if (!config.listener_filters().empty()) { listener_filter_factories_ = diff --git a/test/common/network/socket_option_impl_test.cc b/test/common/network/socket_option_impl_test.cc index 83343455dd636..fcdbbba7c5fc1 100644 --- a/test/common/network/socket_option_impl_test.cc +++ b/test/common/network/socket_option_impl_test.cc @@ -33,20 +33,45 @@ TEST_F(SocketOptionImplTest, BadFd) { // Nop when there are no socket options set. TEST_F(SocketOptionImplTest, SetOptionEmptyNop) { - SocketOptionImpl socket_option{{}}; + SocketOptionImpl socket_option{{}, {}}; EXPECT_TRUE(socket_option.setOption(socket_, Socket::SocketState::PreBind)); EXPECT_TRUE(socket_option.setOption(socket_, Socket::SocketState::PostBind)); } +// We fail to set the option when the underlying setsockopt syscall fails. +TEST_F(SocketOptionImplTest, SetOptionTransparentFailure) { + SocketOptionImpl socket_option{true, {}}; + EXPECT_FALSE(socket_option.setOption(socket_, Socket::SocketState::PreBind)); +} + // We fail to set the option when the underlying setsockopt syscall fails. TEST_F(SocketOptionImplTest, SetOptionFreebindFailure) { - SocketOptionImpl socket_option{true}; + SocketOptionImpl socket_option{{}, true}; EXPECT_FALSE(socket_option.setOption(socket_, Socket::SocketState::PreBind)); } -// The happy path for setOpion(); IP_FREEBIND is set true. -TEST_F(SocketOptionImplTest, SetOptionFreeebindSuccessTrue) { - SocketOptionImpl socket_option{true}; +// The happy path for setOption(); IP_TRANSPARENT is set to true. +TEST_F(SocketOptionImplTest, SetOptionTransparentSuccessTrue) { + SocketOptionImpl socket_option{true, {}}; + if (ENVOY_SOCKET_IP_TRANSPARENT.has_value()) { + Address::Ipv4Instance address("1.2.3.4", 5678); + const int fd = address.socket(Address::SocketType::Stream); + EXPECT_CALL(socket_, fd()).WillRepeatedly(Return(fd)); + EXPECT_CALL(os_sys_calls_, + setsockopt_(_, IPPROTO_IP, ENVOY_SOCKET_IP_TRANSPARENT.value(), _, sizeof(int))) + .WillOnce(Invoke([](int, int, int, const void* optval, socklen_t) -> int { + EXPECT_EQ(1, *static_cast(optval)); + return 0; + })); + EXPECT_TRUE(socket_option.setOption(socket_, Socket::SocketState::PreBind)); + } else { + EXPECT_FALSE(socket_option.setOption(socket_, Socket::SocketState::PreBind)); + } +} + +// The happy path for setOption(); IP_FREEBIND is set to true. +TEST_F(SocketOptionImplTest, SetOptionFreebindSuccessTrue) { + SocketOptionImpl socket_option{{}, true}; if (ENVOY_SOCKET_IP_FREEBIND.has_value()) { Address::Ipv4Instance address("1.2.3.4", 5678); const int fd = address.socket(Address::SocketType::Stream); @@ -63,9 +88,31 @@ TEST_F(SocketOptionImplTest, SetOptionFreeebindSuccessTrue) { } } -// The happy path for setOpion(); IP_FREEBIND is set false. -TEST_F(SocketOptionImplTest, SetOptionFreeebindSuccessFalse) { - SocketOptionImpl socket_option{true}; +// The happy path for setOpion(); IP_TRANSPARENT is set to false. +TEST_F(SocketOptionImplTest, SetOptionTransparentSuccessFalse) { + SocketOptionImpl socket_option{false, {}}; + if (ENVOY_SOCKET_IP_TRANSPARENT.has_value()) { + Address::Ipv4Instance address("1.2.3.4", 5678); + const int fd = address.socket(Address::SocketType::Stream); + EXPECT_CALL(socket_, fd()).WillRepeatedly(Return(fd)); + EXPECT_CALL(os_sys_calls_, + setsockopt_(_, IPPROTO_IP, ENVOY_SOCKET_IP_TRANSPARENT.value(), _, sizeof(int))) + .Times(2) + .WillRepeatedly(Invoke([](int, int, int, const void* optval, socklen_t) -> int { + EXPECT_EQ(0, *static_cast(optval)); + return 0; + })); + EXPECT_TRUE(socket_option.setOption(socket_, Socket::SocketState::PreBind)); + EXPECT_TRUE(socket_option.setOption(socket_, Socket::SocketState::PostBind)); + } else { + EXPECT_FALSE(socket_option.setOption(socket_, Socket::SocketState::PreBind)); + EXPECT_FALSE(socket_option.setOption(socket_, Socket::SocketState::PostBind)); + } +} + +// The happy path for setOpion(); IP_FREEBIND is set to false. +TEST_F(SocketOptionImplTest, SetOptionFreebindSuccessFalse) { + SocketOptionImpl socket_option{{}, false}; if (ENVOY_SOCKET_IP_FREEBIND.has_value()) { Address::Ipv4Instance address("1.2.3.4", 5678); const int fd = address.socket(Address::SocketType::Stream); @@ -73,7 +120,7 @@ TEST_F(SocketOptionImplTest, SetOptionFreeebindSuccessFalse) { EXPECT_CALL(os_sys_calls_, setsockopt_(_, IPPROTO_IP, ENVOY_SOCKET_IP_FREEBIND.value(), _, sizeof(int))) .WillOnce(Invoke([](int, int, int, const void* optval, socklen_t) -> int { - EXPECT_EQ(1, *static_cast(optval)); + EXPECT_EQ(0, *static_cast(optval)); return 0; })); EXPECT_TRUE(socket_option.setOption(socket_, Socket::SocketState::PreBind)); @@ -84,7 +131,7 @@ TEST_F(SocketOptionImplTest, SetOptionFreeebindSuccessFalse) { // Freebind settings have no effect on post-bind behavior. TEST_F(SocketOptionImplTest, SetOptionFreebindPostBind) { - SocketOptionImpl socket_option{true}; + SocketOptionImpl socket_option{{}, true}; EXPECT_TRUE(socket_option.setOption(socket_, Socket::SocketState::PostBind)); } diff --git a/test/server/listener_manager_impl_test.cc b/test/server/listener_manager_impl_test.cc index 3128a8e5ea61a..a819ec47e43b6 100644 --- a/test/server/listener_manager_impl_test.cc +++ b/test/server/listener_manager_impl_test.cc @@ -1392,11 +1392,11 @@ TEST_F(ListenerManagerImplWithRealFiltersTest, OriginalDstTestFilterOptionFail) EXPECT_EQ(0U, manager_->listeners().size()); } -// Validate that when freebind is set in the Listener, we see no socket option -// set. -TEST_F(ListenerManagerImplWithRealFiltersTest, FreebindListenerDisabled) { +// Validate that when neither transparent nor freebind is not set in the +// Listener, we see no socket option set. +TEST_F(ListenerManagerImplWithRealFiltersTest, TransparentFreebindListenerDisabled) { const std::string yaml = TestEnvironment::substitute(R"EOF( - name: "FreebindListener" + name: "TestListener" address: socket_address: { address: 127.0.0.1, port_value: 1111 } filter_chains: @@ -1407,13 +1407,63 @@ TEST_F(ListenerManagerImplWithRealFiltersTest, FreebindListenerDisabled) { .WillOnce(Invoke([&](Network::Address::InstanceConstSharedPtr, const Network::Socket::OptionsSharedPtr& options, bool) -> Network::SocketSharedPtr { - EXPECT_EQ(options.get(), nullptr); + EXPECT_NE(options.get(), nullptr); return listener_factory_.socket_; })); manager_->addOrUpdateListener(parseListenerFromV2Yaml(yaml), true); EXPECT_EQ(1U, manager_->listeners().size()); } +// Validate that when transparent is set in the Listener, we see the socket option +// propagated to setsockopt(). This is as close to an end-to-end test as we have +// for this feature, due to the complexity of creating an integration test +// involving the network stack. We only test the IPv4 case here, as the logic +// around IPv4/IPv6 handling is tested generically in +// socket_option_impl_test.cc. +TEST_F(ListenerManagerImplWithRealFiltersTest, TransparentListenerEnabled) { + NiceMock os_sys_calls; + TestThreadsafeSingletonInjector os_calls(&os_sys_calls); + + const std::string yaml = TestEnvironment::substitute(R"EOF( + name: TransparentListener + address: + socket_address: { address: 127.0.0.1, port_value: 1111 } + filter_chains: + - filters: + transparent: true + )EOF", + Network::Address::IpVersion::v4); + if (ENVOY_SOCKET_IP_TRANSPARENT.has_value()) { + EXPECT_CALL(listener_factory_, createListenSocket(_, _, true)) + .WillOnce(Invoke([this](Network::Address::InstanceConstSharedPtr, + const Network::Socket::OptionsSharedPtr& options, + bool) -> Network::SocketSharedPtr { + EXPECT_NE(options.get(), nullptr); + EXPECT_EQ(options->size(), 1); + EXPECT_TRUE( + (*options->begin()) + ->setOption(*listener_factory_.socket_, Network::Socket::SocketState::PreBind)); + return listener_factory_.socket_; + })); + // Expecting the socket option to bet set twice, once pre-bind, once post-bind. + EXPECT_CALL(os_sys_calls, + setsockopt_(_, IPPROTO_IP, ENVOY_SOCKET_IP_TRANSPARENT.value(), _, sizeof(int))) + .Times(2) + .WillRepeatedly(Invoke([](int, int, int, const void* optval, socklen_t) -> int { + EXPECT_EQ(1, *static_cast(optval)); + return 0; + })); + manager_->addOrUpdateListener(parseListenerFromV2Yaml(yaml), true); + EXPECT_EQ(1U, manager_->listeners().size()); + } else { + // MockListenerSocket is not a real socket, so this always fails in testing. + EXPECT_THROW_WITH_MESSAGE(manager_->addOrUpdateListener(parseListenerFromV2Yaml(yaml), true), + EnvoyException, + "MockListenerComponentFactory: Setting socket options failed"); + EXPECT_EQ(0U, manager_->listeners().size()); + } +} + // Validate that when freebind is set in the Listener, we see the socket option // propagated to setsockopt(). This is as close to an end-to-end test as we have // for this feature, due to the complexity of creating an integration test