diff --git a/api/envoy/api/v2/cds.proto b/api/envoy/api/v2/cds.proto index 3eaa59eeb4058..461bdcb8627e6 100644 --- a/api/envoy/api/v2/cds.proto +++ b/api/envoy/api/v2/cds.proto @@ -41,7 +41,7 @@ service ClusterDiscoveryService { // [#protodoc-title: Clusters] // Configuration for a single upstream cluster. -// [#comment:next free field: 30] +// [#comment:next free field: 31] message Cluster { // Supplies the name of the cluster which must be unique across all clusters. // The cluster name is used when emitting @@ -428,6 +428,9 @@ message Cluster { // Determines how Envoy selects the protocol used to speak to upstream hosts. ClusterProtocolSelection protocol_selection = 26; + + // Optional options for upstream connections. + envoy.api.v2.UpstreamConnectionOptions upstream_connection_options = 30; } // An extensible structure containing the address Envoy should bind to when @@ -436,3 +439,8 @@ message UpstreamBindConfig { // The address Envoy should bind to when establishing upstream connections. core.Address source_address = 1; } + +message UpstreamConnectionOptions { + // If set then set SO_KEEPALIVE on the socket to enable TCP Keepalives. + core.TcpKeepalive tcp_keepalive = 1; +} diff --git a/api/envoy/api/v2/core/address.proto b/api/envoy/api/v2/core/address.proto index fdfb64ee5d3cd..39c50047b0b11 100644 --- a/api/envoy/api/v2/core/address.proto +++ b/api/envoy/api/v2/core/address.proto @@ -58,6 +58,20 @@ message SocketAddress { bool ipv4_compat = 6; } +message TcpKeepalive { + // Maximum number of keepalive probes to send without response before deciding + // the connection is dead. Default is to use the OS level configuration (unless + // overridden, Linux defaults to 9.) + google.protobuf.UInt32Value keepalive_probes = 1; + // The number of seconds a connection needs to be idle before keep-alive probes + // start being sent. Default is to use the OS level configuration (unless + // overridden, Linux defaults to 7200s (ie 2 hours.) + google.protobuf.UInt32Value keepalive_time = 2; + // The number of seconds between keep-alive probes. Default is to use the OS + // level configuration (unless overridden, Linux defaults to 75s.) + google.protobuf.UInt32Value keepalive_interval = 3; +} + message BindConfig { // The address to bind to when creating a socket. SocketAddress source_address = 1 diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index e2deb132eb8c2..a18c7ed3c09f9 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -49,6 +49,8 @@ Version history :ref:`cluster specific ` options. * sockets: added `IP_TRANSPARENT` socket option support for :ref:`listeners `. +* sockets: added `SO_KEEPALIVE` socket option for upstream connections + :ref:`per cluster `. * tracing: the sampling decision is now delegated to the tracers, allowing the tracer to decide when and if to use it. For example, if the :ref:`x-b3-sampled ` header is supplied with the client request, its value will override any sampling decision made by the Envoy proxy. diff --git a/include/envoy/network/listen_socket.h b/include/envoy/network/listen_socket.h index 12a2a671e79a6..91cc94e636642 100644 --- a/include/envoy/network/listen_socket.h +++ b/include/envoy/network/listen_socket.h @@ -68,13 +68,35 @@ class Socket { typedef std::vector Options; typedef std::shared_ptr OptionsSharedPtr; + static OptionsSharedPtr& appendOptions(OptionsSharedPtr& to, const OptionsSharedPtr& from) { + to->insert(to->end(), from->begin(), from->end()); + return to; + } + + static bool applyOptions(const OptionsSharedPtr& options, Socket& socket, SocketState state) { + if (options == nullptr) { + return true; + } + for (const auto& option : *options) { + if (!option->setOption(socket, state)) { + return false; + } + } + return true; + } + /** * Add a socket option visitor for later retrieval with options(). */ virtual void addOption(const OptionConstSharedPtr&) PURE; /** - * @return the socket options stored earlier with addOption() calls, if any. + * Add socket option visitors for later retrieval with options(). + */ + virtual void addOptions(const OptionsSharedPtr&) PURE; + + /** + * @return the socket options stored earlier with addOption() and addOptions() calls, if any. */ virtual const OptionsSharedPtr& options() const PURE; }; diff --git a/include/envoy/server/filter_config.h b/include/envoy/server/filter_config.h index 9e9b57998f4e0..bb1521d600815 100644 --- a/include/envoy/server/filter_config.h +++ b/include/envoy/server/filter_config.h @@ -136,6 +136,8 @@ class ListenerFactoryContext : public FactoryContext { * Store socket options to be set on the listen socket before listening. */ virtual void addListenSocketOption(const Network::Socket::OptionConstSharedPtr& option) PURE; + + virtual void addListenSocketOptions(const Network::Socket::OptionsSharedPtr& options) PURE; }; /** diff --git a/include/envoy/upstream/upstream.h b/include/envoy/upstream/upstream.h index f9294e3e0cff9..9e3c1623f3801 100644 --- a/include/envoy/upstream/upstream.h +++ b/include/envoy/upstream/upstream.h @@ -397,8 +397,6 @@ class ClusterInfo { // Use the downstream protocol (HTTP1.1, HTTP2) for upstream connections as well, if available. // This is used when creating connection pools. static const uint64_t USE_DOWNSTREAM_PROTOCOL = 0x2; - // Use IP_FREEBIND socket option when binding. - static const uint64_t FREEBIND = 0x4; }; virtual ~ClusterInfo() {} @@ -521,6 +519,13 @@ class ClusterInfo { * @return const envoy::api::v2::core::Metadata& the configuration metadata for this cluster. */ virtual const envoy::api::v2::core::Metadata& metadata() const PURE; + + /** + * + * @return const Network::ConnectionSocket::OptionsSharedPtr& socket options for all + * connections for this cluster. + */ + virtual const Network::ConnectionSocket::OptionsSharedPtr& clusterSocketOptions() const PURE; }; typedef std::shared_ptr ClusterInfoConstSharedPtr; diff --git a/source/common/network/BUILD b/source/common/network/BUILD index 1b84744a08520..5214b2f60d426 100644 --- a/source/common/network/BUILD +++ b/source/common/network/BUILD @@ -187,6 +187,35 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "addr_family_aware_socket_option_lib", + srcs = ["addr_family_aware_socket_option_impl.cc"], + hdrs = ["addr_family_aware_socket_option_impl.h"], + external_deps = ["abseil_optional"], + deps = [ + ":address_lib", + ":socket_option_lib", + "//include/envoy/network:listen_socket_interface", + "//source/common/api:os_sys_calls_lib", + "//source/common/common:assert_lib", + "//source/common/common:logger_lib", + ], +) + +envoy_cc_library( + name = "socket_option_factory_lib", + srcs = ["socket_option_factory.cc"], + hdrs = ["socket_option_factory.h"], + external_deps = ["abseil_optional"], + deps = [ + ":addr_family_aware_socket_option_lib", + ":address_lib", + ":socket_option_lib", + "//include/envoy/network:listen_socket_interface", + "//source/common/common:logger_lib", + ], +) + envoy_cc_library( name = "utility_lib", srcs = ["utility.cc"], diff --git a/source/common/network/addr_family_aware_socket_option_impl.cc b/source/common/network/addr_family_aware_socket_option_impl.cc new file mode 100644 index 0000000000000..0552186e9ade5 --- /dev/null +++ b/source/common/network/addr_family_aware_socket_option_impl.cc @@ -0,0 +1,58 @@ +#include "common/network/addr_family_aware_socket_option_impl.h" + +#include "envoy/common/exception.h" + +#include "common/api/os_sys_calls_impl.h" +#include "common/common/assert.h" +#include "common/network/address_impl.h" +#include "common/network/socket_option_impl.h" + +namespace Envoy { +namespace Network { + +bool AddrFailyAwareSocketOptionImpl::setOption(Socket& socket, Socket::SocketState state) const { + return setIpSocketOption(socket, state, ipv4_option_, ipv6_option_); +} + +bool AddrFailyAwareSocketOptionImpl::setIpSocketOption( + Socket& socket, Socket::SocketState state, const std::unique_ptr& ipv4_option, + const std::unique_ptr& ipv6_option) { + // If this isn't IP, we're out of luck. + Address::InstanceConstSharedPtr address; + const Address::Ip* ip = nullptr; + try { + // We have local address when the socket is used in a listener but have to + // infer the IP from the socket FD when initiating connections. + // TODO(htuch): Figure out a way to obtain a consistent interface for IP + // version from socket. + if (socket.localAddress()) { + ip = socket.localAddress()->ip(); + } else { + address = Address::addressFromFd(socket.fd()); + ip = address->ip(); + } + } catch (const EnvoyException&) { + // Ignore, we get here because we failed in getsockname(). + // TODO(htuch): We should probably clean up this logic to avoid relying on exceptions. + } + if (ip == nullptr) { + ENVOY_LOG(warn, "Failed to set IP socket option on non-IP socket"); + return false; + } + + // If the FD is v4, we can only try the IPv4 variant. + if (ip->version() == Network::Address::IpVersion::v4) { + return ipv4_option->setOption(socket, state); + } + + // If the FD is v6, we first try the IPv6 variant if the platfrom supports it and fallback to the + // IPv4 variant otherwise. + ASSERT(ip->version() == Network::Address::IpVersion::v6); + if (ipv6_option->isSupported()) { + return ipv6_option->setOption(socket, state); + } + return ipv4_option->setOption(socket, state); +} + +} // namespace Network +} // namespace Envoy diff --git a/source/common/network/addr_family_aware_socket_option_impl.h b/source/common/network/addr_family_aware_socket_option_impl.h new file mode 100644 index 0000000000000..d0fd7b8bde83e --- /dev/null +++ b/source/common/network/addr_family_aware_socket_option_impl.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include + +#include "envoy/network/listen_socket.h" + +#include "common/common/logger.h" +#include "common/network/socket_option_impl.h" + +#include "absl/types/optional.h" + +namespace Envoy { +namespace Network { + +class AddrFailyAwareSocketOptionImpl : public Socket::Option, + Logger::Loggable { +public: + AddrFailyAwareSocketOptionImpl(Socket::SocketState in_state, SocketOptionName ipv4_optname, + SocketOptionName ipv6_optname, int value) + : ipv4_option_(absl::make_unique(in_state, ipv4_optname, value)), + ipv6_option_(absl::make_unique(in_state, ipv6_optname, value)) {} + + // Socket::Option + bool setOption(Socket& socket, Socket::SocketState state) const override; + // The common socket options don't require a hash key. + void hashKey(std::vector&) const override {} + + /** + * Set a socket option that applies at both IPv4 and IPv6 socket levels. When the underlying FD + * is IPv6, this function will attempt to set at IPv6 unless the platform only supports the + * option at the IPv4 level. + * @param socket. + * @param ipv4_optname SocketOptionName for IPv4 level. Set to empty if not supported on + * platform. + * @param ipv6_optname SocketOptionName for IPv6 level. Set to empty if not supported on + * platform. + * @param optval as per setsockopt(2). + * @param optlen as per setsockopt(2). + * @return int as per setsockopt(2). ENOTSUP is returned if the option is not supported on the + * platform for fd after the above option level fallback semantics are taken into account or the + * socket is non-IP. + */ + static bool setIpSocketOption(Socket& socket, Socket::SocketState state, + const std::unique_ptr& ipv4_option, + const std::unique_ptr& ipv6_option); + +private: + const std::unique_ptr ipv4_option_; + const std::unique_ptr ipv6_option_; +}; + +} // namespace Network +} // namespace Envoy diff --git a/source/common/network/connection_impl.cc b/source/common/network/connection_impl.cc index 18b3ade832a9b..16a355c9a05ef 100644 --- a/source/common/network/connection_impl.cc +++ b/source/common/network/connection_impl.cc @@ -536,18 +536,15 @@ ClientConnectionImpl::ClientConnectionImpl( const Network::ConnectionSocket::OptionsSharedPtr& options) : ConnectionImpl(dispatcher, std::make_unique(remote_address), std::move(transport_socket), false) { - if (options) { - for (const auto& option : *options) { - if (!option->setOption(*socket_, Socket::SocketState::PreBind)) { - // Set a special error state to ensure asynchronous close to give the owner of the - // ConnectionImpl a chance to add callbacks and detect the "disconnect". - immediate_error_event_ = ConnectionEvent::LocalClose; - // Trigger a write event to close this connection out-of-band. - file_event_->activate(Event::FileReadyType::Write); - return; - } - } + if (!Network::Socket::applyOptions(options, *socket_, Socket::SocketState::PreBind)) { + // Set a special error state to ensure asynchronous close to give the owner of the + // ConnectionImpl a chance to add callbacks and detect the "disconnect". + immediate_error_event_ = ConnectionEvent::LocalClose; + // Trigger a write event to close this connection out-of-band. + file_event_->activate(Event::FileReadyType::Write); + return; } + if (source_address != nullptr) { const int rc = source_address->bind(fd()); if (rc < 0) { diff --git a/source/common/network/listen_socket_impl.cc b/source/common/network/listen_socket_impl.cc index a9058c1cca6f6..59e339c66e7a0 100644 --- a/source/common/network/listen_socket_impl.cc +++ b/source/common/network/listen_socket_impl.cc @@ -30,12 +30,8 @@ void ListenSocketImpl::doBind() { } void ListenSocketImpl::setListenSocketOptions(const Network::Socket::OptionsSharedPtr& options) { - if (options) { - for (const auto& option : *options) { - if (!option->setOption(*this, SocketState::PreBind)) { - throw EnvoyException("ListenSocket: Setting socket options failed"); - } - } + if (!Network::Socket::applyOptions(options, *this, Socket::SocketState::PreBind)) { + throw EnvoyException("ListenSocket: Setting socket options failed"); } } diff --git a/source/common/network/listen_socket_impl.h b/source/common/network/listen_socket_impl.h index 945a1ea2fef18..d278943b7120a 100644 --- a/source/common/network/listen_socket_impl.h +++ b/source/common/network/listen_socket_impl.h @@ -27,12 +27,19 @@ class SocketImpl : public virtual Socket { fd_ = -1; } } - void addOption(const OptionConstSharedPtr& option) override { + void ensureOptions() { if (!options_) { options_ = std::make_shared>(); } + } + void addOption(const OptionConstSharedPtr& option) override { + ensureOptions(); options_->emplace_back(std::move(option)); } + void addOptions(const OptionsSharedPtr& options) override { + ensureOptions(); + Network::Socket::appendOptions(options_, options); + } const OptionsSharedPtr& options() const override { return options_; } protected: diff --git a/source/common/network/listener_impl.cc b/source/common/network/listener_impl.cc index e3f67296cd7b7..b09afd90c6796 100644 --- a/source/common/network/listener_impl.cc +++ b/source/common/network/listener_impl.cc @@ -66,15 +66,9 @@ ListenerImpl::ListenerImpl(Event::DispatcherImpl& dispatcher, Socket& socket, Li fmt::format("cannot listen on socket: {}", socket.localAddress()->asString())); } - Socket::OptionsSharedPtr options = socket.options(); - if (options != nullptr) { - for (auto& option : *options) { - if (!option->setOption(socket, Socket::SocketState::Listening)) { - throw CreateListenerException( - fmt::format("cannot set post-listen socket option on socket: {}", - socket.localAddress()->asString())); - } - } + if (!Network::Socket::applyOptions(socket.options(), socket, Socket::SocketState::Listening)) { + throw CreateListenerException(fmt::format( + "cannot set post-listen socket option on socket: {}", socket.localAddress()->asString())); } evconnlistener_set_error_cb(listener_.get(), errorCallback); diff --git a/source/common/network/socket_option_factory.cc b/source/common/network/socket_option_factory.cc new file mode 100644 index 0000000000000..d2766bca349d3 --- /dev/null +++ b/source/common/network/socket_option_factory.cc @@ -0,0 +1,61 @@ +#include "common/network/socket_option_factory.h" + +#include "common/network/addr_family_aware_socket_option_impl.h" +#include "common/network/socket_option_impl.h" + +namespace Envoy { +namespace Network { + +std::unique_ptr +SocketOptionFactory::buildTcpKeepaliveOptions(Network::TcpKeepaliveConfig keepalive_config) { + std::unique_ptr options = absl::make_unique(); + options->push_back(std::make_shared( + Network::Socket::SocketState::PreBind, ENVOY_SOCKET_SO_KEEPALIVE, 1)); + + if (keepalive_config.keepalive_probes_.has_value()) { + options->push_back(std::make_shared( + Network::Socket::SocketState::PreBind, ENVOY_SOCKET_TCP_KEEPCNT, + keepalive_config.keepalive_probes_.value())); + } + if (keepalive_config.keepalive_interval_.has_value()) { + options->push_back(std::make_shared( + Network::Socket::SocketState::PreBind, ENVOY_SOCKET_TCP_KEEPINTVL, + keepalive_config.keepalive_interval_.value())); + } + if (keepalive_config.keepalive_time_.has_value()) { + options->push_back(std::make_shared( + Network::Socket::SocketState::PreBind, ENVOY_SOCKET_TCP_KEEPIDLE, + keepalive_config.keepalive_time_.value())); + } + return options; +} + +std::unique_ptr SocketOptionFactory::buildIpFreebindOptions() { + std::unique_ptr options = absl::make_unique(); + options->push_back(std::make_shared( + Network::Socket::SocketState::PreBind, ENVOY_SOCKET_IP_FREEBIND, ENVOY_SOCKET_IPV6_FREEBIND, + 1)); + return options; +} + +std::unique_ptr SocketOptionFactory::buildIpTransparentOptions() { + std::unique_ptr options = absl::make_unique(); + options->push_back(std::make_shared( + Network::Socket::SocketState::PreBind, ENVOY_SOCKET_IP_TRANSPARENT, + ENVOY_SOCKET_IPV6_TRANSPARENT, 1)); + options->push_back(std::make_shared( + Network::Socket::SocketState::PostBind, ENVOY_SOCKET_IP_TRANSPARENT, + ENVOY_SOCKET_IPV6_TRANSPARENT, 1)); + return options; +} + +std::unique_ptr +SocketOptionFactory::buildTcpFastOpenOptions(uint32_t queue_length) { + std::unique_ptr options = absl::make_unique(); + options->push_back(std::make_shared( + Network::Socket::SocketState::Listening, ENVOY_SOCKET_TCP_FASTOPEN, queue_length)); + return options; +} + +} // namespace Network +} // namespace Envoy \ No newline at end of file diff --git a/source/common/network/socket_option_factory.h b/source/common/network/socket_option_factory.h new file mode 100644 index 0000000000000..e377c2dbacd7a --- /dev/null +++ b/source/common/network/socket_option_factory.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + +#include "envoy/network/listen_socket.h" + +#include "common/common/logger.h" + +#include "absl/types/optional.h" + +namespace Envoy { +namespace Network { + +struct TcpKeepaliveConfig { + absl::optional + keepalive_probes_; // Number of unanswered probes before the connection is dropped + absl::optional keepalive_time_; // Connection idle time before probing will start, in ms + absl::optional keepalive_interval_; // Interval between probes, in ms +}; + +class SocketOptionFactory : Logger::Loggable { +public: + static std::unique_ptr + buildTcpKeepaliveOptions(Network::TcpKeepaliveConfig keepalive_config); + static std::unique_ptr buildIpFreebindOptions(); + static std::unique_ptr buildIpTransparentOptions(); + static std::unique_ptr buildTcpFastOpenOptions(uint32_t queue_length); +}; +} // namespace Network +} // namespace Envoy diff --git a/source/common/network/socket_option_impl.cc b/source/common/network/socket_option_impl.cc index 23d0ce3cb6561..273b153d1a31d 100644 --- a/source/common/network/socket_option_impl.cc +++ b/source/common/network/socket_option_impl.cc @@ -9,86 +9,30 @@ namespace Envoy { namespace Network { +// Socket::Option bool SocketOptionImpl::setOption(Socket& socket, Socket::SocketState state) const { - if (state == Socket::SocketState::PreBind || state == Socket::SocketState::PostBind) { - 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 (in_state_ == state) { + const int error = SocketOptionImpl::setSocketOption(socket, optname_, value_); + if (error != 0) { + ENVOY_LOG(warn, "Setting option on socket failed: {}", strerror(errno)); + return false; } } - - if (state == Socket::SocketState::PreBind) { - if (freebind_.has_value()) { - const int should_freebind = freebind_.value() ? 1 : 0; - const int error = - setIpSocketOption(socket, ENVOY_SOCKET_IP_FREEBIND, ENVOY_SOCKET_IPV6_FREEBIND, - &should_freebind, sizeof(should_freebind)); - if (error != 0) { - ENVOY_LOG(warn, "Setting IP_FREEBIND on listener socket failed: {}", strerror(errno)); - return false; - } - } - } - return true; } -int SocketOptionImpl::setIpSocketOption(Socket& socket, SocketOptionName ipv4_optname, - SocketOptionName ipv6_optname, const void* optval, - socklen_t optlen) { - auto& os_syscalls = Api::OsSysCallsSingleton::get(); +bool SocketOptionImpl::isSupported() const { return optname_.has_value(); } - // If this isn't IP, we're out of luck. - Address::InstanceConstSharedPtr address; - const Address::Ip* ip = nullptr; - try { - // We have local address when the socket is used in a listener but have to - // infer the IP from the socket FD when initiating connections. - // TODO(htuch): Figure out a way to obtain a consistent interface for IP - // version from socket. - if (socket.localAddress()) { - ip = socket.localAddress()->ip(); - } else { - address = Address::addressFromFd(socket.fd()); - ip = address->ip(); - } - } catch (const EnvoyException&) { - // Ignore, we get here because we failed in getsockname(). - // TODO(htuch): We should probably clean up this logic to avoid relying on exceptions. - } - if (ip == nullptr) { - ENVOY_LOG(warn, "Failed to set IP socket option on non-IP socket"); - return ENOTSUP; - } - - // If the FD is v4, we can only try the IPv4 variant. - if (ip->version() == Network::Address::IpVersion::v4) { - if (!ipv4_optname) { - ENVOY_LOG(warn, "Unsupported IPv4 socket option"); - errno = ENOTSUP; - return -1; - } - return os_syscalls.setsockopt(socket.fd(), IPPROTO_IP, ipv4_optname.value(), optval, optlen); - } +int SocketOptionImpl::setSocketOption(Socket& socket, Network::SocketOptionName optname, + int value) { - // If the FD is v6, we first try the IPv6 variant if the platfrom supports it and fallback to the - // IPv4 variant otherwise. - ASSERT(ip->version() == Network::Address::IpVersion::v6); - if (ipv6_optname) { - return os_syscalls.setsockopt(socket.fd(), IPPROTO_IPV6, ipv6_optname.value(), optval, optlen); - } - if (ipv4_optname) { - return os_syscalls.setsockopt(socket.fd(), IPPROTO_IP, ipv4_optname.value(), optval, optlen); + if (!optname.has_value()) { + errno = ENOTSUP; + return -1; } - ENVOY_LOG(warn, "Unsupported IPv6 socket option"); - errno = ENOTSUP; - return -1; + auto& os_syscalls = Api::OsSysCallsSingleton::get(); + return os_syscalls.setsockopt(socket.fd(), optname.value().first, optname.value().second, &value, + sizeof(value)); } } // namespace Network diff --git a/source/common/network/socket_option_impl.h b/source/common/network/socket_option_impl.h index 0e7277aa1eda8..78cabf84b77af 100644 --- a/source/common/network/socket_option_impl.h +++ b/source/common/network/socket_option_impl.h @@ -2,6 +2,7 @@ #include #include +#include #include #include "envoy/network/listen_socket.h" @@ -15,69 +16,91 @@ namespace Network { // Optional variant of setsockopt(2) optname. The idea here is that if the option is not supported // on a platform, we can make this the empty value. This allows us to avoid proliferation of #ifdef. -typedef absl::optional SocketOptionName; +typedef absl::optional> SocketOptionName; #ifdef IP_TRANSPARENT -#define ENVOY_SOCKET_IP_TRANSPARENT Network::SocketOptionName(IP_TRANSPARENT) +#define ENVOY_SOCKET_IP_TRANSPARENT \ + Network::SocketOptionName(std::make_pair(IPPROTO_IP, IP_TRANSPARENT)) #else #define ENVOY_SOCKET_IP_TRANSPARENT Network::SocketOptionName() #endif #ifdef IPV6_TRANSPARENT -#define ENVOY_SOCKET_IPV6_TRANSPARENT Network::SocketOptionName(IPV6_TRANSPARENT) +#define ENVOY_SOCKET_IPV6_TRANSPARENT \ + Network::SocketOptionName(std::make_pair(IPPROTO_IPV6, IPV6_TRANSPARENT)) #else #define ENVOY_SOCKET_IPV6_TRANSPARENT Network::SocketOptionName() #endif #ifdef IP_FREEBIND -#define ENVOY_SOCKET_IP_FREEBIND Network::SocketOptionName(IP_FREEBIND) +#define ENVOY_SOCKET_IP_FREEBIND Network::SocketOptionName(std::make_pair(IPPROTO_IP, IP_FREEBIND)) #else #define ENVOY_SOCKET_IP_FREEBIND Network::SocketOptionName() #endif #ifdef IPV6_FREEBIND -#define ENVOY_SOCKET_IPV6_FREEBIND Network::SocketOptionName(IPV6_FREEBIND) +#define ENVOY_SOCKET_IPV6_FREEBIND \ + Network::SocketOptionName(std::make_pair(IPPROTO_IPV6, IPV6_FREEBIND)) #else #define ENVOY_SOCKET_IPV6_FREEBIND Network::SocketOptionName() #endif +#ifdef SO_KEEPALIVE +#define ENVOY_SOCKET_SO_KEEPALIVE \ + Network::SocketOptionName(std::make_pair(SOL_SOCKET, SO_KEEPALIVE)) +#else +#define ENVOY_SOCKET_SO_KEEPALIVE Network::SocketOptionName() +#endif + +#ifdef TCP_KEEPCNT +#define ENVOY_SOCKET_TCP_KEEPCNT Network::SocketOptionName(std::make_pair(IPPROTO_TCP, TCP_KEEPCNT)) +#else +#define ENVOY_SOCKET_TCP_KEEPCNT Network::SocketOptionName() +#endif + +#ifdef TCP_KEEPIDLE +#define ENVOY_SOCKET_TCP_KEEPIDLE \ + Network::SocketOptionName(std::make_pair(IPPROTO_TCP, TCP_KEEPIDLE)) +#elif TCP_KEEPALIVE // MacOS uses a different name from Linux for just this option. +#define ENVOY_SOCKET_TCP_KEEPIDLE \ + Network::SocketOptionName(std::make_pair(IPPROTO_TCP, TCP_KEEPALIVE)) +#else +#define ENVOY_SOCKET_TCP_KEEPIDLE Network::SocketOptionName() +#endif + +#ifdef TCP_KEEPINTVL +#define ENVOY_SOCKET_TCP_KEEPINTVL \ + Network::SocketOptionName(std::make_pair(IPPROTO_TCP, TCP_KEEPINTVL)) +#else +#define ENVOY_SOCKET_TCP_KEEPINTVL Network::SocketOptionName() +#endif + #ifdef TCP_FASTOPEN -#define ENVOY_SOCKET_TCP_FASTOPEN Network::SocketOptionName(TCP_FASTOPEN) +#define ENVOY_SOCKET_TCP_FASTOPEN \ + Network::SocketOptionName(std::make_pair(IPPROTO_TCP, TCP_FASTOPEN)) #else #define ENVOY_SOCKET_TCP_FASTOPEN Network::SocketOptionName() #endif -class SocketOptionImpl : public Socket::Option, protected Logger::Loggable { +class SocketOptionImpl : public Socket::Option, Logger::Loggable { public: - SocketOptionImpl(absl::optional transparent, absl::optional freebind) - : transparent_(transparent), freebind_(freebind) {} + SocketOptionImpl(Socket::SocketState in_state, Network::SocketOptionName optname, int value) + : in_state_(in_state), optname_(optname), value_(value) {} // Socket::Option bool setOption(Socket& socket, Socket::SocketState state) const override; + // The common socket options don't require a hash key. void hashKey(std::vector&) const override {} - /** - * Set a socket option that applies at both IPv4 and IPv6 socket levels. When the underlying FD - * is IPv6, this function will attempt to set at IPv6 unless the platform only supports the - * option at the IPv4 level. - * @param socket. - * @param ipv4_optname SocketOptionName for IPv4 level. Set to empty if not supported on - * platform. - * @param ipv6_optname SocketOptionName for IPv6 level. Set to empty if not supported on - * platform. - * @param optval as per setsockopt(2). - * @param optlen as per setsockopt(2). - * @return int as per setsockopt(2). ENOTSUP is returned if the option is not supported on the - * platform for fd after the above option level fallback semantics are taken into account or the - * socket is non-IP. - */ - static int setIpSocketOption(Socket& socket, SocketOptionName ipv4_optname, - SocketOptionName ipv6_optname, const void* optval, socklen_t optlen); + bool isSupported() const; + + static int setSocketOption(Socket& socket, Network::SocketOptionName optname, int value); private: - const absl::optional transparent_; - const absl::optional freebind_; + const Socket::SocketState in_state_; + const Network::SocketOptionName optname_; + const int value_; }; } // namespace Network diff --git a/source/common/upstream/BUILD b/source/common/upstream/BUILD index c4a64d2baad2f..da16c37fc37c5 100644 --- a/source/common/upstream/BUILD +++ b/source/common/upstream/BUILD @@ -329,6 +329,7 @@ envoy_cc_library( "//include/envoy/event:dispatcher_interface", "//include/envoy/event:timer_interface", "//include/envoy/network:dns_interface", + "//include/envoy/network:listen_socket_interface", "//include/envoy/ssl:context_interface", "//include/envoy/upstream:health_checker_interface", "//source/common/common:enum_to_int", @@ -338,7 +339,7 @@ envoy_cc_library( "//source/common/http:utility_lib", "//source/common/network:address_lib", "//source/common/network:resolver_lib", - "//source/common/network:socket_option_lib", + "//source/common/network:socket_option_factory_lib", "//source/common/network:utility_lib", "//source/common/protobuf", "//source/common/protobuf:utility_lib", diff --git a/source/common/upstream/upstream_impl.cc b/source/common/upstream/upstream_impl.cc index 8c5b445e51c7c..486c4fa517550 100644 --- a/source/common/upstream/upstream_impl.cc +++ b/source/common/upstream/upstream_impl.cc @@ -24,7 +24,7 @@ #include "common/http/utility.h" #include "common/network/address_impl.h" #include "common/network/resolver_impl.h" -#include "common/network/socket_option_impl.h" +#include "common/network/socket_option_factory.h" #include "common/protobuf/protobuf.h" #include "common/protobuf/utility.h" #include "common/upstream/eds.h" @@ -54,8 +54,7 @@ getSourceAddress(const envoy::api::v2::Cluster& cluster, return nullptr; } -uint64_t parseFeatures(const envoy::api::v2::Cluster& config, - const envoy::api::v2::core::BindConfig bind_config) { +uint64_t parseFeatures(const envoy::api::v2::Cluster& config) { uint64_t features = 0; if (config.has_http2_protocol_options()) { features |= ClusterInfoImpl::Features::HTTP2; @@ -63,24 +62,40 @@ uint64_t parseFeatures(const envoy::api::v2::Cluster& config, if (config.protocol_selection() == envoy::api::v2::Cluster::USE_DOWNSTREAM_PROTOCOL) { features |= ClusterInfoImpl::Features::USE_DOWNSTREAM_PROTOCOL; } + return features; +} + +Network::TcpKeepaliveConfig parseTcpKeepaliveConfig(const envoy::api::v2::Cluster& config) { + const envoy::api::v2::core::TcpKeepalive& options = + config.upstream_connection_options().tcp_keepalive(); + return Network::TcpKeepaliveConfig{ + PROTOBUF_GET_WRAPPED_OR_DEFAULT(options, keepalive_probes, absl::optional()), + PROTOBUF_GET_WRAPPED_OR_DEFAULT(options, keepalive_time, absl::optional()), + PROTOBUF_GET_WRAPPED_OR_DEFAULT(options, keepalive_interval, absl::optional())}; +} + +const Network::ConnectionSocket::OptionsSharedPtr +parseClusterSocketOptions(const envoy::api::v2::Cluster& config, + const envoy::api::v2::core::BindConfig bind_config) { + Network::ConnectionSocket::OptionsSharedPtr cluster_options = + std::make_shared(); // Cluster IP_FREEBIND settings, when set, will override the cluster manager wide settings. if ((bind_config.freebind().value() && !config.upstream_bind_config().has_freebind()) || config.upstream_bind_config().freebind().value()) { - features |= ClusterInfoImpl::Features::FREEBIND; + Network::Socket::appendOptions(cluster_options, + Network::SocketOptionFactory::buildIpFreebindOptions()); } - return features; + if (config.upstream_connection_options().has_tcp_keepalive()) { + Network::Socket::appendOptions( + cluster_options, + Network::SocketOptionFactory::buildTcpKeepaliveOptions(parseTcpKeepaliveConfig(config))); + } + if (cluster_options->empty()) { + return nullptr; + } + return cluster_options; } -// Socket::Option implementation for API-defined upstream options. This same -// object can be extended to handle additional upstream socket options. -class UpstreamSocketOption : public Network::SocketOptionImpl { -public: - UpstreamSocketOption(const ClusterInfo& cluster_info) - : Network::SocketOptionImpl({}, cluster_info.features() & ClusterInfo::Features::FREEBIND - ? true - : absl::optional{}) {} -}; - } // namespace Host::CreateConnectionData @@ -99,19 +114,23 @@ Network::ClientConnectionPtr HostImpl::createConnection(Event::Dispatcher& dispatcher, const ClusterInfo& cluster, Network::Address::InstanceConstSharedPtr address, const Network::ConnectionSocket::OptionsSharedPtr& options) { - Network::ConnectionSocket::OptionsSharedPtr cluster_options; - if (cluster.features() & ClusterInfo::Features::FREEBIND) { - cluster_options = std::make_shared(); + Network::ConnectionSocket::OptionsSharedPtr connection_options; + if (cluster.clusterSocketOptions() != nullptr) { if (options) { - *cluster_options = *options; + connection_options = std::make_shared(); + *connection_options = *options; + std::copy(cluster.clusterSocketOptions()->begin(), cluster.clusterSocketOptions()->end(), + std::back_inserter(*connection_options)); + } else { + connection_options = cluster.clusterSocketOptions(); } - cluster_options->emplace_back(new UpstreamSocketOption(cluster)); } else { - cluster_options = options; + connection_options = options; } + Network::ClientConnectionPtr connection = dispatcher.createClientConnection( address, cluster.sourceAddress(), cluster.transportSocketFactory().createTransportSocket(), - cluster_options); + connection_options); connection->setBufferLimits(cluster.perConnectionBufferLimitBytes()); return connection; } @@ -238,7 +257,7 @@ ClusterInfoImpl::ClusterInfoImpl(const envoy::api::v2::Cluster& config, config.alt_stat_name().empty() ? name_ : std::string(config.alt_stat_name())))), stats_(generateStats(*stats_scope_)), load_report_stats_(generateLoadReportStats(load_report_stats_store_)), - features_(parseFeatures(config, bind_config)), + features_(parseFeatures(config)), http2_settings_(Http::Utility::parseHttp2Settings(config.http2_protocol_options())), resource_managers_(config, runtime, name_), maintenance_mode_runtime_key_(fmt::format("upstream.maintenance_mode.{}", name_)), @@ -246,7 +265,8 @@ ClusterInfoImpl::ClusterInfoImpl(const envoy::api::v2::Cluster& config, lb_ring_hash_config_(envoy::api::v2::Cluster::RingHashLbConfig(config.ring_hash_lb_config())), ssl_context_manager_(ssl_context_manager), added_via_api_(added_via_api), lb_subset_(LoadBalancerSubsetInfoImpl(config.lb_subset_config())), - metadata_(config.metadata()), common_lb_config_(config.common_lb_config()) { + metadata_(config.metadata()), common_lb_config_(config.common_lb_config()), + cluster_socket_options_(parseClusterSocketOptions(config, bind_config)) { // If the cluster doesn't have a transport socket configured, override with the default transport // socket implementation based on the tls_context. We copy by value first then override if diff --git a/source/common/upstream/upstream_impl.h b/source/common/upstream/upstream_impl.h index 9fc661ab80af5..0fa6fdff2ede5 100644 --- a/source/common/upstream/upstream_impl.h +++ b/source/common/upstream/upstream_impl.h @@ -356,6 +356,10 @@ class ClusterInfoImpl : public ClusterInfo, // Server::Configuration::TransportSocketFactoryContext Ssl::ContextManager& sslContextManager() override { return ssl_context_manager_; } + const Network::ConnectionSocket::OptionsSharedPtr& clusterSocketOptions() const override { + return cluster_socket_options_; + }; + private: struct ResourceManagers { ResourceManagers(const envoy::api::v2::Cluster& config, Runtime::Loader& runtime, @@ -393,6 +397,7 @@ class ClusterInfoImpl : public ClusterInfo, LoadBalancerSubsetInfoImpl lb_subset_; const envoy::api::v2::core::Metadata metadata_; const envoy::api::v2::Cluster::CommonLbConfig common_lb_config_; + const Network::ConnectionSocket::OptionsSharedPtr cluster_socket_options_; }; /** diff --git a/source/server/BUILD b/source/server/BUILD index 8777b2ae36942..cf876b89f16f6 100644 --- a/source/server/BUILD +++ b/source/server/BUILD @@ -197,7 +197,6 @@ envoy_cc_library( ":configuration_lib", ":drain_manager_lib", ":init_manager_lib", - ":listener_socket_option_lib", "//include/envoy/server:filter_config_interface", "//include/envoy/server:listener_manager_interface", "//include/envoy/server:transport_socket_config_interface", @@ -206,7 +205,7 @@ envoy_cc_library( "//source/common/config:utility_lib", "//source/common/network:listen_socket_lib", "//source/common/network:resolver_lib", - "//source/common/network:socket_option_lib", + "//source/common/network:socket_option_factory_lib", "//source/common/network:utility_lib", "//source/common/protobuf:utility_lib", "//source/common/ssl:context_config_lib", @@ -216,19 +215,6 @@ envoy_cc_library( ], ) -envoy_cc_library( - name = "listener_socket_option_lib", - srcs = ["listener_socket_option_impl.cc"], - hdrs = ["listener_socket_option_impl.h"], - deps = [ - ":configuration_lib", - "//source/common/api:os_sys_calls_lib", - "//source/common/network:listen_socket_lib", - "//source/common/network:socket_option_lib", - "//source/common/protobuf:utility_lib", - ], -) - envoy_cc_library( name = "proto_descriptors_lib", srcs = ["proto_descriptors.cc"], diff --git a/source/server/listener_manager_impl.cc b/source/server/listener_manager_impl.cc index 4ebab96799d36..68661e8e463c6 100644 --- a/source/server/listener_manager_impl.cc +++ b/source/server/listener_manager_impl.cc @@ -9,13 +9,12 @@ #include "common/config/utility.h" #include "common/network/listen_socket_impl.h" #include "common/network/resolver_impl.h" -#include "common/network/socket_option_impl.h" +#include "common/network/socket_option_factory.h" #include "common/network/utility.h" #include "common/protobuf/utility.h" #include "server/configuration_impl.h" #include "server/drain_manager_impl.h" -#include "server/listener_socket_option_impl.h" #include "extensions/filters/listener/well_known_names.h" #include "extensions/transport_sockets/well_known_names.h" @@ -132,8 +131,16 @@ ListenerImpl::ListenerImpl(const envoy::api::v2::Listener& config, ListenerManag // filter chain #1308. ASSERT(config.filter_chains().size() >= 1); - // Add listen socket options from the config. - addListenSocketOption(std::make_shared(config)); + if (config.has_transparent()) { + addListenSocketOptions(Network::SocketOptionFactory::buildIpTransparentOptions()); + } + if (config.has_freebind()) { + addListenSocketOptions(Network::SocketOptionFactory::buildIpFreebindOptions()); + } + if (config.has_tcp_fast_open_queue_length()) { + addListenSocketOptions(Network::SocketOptionFactory::buildTcpFastOpenOptions( + config.tcp_fast_open_queue_length().value())); + } if (!config.listener_filters().empty()) { listener_filter_factories_ = @@ -287,21 +294,20 @@ void ListenerImpl::setSocket(const Network::SocketSharedPtr& socket) { // Server config validation sets nullptr sockets. if (socket_ && listen_socket_options_) { // 'pre_bind = false' as bind() is never done after this. - for (const auto& option : *listen_socket_options_) { - bool ok = option->setOption(*socket_, Network::Socket::SocketState::PostBind); - const std::string message = - fmt::format("{}: Setting socket options {}", name_, ok ? "succeeded" : "failed"); - if (!ok) { - ENVOY_LOG(warn, "{}", message); - throw EnvoyException(message); - } else { - ENVOY_LOG(debug, "{}", message); - } - - // Add the options to the socket_ so that SocketState::Listening options can be - // set in the worker after listen()/evconnlistener_new() is called. - socket_->addOption(option); + bool ok = Network::Socket::applyOptions(listen_socket_options_, *socket_, + Network::Socket::SocketState::PostBind); + const std::string message = + fmt::format("{}: Setting socket options {}", name_, ok ? "succeeded" : "failed"); + if (!ok) { + ENVOY_LOG(warn, "{}", message); + throw EnvoyException(message); + } else { + ENVOY_LOG(debug, "{}", message); } + + // Add the options to the socket_ so that SocketState::Listening options can be + // set in the worker after listen()/evconnlistener_new() is called. + socket_->addOptions(listen_socket_options_); } } diff --git a/source/server/listener_manager_impl.h b/source/server/listener_manager_impl.h index 856b7e3877b91..ae4bc40ab6f43 100644 --- a/source/server/listener_manager_impl.h +++ b/source/server/listener_manager_impl.h @@ -249,13 +249,20 @@ class ListenerImpl : public Network::ListenerConfig, ThreadLocal::Instance& threadLocal() override { return parent_.server_.threadLocal(); } Admin& admin() override { return parent_.server_.admin(); } const envoy::api::v2::core::Metadata& listenerMetadata() const override { return metadata_; }; - void addListenSocketOption(const Network::Socket::OptionConstSharedPtr& option) override { + void ensureSocketOptions() { if (!listen_socket_options_) { listen_socket_options_ = std::make_shared>(); } + } + void addListenSocketOption(const Network::Socket::OptionConstSharedPtr& option) override { + ensureSocketOptions(); listen_socket_options_->emplace_back(std::move(option)); } + void addListenSocketOptions(const Network::Socket::OptionsSharedPtr& options) override { + ensureSocketOptions(); + Network::Socket::appendOptions(listen_socket_options_, options); + } // Network::DrainDecision bool drainClose() const override; diff --git a/source/server/listener_socket_option_impl.cc b/source/server/listener_socket_option_impl.cc deleted file mode 100644 index a69fbe7fb8ef7..0000000000000 --- a/source/server/listener_socket_option_impl.cc +++ /dev/null @@ -1,53 +0,0 @@ -#include "server/listener_socket_option_impl.h" - -#include "common/api/os_sys_calls_impl.h" - -namespace Envoy { -namespace Server { - -ListenerSocketOptionImpl::ListenerSocketOptionImpl(const envoy::api::v2::Listener& config) - : Network::SocketOptionImpl( - PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, transparent, absl::optional{}), - PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, freebind, absl::optional{})), - tcp_fast_open_queue_length_(PROTOBUF_GET_WRAPPED_OR_DEFAULT( - config, tcp_fast_open_queue_length, absl::optional{})) {} - -ListenerSocketOptionImpl::ListenerSocketOptionImpl( - absl::optional transparent, absl::optional freebind, - absl::optional tcp_fast_open_queue_length) - : Network::SocketOptionImpl(transparent, freebind), - tcp_fast_open_queue_length_(tcp_fast_open_queue_length) {} - -bool ListenerSocketOptionImpl::setOption(Network::Socket& socket, - Network::Socket::SocketState state) const { - bool result = Network::SocketOptionImpl::setOption(socket, state); - if (!result) { - return result; - } - - if (state == Network::Socket::SocketState::Listening) { - if (tcp_fast_open_queue_length_.has_value()) { - const int tfo_value = tcp_fast_open_queue_length_.value(); - const Network::SocketOptionName option_name = ENVOY_SOCKET_TCP_FASTOPEN; - if (option_name) { - const int error = Api::OsSysCallsSingleton::get().setsockopt( - socket.fd(), IPPROTO_TCP, option_name.value(), - reinterpret_cast(&tfo_value), sizeof(tfo_value)); - if (error != 0) { - ENVOY_LOG(warn, "Setting TCP_FASTOPEN on listener socket failed: {}", strerror(errno)); - return false; - } else { - ENVOY_LOG(debug, "Successfully set socket option TCP_FASTOPEN to {}", tfo_value); - } - } else { - ENVOY_LOG(warn, "Unsupported socket option TCP_FASTOPEN"); - return false; - } - } - } - - return true; -} - -} // namespace Server -} // namespace Envoy diff --git a/source/server/listener_socket_option_impl.h b/source/server/listener_socket_option_impl.h deleted file mode 100644 index 0ffaa806a1c3b..0000000000000 --- a/source/server/listener_socket_option_impl.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include - -#include "envoy/api/v2/listener/listener.pb.h" - -#include "common/network/listen_socket_impl.h" -#include "common/network/socket_option_impl.h" - -#include "server/configuration_impl.h" - -namespace Envoy { -namespace Server { - -// Socket::Option implementation for API-defined listener socket options. -// This same object can be extended to handle additional listener socket options. -class ListenerSocketOptionImpl : public Network::SocketOptionImpl { -public: - ListenerSocketOptionImpl(const envoy::api::v2::Listener& config); - - ListenerSocketOptionImpl(absl::optional transparent, absl::optional freebind, - absl::optional tcp_fast_open_queue_length); - - // Socket::Option - bool setOption(Network::Socket& socket, Network::Socket::SocketState state) const override; - -private: - const absl::optional tcp_fast_open_queue_length_; -}; - -} // namespace Server -} // namespace Envoy diff --git a/test/common/network/BUILD b/test/common/network/BUILD index d76e0cb6b7f53..15d00699e36a5 100644 --- a/test/common/network/BUILD +++ b/test/common/network/BUILD @@ -158,8 +158,8 @@ envoy_cc_test_library( srcs = ["socket_option_test.h"], deps = [ "//source/common/network:address_lib", + "//source/common/network:socket_option_factory_lib", "//source/common/network:socket_option_lib", - "//source/server:listener_socket_option_lib", "//test/mocks/api:api_mocks", "//test/mocks/network:network_mocks", "//test/test_common:logging_lib", @@ -176,6 +176,15 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "addr_family_aware_socket_option_impl_test", + srcs = ["addr_family_aware_socket_option_impl_test.cc"], + deps = [ + ":socket_option_test", + "//source/common/network:addr_family_aware_socket_option_lib", + ], +) + envoy_cc_test( name = "utility_test", srcs = ["utility_test.cc"], diff --git a/test/common/network/addr_family_aware_socket_option_impl_test.cc b/test/common/network/addr_family_aware_socket_option_impl_test.cc new file mode 100644 index 0000000000000..8835c8979fc0b --- /dev/null +++ b/test/common/network/addr_family_aware_socket_option_impl_test.cc @@ -0,0 +1,110 @@ +#include "common/network/addr_family_aware_socket_option_impl.h" + +#include "test/common/network/socket_option_test.h" + +namespace Envoy { +namespace Network { +namespace { + +class AddrFailyAwareSocketOptionImplTest : public SocketOptionTest {}; + +// We fail to set the option when the underlying setsockopt syscall fails. +TEST_F(AddrFailyAwareSocketOptionImplTest, SetOptionFailure) { + EXPECT_CALL(socket_, fd()).WillOnce(Return(-1)); + AddrFailyAwareSocketOptionImpl socket_option{ + Socket::SocketState::PreBind, Network::SocketOptionName(std::make_pair(5, 10)), {}, 1}; + EXPECT_LOG_CONTAINS( + "warning", "Failed to set IP socket option on non-IP socket", + EXPECT_FALSE(socket_option.setOption(socket_, Socket::SocketState::PreBind));); +} + +// If a platform supports IPv4 socket option variant for an IPv4 address, it works +TEST_F(AddrFailyAwareSocketOptionImplTest, SetOptionSuccess) { + Address::Ipv4Instance address("1.2.3.4", 5678); + const int fd = address.socket(Address::SocketType::Stream); + EXPECT_CALL(socket_, fd()).WillRepeatedly(Return(fd)); + + AddrFailyAwareSocketOptionImpl socket_option{ + Socket::SocketState::PreBind, Network::SocketOptionName(std::make_pair(5, 10)), {}, 1}; + testSetSocketOptionSuccess(socket_option, Network::SocketOptionName(std::make_pair(5, 10)), 1, + {Socket::SocketState::PreBind}); +} + +// If a platform doesn't support IPv4 socket option variant for an IPv4 address we fail +TEST_F(AddrFailyAwareSocketOptionImplTest, V4EmptyOptionNames) { + Address::Ipv4Instance address("1.2.3.4", 5678); + const int fd = address.socket(Address::SocketType::Stream); + EXPECT_CALL(socket_, fd()).WillRepeatedly(Return(fd)); + AddrFailyAwareSocketOptionImpl socket_option{Socket::SocketState::PreBind, {}, {}, 1}; + + EXPECT_LOG_CONTAINS("warning", "Setting option on socket failed: Operation not supported", + EXPECT_FALSE(socket_option.setOption(socket_, Socket::SocketState::PreBind))); +} + +// If a platform doesn't support IPv4 and IPv6 socket option variants for an IPv4 address, we fail +TEST_F(AddrFailyAwareSocketOptionImplTest, V6EmptyOptionNames) { + Address::Ipv6Instance address("::1:2:3:4", 5678); + const int fd = address.socket(Address::SocketType::Stream); + EXPECT_CALL(socket_, fd()).WillRepeatedly(Return(fd)); + AddrFailyAwareSocketOptionImpl socket_option{Socket::SocketState::PreBind, {}, {}, 1}; + + EXPECT_LOG_CONTAINS("warning", "Setting option on socket failed: Operation not supported", + EXPECT_FALSE(socket_option.setOption(socket_, Socket::SocketState::PreBind))); +} + +// If a platform suppports IPv4 and IPv6 socket option variants for an IPv4 address, we apply the +// IPv4 varient +TEST_F(AddrFailyAwareSocketOptionImplTest, V4IgnoreV6) { + Address::Ipv4Instance address("1.2.3.4", 5678); + const int fd = address.socket(Address::SocketType::Stream); + EXPECT_CALL(socket_, fd()).WillRepeatedly(Return(fd)); + + AddrFailyAwareSocketOptionImpl socket_option{Socket::SocketState::PreBind, + Network::SocketOptionName(std::make_pair(5, 10)), + Network::SocketOptionName(std::make_pair(6, 11)), 1}; + testSetSocketOptionSuccess(socket_option, Network::SocketOptionName(std::make_pair(5, 10)), 1, + {Socket::SocketState::PreBind}); +} + +// If a platform suppports IPv6 socket option variant for an IPv6 address it works +TEST_F(AddrFailyAwareSocketOptionImplTest, V6Only) { + Address::Ipv6Instance address("::1:2:3:4", 5678); + const int fd = address.socket(Address::SocketType::Stream); + EXPECT_CALL(socket_, fd()).WillRepeatedly(Return(fd)); + + AddrFailyAwareSocketOptionImpl socket_option{ + Socket::SocketState::PreBind, {}, Network::SocketOptionName(std::make_pair(6, 11)), 1}; + testSetSocketOptionSuccess(socket_option, Network::SocketOptionName(std::make_pair(6, 11)), 1, + {Socket::SocketState::PreBind}); +} + +// If a platform suppports only the IPv4 variant for an IPv6 address, +// we apply the IPv4 variant. +TEST_F(AddrFailyAwareSocketOptionImplTest, V6OnlyV4Fallback) { + Address::Ipv6Instance address("::1:2:3:4", 5678); + const int fd = address.socket(Address::SocketType::Stream); + EXPECT_CALL(socket_, fd()).WillRepeatedly(Return(fd)); + + AddrFailyAwareSocketOptionImpl socket_option{ + Socket::SocketState::PreBind, Network::SocketOptionName(std::make_pair(5, 10)), {}, 1}; + testSetSocketOptionSuccess(socket_option, Network::SocketOptionName(std::make_pair(5, 10)), 1, + {Socket::SocketState::PreBind}); +} + +// If a platform suppports IPv4 and IPv6 socket option variants for an IPv6 address, +// AddrFailyAwareSocketOptionImpl::setIpSocketOption() works with the IPv6 variant. +TEST_F(AddrFailyAwareSocketOptionImplTest, V6Precedence) { + Address::Ipv6Instance address("::1:2:3:4", 5678); + const int fd = address.socket(Address::SocketType::Stream); + EXPECT_CALL(socket_, fd()).WillRepeatedly(Return(fd)); + + AddrFailyAwareSocketOptionImpl socket_option{Socket::SocketState::PreBind, + Network::SocketOptionName(std::make_pair(5, 10)), + Network::SocketOptionName(std::make_pair(6, 11)), 1}; + testSetSocketOptionSuccess(socket_option, Network::SocketOptionName(std::make_pair(6, 11)), 1, + {Socket::SocketState::PreBind}); +} + +} // namespace +} // namespace Network +} // namespace Envoy diff --git a/test/common/network/socket_option_impl_test.cc b/test/common/network/socket_option_impl_test.cc index 39c7a84ddd07b..6c4c5f5746cbb 100644 --- a/test/common/network/socket_option_impl_test.cc +++ b/test/common/network/socket_option_impl_test.cc @@ -1,5 +1,4 @@ #include "test/common/network/socket_option_test.h" -#include "test/test_common/environment.h" namespace Envoy { namespace Network { @@ -7,149 +6,20 @@ namespace { class SocketOptionImplTest : public SocketOptionTest {}; -INSTANTIATE_TEST_CASE_P(IpVersions, SocketOptionImplTest, - testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), - TestUtility::ipTestParamsToString); - -// We fail to set the option if the socket FD is bad. -TEST_P(SocketOptionImplTest, BadFd) { - EXPECT_CALL(socket_, fd()).WillOnce(Return(-1)); - EXPECT_EQ(ENOTSUP, SocketOptionImpl::setIpSocketOption(socket_, {}, {}, nullptr, 0)); -} - -// Nop when there are no socket options set. -TEST_P(SocketOptionImplTest, SetOptionEmptyNop) { - SocketOptionImpl socket_option{{}, {}}; - EXPECT_TRUE(socket_option.setOption(socket_, Socket::SocketState::PreBind)); - EXPECT_TRUE(socket_option.setOption(socket_, Socket::SocketState::PostBind)); - EXPECT_TRUE(socket_option.setOption(socket_, Socket::SocketState::Listening)); -} - -// We fail to set the option when the underlying setsockopt syscall fails. -TEST_P(SocketOptionImplTest, SetOptionTransparentFailure) { - SocketOptionImpl socket_option{true, {}}; - EXPECT_FALSE(socket_option.setOption(socket_, Socket::SocketState::PreBind)); - EXPECT_LOG_CONTAINS( - "warning", "Failed to set IP socket option on non-IP socket", - EXPECT_EQ(ENOTSUP, SocketOptionImpl::setIpSocketOption(socket_, {}, {}, nullptr, 0))); -} - -// We fail to set the option when the underlying setsockopt syscall fails. -TEST_P(SocketOptionImplTest, SetOptionFreebindFailure) { - SocketOptionImpl socket_option{{}, true}; - EXPECT_FALSE(socket_option.setOption(socket_, Socket::SocketState::PreBind)); - EXPECT_LOG_CONTAINS( - "warning", "Failed to set IP socket option on non-IP socket", - EXPECT_EQ(ENOTSUP, SocketOptionImpl::setIpSocketOption(socket_, {}, {}, nullptr, 0))); -} - -// The happy path for setOption(); IP_TRANSPARENT is set to true. -TEST_P(SocketOptionImplTest, SetOptionTransparentSuccessTrue) { - SocketOptionImpl socket_option{true, {}}; - testSetSocketOptionSuccess(socket_option, IPPROTO_IP, ENVOY_SOCKET_IP_TRANSPARENT, 1, - {Socket::SocketState::PreBind, Socket::SocketState::PostBind}); -} - -// The happy path for setOption(); IP_FREEBIND is set to true. -TEST_P(SocketOptionImplTest, SetOptionFreebindSuccessTrue) { - SocketOptionImpl socket_option{{}, true}; - testSetSocketOptionSuccess(socket_option, IPPROTO_IP, ENVOY_SOCKET_IP_FREEBIND, 1, - {Socket::SocketState::PreBind}); -} - -// The happy path for setOption(); IP_TRANSPARENT is set to false. -TEST_P(SocketOptionImplTest, SetOptionTransparentSuccessFalse) { - SocketOptionImpl socket_option{false, {}}; - testSetSocketOptionSuccess(socket_option, IPPROTO_IP, ENVOY_SOCKET_IP_TRANSPARENT, 0, - {Socket::SocketState::PreBind, Socket::SocketState::PostBind}); -} - -// The happy path for setOption(); IP_FREEBIND is set to false. -TEST_P(SocketOptionImplTest, SetOptionFreebindSuccessFalse) { - SocketOptionImpl socket_option{{}, false}; - testSetSocketOptionSuccess(socket_option, IPPROTO_IP, ENVOY_SOCKET_IP_FREEBIND, 0, - {Socket::SocketState::PreBind}); -} - -// If a platform doesn't suppport IPv4 socket option variant for an IPv4 address, we fail -// SocketOptionImpl::setIpSocketOption(). -TEST_P(SocketOptionImplTest, V4EmptyOptionNames) { - Address::Ipv4Instance address("1.2.3.4", 5678); - const int fd = address.socket(Address::SocketType::Stream); - EXPECT_CALL(socket_, fd()).WillRepeatedly(Return(fd)); - EXPECT_EQ(-1, SocketOptionImpl::setIpSocketOption(socket_, {}, {}, nullptr, 0)); - EXPECT_EQ(ENOTSUP, errno); - EXPECT_LOG_CONTAINS( - "warning", "Unsupported IPv4 socket option", - EXPECT_EQ(-1, SocketOptionImpl::setIpSocketOption(socket_, {}, {}, nullptr, 0))); -} - -// If a platform doesn't suppport IPv4 and IPv6 socket option variants for an IPv4 address, we fail -// SocketOptionImpl::setIpSocketOption(). -TEST_P(SocketOptionImplTest, V6EmptyOptionNames) { - Address::Ipv6Instance address("::1:2:3:4", 5678); - const int fd = address.socket(Address::SocketType::Stream); - EXPECT_CALL(socket_, fd()).WillRepeatedly(Return(fd)); - EXPECT_EQ(-1, SocketOptionImpl::setIpSocketOption(socket_, {}, {}, nullptr, 0)); +TEST_F(SocketOptionImplTest, BadFd) { + EXPECT_EQ(-1, SocketOptionImpl::setSocketOption(socket_, {}, 0)); EXPECT_EQ(ENOTSUP, errno); - EXPECT_LOG_CONTAINS( - "warning", "Unsupported IPv6 socket option", - EXPECT_EQ(-1, SocketOptionImpl::setIpSocketOption(socket_, {}, {}, nullptr, 0))); -} - -// If a platform suppports IPv4 socket option variant for an IPv4 address, -// SocketOptionImpl::setIpSocketOption() works. -TEST_P(SocketOptionImplTest, V4Only) { - Address::Ipv4Instance address("1.2.3.4", 5678); - const int fd = address.socket(Address::SocketType::Stream); - EXPECT_CALL(socket_, fd()).WillRepeatedly(Return(fd)); - const int option = 42; - EXPECT_CALL(os_sys_calls_, setsockopt_(fd, IPPROTO_IP, 123, &option, sizeof(int))); - EXPECT_EQ(0, SocketOptionImpl::setIpSocketOption(socket_, {123}, {}, &option, sizeof(option))); } -// If a platform suppports IPv4 and IPv6 socket option variants for an IPv4 address, -// SocketOptionImpl::setIpSocketOption() works with the IPv4 variant. -TEST_P(SocketOptionImplTest, V4IgnoreV6) { - Address::Ipv4Instance address("1.2.3.4", 5678); - const int fd = address.socket(Address::SocketType::Stream); - EXPECT_CALL(socket_, fd()).WillRepeatedly(Return(fd)); - const int option = 42; - EXPECT_CALL(os_sys_calls_, setsockopt_(fd, IPPROTO_IP, 123, &option, sizeof(int))); - EXPECT_EQ(0, SocketOptionImpl::setIpSocketOption(socket_, {123}, {456}, &option, sizeof(option))); -} - -// If a platform suppports IPv6 socket option variant for an IPv6 address, -// SocketOptionImpl::setIpSocketOption() works. -TEST_P(SocketOptionImplTest, V6Only) { - Address::Ipv6Instance address("::1:2:3:4", 5678); - const int fd = address.socket(Address::SocketType::Stream); - EXPECT_CALL(socket_, fd()).WillRepeatedly(Return(fd)); - const int option = 42; - EXPECT_CALL(os_sys_calls_, setsockopt_(fd, IPPROTO_IPV6, 456, &option, sizeof(int))); - EXPECT_EQ(0, SocketOptionImpl::setIpSocketOption(socket_, {}, {456}, &option, sizeof(option))); -} - -// If a platform suppports only the IPv4 variant for an IPv6 address, -// SocketOptionImpl::setIpSocketOption() works with the IPv4 variant. -TEST_P(SocketOptionImplTest, V6OnlyV4Fallback) { - Address::Ipv6Instance address("::1:2:3:4", 5678); - const int fd = address.socket(Address::SocketType::Stream); - EXPECT_CALL(socket_, fd()).WillRepeatedly(Return(fd)); - const int option = 42; - EXPECT_CALL(os_sys_calls_, setsockopt_(fd, IPPROTO_IP, 123, &option, sizeof(int))); - EXPECT_EQ(0, SocketOptionImpl::setIpSocketOption(socket_, {123}, {}, &option, sizeof(option))); -} - -// If a platform suppports IPv4 and IPv6 socket option variants for an IPv6 address, -// SocketOptionImpl::setIpSocketOption() works with the IPv6 variant. -TEST_P(SocketOptionImplTest, V6Precedence) { - Address::Ipv6Instance address("::1:2:3:4", 5678); - const int fd = address.socket(Address::SocketType::Stream); - EXPECT_CALL(socket_, fd()).WillRepeatedly(Return(fd)); - const int option = 42; - EXPECT_CALL(os_sys_calls_, setsockopt_(fd, IPPROTO_IPV6, 456, &option, sizeof(int))); - EXPECT_EQ(0, SocketOptionImpl::setIpSocketOption(socket_, {123}, {456}, &option, sizeof(option))); +TEST_F(SocketOptionImplTest, SetOptionSuccessTrue) { + SocketOptionImpl socket_option{Socket::SocketState::PreBind, + Network::SocketOptionName(std::make_pair(5, 10)), 1}; + EXPECT_CALL(os_sys_calls_, setsockopt_(_, 5, 10, _, 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)); } } // namespace diff --git a/test/common/network/socket_option_test.h b/test/common/network/socket_option_test.h index f48fdcbda39f6..2028e6b5d4241 100644 --- a/test/common/network/socket_option_test.h +++ b/test/common/network/socket_option_test.h @@ -17,7 +17,7 @@ namespace Envoy { namespace Network { namespace { -class SocketOptionTest : public testing::TestWithParam { +class SocketOptionTest : public testing::Test { public: SocketOptionTest() { socket_.local_address_.reset(); } @@ -25,23 +25,13 @@ class SocketOptionTest : public testing::TestWithParam { Api::MockOsSysCalls os_sys_calls_; TestThreadsafeSingletonInjector os_calls{&os_sys_calls_}; - void testSetSocketOptionSuccess(SocketOptionImpl& socket_option, int socket_level, + void testSetSocketOptionSuccess(Socket::Option& socket_option, Network::SocketOptionName option_name, int option_val, const std::set& when) { - int fd; - if (GetParam() == Network::Address::IpVersion::v4) { - Address::Ipv4Instance address("1.2.3.4", 5678); - fd = address.socket(Address::SocketType::Stream); - } else { - Address::Ipv6Instance address("::1", 5678); - fd = address.socket(Address::SocketType::Stream); - } - EXPECT_CALL(socket_, fd()).WillRepeatedly(Return(fd)); - for (Socket::SocketState state : when) { if (option_name.has_value()) { - EXPECT_CALL(os_sys_calls_, - setsockopt_(_, socket_level, option_name.value(), _, sizeof(int))) + EXPECT_CALL(os_sys_calls_, setsockopt_(_, option_name.value().first, + option_name.value().second, _, sizeof(int))) .WillOnce(Invoke([option_val](int, int, int, const void* optval, socklen_t) -> int { EXPECT_EQ(option_val, *static_cast(optval)); return 0; diff --git a/test/common/upstream/cluster_manager_impl_test.cc b/test/common/upstream/cluster_manager_impl_test.cc index 504adccf3d4c9..5ed371441f17b 100644 --- a/test/common/upstream/cluster_manager_impl_test.cc +++ b/test/common/upstream/cluster_manager_impl_test.cc @@ -1306,18 +1306,18 @@ class FreebindTest : public ClusterManagerImplTest { void expectSetsockoptFreebind() { if (!ENVOY_SOCKET_IP_FREEBIND.has_value()) { EXPECT_CALL(factory_.tls_.dispatcher_, createClientConnection_(_, _, _, _)) - .WillOnce( - Invoke([this](Network::Address::InstanceConstSharedPtr, - Network::Address::InstanceConstSharedPtr, Network::TransportSocketPtr&, - const Network::ConnectionSocket::OptionsSharedPtr& options) - -> Network::ClientConnection* { - EXPECT_NE(nullptr, options.get()); - EXPECT_EQ(1, options->size()); - NiceMock socket; - EXPECT_FALSE( - (*options->begin())->setOption(socket, Network::Socket::SocketState::PreBind)); - return connection_; - })); + .WillOnce(Invoke([this](Network::Address::InstanceConstSharedPtr, + Network::Address::InstanceConstSharedPtr, + Network::TransportSocketPtr&, + const Network::ConnectionSocket::OptionsSharedPtr& options) + -> Network::ClientConnection* { + EXPECT_NE(nullptr, options.get()); + EXPECT_EQ(1, options->size()); + NiceMock socket; + EXPECT_FALSE((Network::Socket::applyOptions(options, socket, + Network::Socket::SocketState::PreBind))); + return connection_; + })); cluster_manager_->tcpConnForCluster("FreebindCluster", nullptr); return; } @@ -1332,12 +1332,12 @@ class FreebindTest : public ClusterManagerImplTest { EXPECT_NE(nullptr, options.get()); EXPECT_EQ(1, options->size()); NiceMock socket; - EXPECT_TRUE( - (*options->begin())->setOption(socket, Network::Socket::SocketState::PreBind)); + EXPECT_TRUE((Network::Socket::applyOptions(options, socket, + Network::Socket::SocketState::PreBind))); return connection_; })); - EXPECT_CALL(os_sys_calls, - setsockopt_(_, IPPROTO_IP, ENVOY_SOCKET_IP_FREEBIND.value(), _, sizeof(int))) + EXPECT_CALL(os_sys_calls, setsockopt_(_, ENVOY_SOCKET_IP_FREEBIND.value().first, + ENVOY_SOCKET_IP_FREEBIND.value().second, _, sizeof(int))) .WillOnce(Invoke([](int, int, int, const void* optval, socklen_t) -> int { EXPECT_EQ(1, *static_cast(optval)); return 0; @@ -1441,6 +1441,185 @@ TEST_F(FreebindTest, FreebindClusterOverride) { expectSetsockoptFreebind(); } +// Validate that when tcp keepalives are set in the Cluster, 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 +// tcp_keepalive_option_impl_test.cc. +class TcpKeepaliveTest : public ClusterManagerImplTest { +public: + void initialize(const std::string& yaml) { create(parseBootstrapFromV2Yaml(yaml)); } + + void TearDown() override { factory_.tls_.shutdownThread(); } + + void expectSetsockoptSoKeepalive(absl::optional keepalive_probes, + absl::optional keepalive_time, + absl::optional keepalive_interval) { + if (!ENVOY_SOCKET_SO_KEEPALIVE.has_value()) { + EXPECT_CALL(factory_.tls_.dispatcher_, createClientConnection_(_, _, _, _)) + .WillOnce(Invoke([this](Network::Address::InstanceConstSharedPtr, + Network::Address::InstanceConstSharedPtr, + Network::TransportSocketPtr&, + const Network::ConnectionSocket::OptionsSharedPtr& options) + -> Network::ClientConnection* { + EXPECT_NE(nullptr, options.get()); + EXPECT_EQ(1, options->size()); + NiceMock socket; + EXPECT_FALSE((Network::Socket::applyOptions(options, socket, + Network::Socket::SocketState::PreBind))); + return connection_; + })); + cluster_manager_->tcpConnForCluster("TcpKeepaliveCluster", nullptr); + return; + } + NiceMock os_sys_calls; + TestThreadsafeSingletonInjector os_calls(&os_sys_calls); + EXPECT_CALL(factory_.tls_.dispatcher_, createClientConnection_(_, _, _, _)) + .WillOnce( + Invoke([this](Network::Address::InstanceConstSharedPtr, + Network::Address::InstanceConstSharedPtr, Network::TransportSocketPtr&, + const Network::ConnectionSocket::OptionsSharedPtr& options) + -> Network::ClientConnection* { + EXPECT_NE(nullptr, options.get()); + NiceMock socket; + EXPECT_TRUE((Network::Socket::applyOptions(options, socket, + Network::Socket::SocketState::PreBind))); + return connection_; + })); + EXPECT_CALL(os_sys_calls, setsockopt_(_, ENVOY_SOCKET_SO_KEEPALIVE.value().first, + ENVOY_SOCKET_SO_KEEPALIVE.value().second, _, sizeof(int))) + .WillOnce(Invoke([](int, int, int, const void* optval, socklen_t) -> int { + EXPECT_EQ(1, *static_cast(optval)); + return 0; + })); + if (keepalive_probes.has_value()) { + EXPECT_CALL(os_sys_calls, + setsockopt_(_, ENVOY_SOCKET_TCP_KEEPCNT.value().first, + ENVOY_SOCKET_TCP_KEEPCNT.value().second, _, sizeof(int))) + .WillOnce( + Invoke([&keepalive_probes](int, int, int, const void* optval, socklen_t) -> int { + EXPECT_EQ(keepalive_probes.value(), *static_cast(optval)); + return 0; + })); + } + if (keepalive_time.has_value()) { + EXPECT_CALL(os_sys_calls, + setsockopt_(_, ENVOY_SOCKET_TCP_KEEPIDLE.value().first, + ENVOY_SOCKET_TCP_KEEPIDLE.value().second, _, sizeof(int))) + .WillOnce(Invoke([&keepalive_time](int, int, int, const void* optval, socklen_t) -> int { + EXPECT_EQ(keepalive_time.value(), *static_cast(optval)); + return 0; + })); + } + if (keepalive_interval.has_value()) { + EXPECT_CALL(os_sys_calls, + setsockopt_(_, ENVOY_SOCKET_TCP_KEEPINTVL.value().first, + ENVOY_SOCKET_TCP_KEEPINTVL.value().second, _, sizeof(int))) + .WillOnce( + Invoke([&keepalive_interval](int, int, int, const void* optval, socklen_t) -> int { + EXPECT_EQ(keepalive_interval.value(), *static_cast(optval)); + return 0; + })); + } + auto conn_data = cluster_manager_->tcpConnForCluster("TcpKeepaliveCluster", nullptr); + EXPECT_EQ(connection_, conn_data.connection_.get()); + } + + void expectNoSocketOptions() { + EXPECT_CALL(factory_.tls_.dispatcher_, createClientConnection_(_, _, _, _)) + .WillOnce( + Invoke([this](Network::Address::InstanceConstSharedPtr, + Network::Address::InstanceConstSharedPtr, Network::TransportSocketPtr&, + const Network::ConnectionSocket::OptionsSharedPtr& options) + -> Network::ClientConnection* { + EXPECT_EQ(nullptr, options.get()); + return connection_; + })); + auto conn_data = cluster_manager_->tcpConnForCluster("TcpKeepaliveCluster", nullptr); + EXPECT_EQ(connection_, conn_data.connection_.get()); + } + + Network::MockClientConnection* connection_ = new NiceMock(); +}; + +TEST_F(TcpKeepaliveTest, TcpKeepaliveUnset) { + const std::string yaml = R"EOF( + static_resources: + clusters: + - name: TcpKeepaliveCluster + connect_timeout: 0.250s + lb_policy: ROUND_ROBIN + type: STATIC + hosts: + - socket_address: + address: "127.0.0.1" + port_value: 11001 + )EOF"; + initialize(yaml); + expectNoSocketOptions(); +} + +TEST_F(TcpKeepaliveTest, TcpKeepaliveCluster) { + const std::string yaml = R"EOF( + static_resources: + clusters: + - name: TcpKeepaliveCluster + connect_timeout: 0.250s + lb_policy: ROUND_ROBIN + type: STATIC + hosts: + - socket_address: + address: "127.0.0.1" + port_value: 11001 + upstream_connection_options: + tcp_keepalive: {} + )EOF"; + initialize(yaml); + expectSetsockoptSoKeepalive({}, {}, {}); +} + +TEST_F(TcpKeepaliveTest, TcpKeepaliveClusterProbes) { + const std::string yaml = R"EOF( + static_resources: + clusters: + - name: TcpKeepaliveCluster + connect_timeout: 0.250s + lb_policy: ROUND_ROBIN + type: STATIC + hosts: + - socket_address: + address: "127.0.0.1" + port_value: 11001 + upstream_connection_options: + tcp_keepalive: + keepalive_probes: 7 + )EOF"; + initialize(yaml); + expectSetsockoptSoKeepalive(7, {}, {}); +} + +TEST_F(TcpKeepaliveTest, TcpKeepaliveWithAllOptions) { + const std::string yaml = R"EOF( + static_resources: + clusters: + - name: TcpKeepaliveCluster + connect_timeout: 0.250s + lb_policy: ROUND_ROBIN + type: STATIC + hosts: + - socket_address: + address: "127.0.0.1" + port_value: 11001 + upstream_connection_options: + tcp_keepalive: + keepalive_probes: 7 + keepalive_time: 4 + keepalive_interval: 1 + )EOF"; + initialize(yaml); + expectSetsockoptSoKeepalive(7, 4, 1); +} + } // namespace } // namespace Upstream } // namespace Envoy diff --git a/test/mocks/network/mocks.h b/test/mocks/network/mocks.h index c562e7db52ab3..e4d216ab0e3b4 100644 --- a/test/mocks/network/mocks.h +++ b/test/mocks/network/mocks.h @@ -272,11 +272,13 @@ class MockListenSocket : public Socket { ~MockListenSocket(); void addOption(const Socket::OptionConstSharedPtr& option) override { addOption_(option); } + void addOptions(const Socket::OptionsSharedPtr& options) override { addOptions_(options); } MOCK_CONST_METHOD0(localAddress, const Address::InstanceConstSharedPtr&()); MOCK_CONST_METHOD0(fd, int()); MOCK_METHOD0(close, void()); MOCK_METHOD1(addOption_, void(const Socket::OptionConstSharedPtr& option)); + MOCK_METHOD1(addOptions_, void(const Socket::OptionsSharedPtr& options)); MOCK_CONST_METHOD0(options, const OptionsSharedPtr&()); Address::InstanceConstSharedPtr local_address_; @@ -298,6 +300,7 @@ class MockConnectionSocket : public ConnectionSocket { ~MockConnectionSocket(); void addOption(const Socket::OptionConstSharedPtr& option) override { addOption_(option); } + void addOptions(const Socket::OptionsSharedPtr& options) override { addOptions_(options); } MOCK_CONST_METHOD0(localAddress, const Address::InstanceConstSharedPtr&()); MOCK_METHOD2(setLocalAddress, void(const Address::InstanceConstSharedPtr&, bool)); @@ -309,6 +312,7 @@ class MockConnectionSocket : public ConnectionSocket { MOCK_METHOD1(setRequestedServerName, void(absl::string_view)); MOCK_CONST_METHOD0(requestedServerName, absl::string_view()); MOCK_METHOD1(addOption_, void(const Socket::OptionConstSharedPtr&)); + MOCK_METHOD1(addOptions_, void(const Socket::OptionsSharedPtr&)); MOCK_CONST_METHOD0(options, const Network::ConnectionSocket::OptionsSharedPtr&()); MOCK_CONST_METHOD0(fd, int()); MOCK_METHOD0(close, void()); diff --git a/test/mocks/server/mocks.cc b/test/mocks/server/mocks.cc index 83159bf7eb855..52f0d67df0679 100644 --- a/test/mocks/server/mocks.cc +++ b/test/mocks/server/mocks.cc @@ -66,12 +66,9 @@ MockListenerComponentFactory::MockListenerComponentFactory() .WillByDefault(Invoke([&](Network::Address::InstanceConstSharedPtr, const Network::Socket::OptionsSharedPtr& options, bool) -> Network::SocketSharedPtr { - if (options) { - for (const auto& option : *options) { - if (!option->setOption(*socket_, Network::Socket::SocketState::PreBind)) { - throw EnvoyException("MockListenerComponentFactory: Setting socket options failed"); - } - } + if (!Network::Socket::applyOptions(options, *socket_, + Network::Socket::SocketState::PreBind)) { + throw EnvoyException("MockListenerComponentFactory: Setting socket options failed"); } return socket_; })); diff --git a/test/mocks/server/mocks.h b/test/mocks/server/mocks.h index fa0bdefa778a4..927d741e307b9 100644 --- a/test/mocks/server/mocks.h +++ b/test/mocks/server/mocks.h @@ -396,6 +396,10 @@ class MockListenerFactoryContext : public virtual MockFactoryContext, addListenSocketOption_(option); } MOCK_METHOD1(addListenSocketOption_, void(const Network::Socket::OptionConstSharedPtr&)); + void addListenSocketOptions(const Network::Socket::OptionsSharedPtr& options) override { + addListenSocketOptions_(options); + } + MOCK_METHOD1(addListenSocketOptions_, void(const Network::Socket::OptionsSharedPtr&)); }; class MockHealthCheckerFactoryContext : public virtual HealthCheckerFactoryContext { diff --git a/test/mocks/upstream/cluster_info.cc b/test/mocks/upstream/cluster_info.cc index a6a9ed74a93d0..c0fe5cbda3b71 100644 --- a/test/mocks/upstream/cluster_info.cc +++ b/test/mocks/upstream/cluster_info.cc @@ -52,6 +52,7 @@ MockClusterInfo::MockClusterInfo() ON_CALL(*this, lbSubsetInfo()).WillByDefault(ReturnRef(lb_subset_)); ON_CALL(*this, lbRingHashConfig()).WillByDefault(ReturnRef(lb_ring_hash_config_)); ON_CALL(*this, lbConfig()).WillByDefault(ReturnRef(lb_config_)); + ON_CALL(*this, clusterSocketOptions()).WillByDefault(ReturnRef(cluster_socket_options_)); } MockClusterInfo::~MockClusterInfo() {} diff --git a/test/mocks/upstream/cluster_info.h b/test/mocks/upstream/cluster_info.h index 0db432f131829..5a6017ce5852c 100644 --- a/test/mocks/upstream/cluster_info.h +++ b/test/mocks/upstream/cluster_info.h @@ -62,6 +62,7 @@ class MockClusterInfo : public ClusterInfo { MOCK_CONST_METHOD0(sourceAddress, const Network::Address::InstanceConstSharedPtr&()); MOCK_CONST_METHOD0(lbSubsetInfo, const LoadBalancerSubsetInfo&()); MOCK_CONST_METHOD0(metadata, const envoy::api::v2::core::Metadata&()); + MOCK_CONST_METHOD0(clusterSocketOptions, const Network::ConnectionSocket::OptionsSharedPtr&()); std::string name_{"fake_cluster"}; Http::Http2Settings http2_settings_{}; @@ -78,6 +79,7 @@ class MockClusterInfo : public ClusterInfo { envoy::api::v2::Cluster::DiscoveryType type_{envoy::api::v2::Cluster::STRICT_DNS}; NiceMock lb_subset_; absl::optional lb_ring_hash_config_; + Network::ConnectionSocket::OptionsSharedPtr cluster_socket_options_; envoy::api::v2::Cluster::CommonLbConfig lb_config_; }; diff --git a/test/server/BUILD b/test/server/BUILD index bb722d2bd08c1..e4b68c1763f5b 100644 --- a/test/server/BUILD +++ b/test/server/BUILD @@ -131,6 +131,7 @@ envoy_cc_test( ":utility_lib", "//source/common/api:os_sys_calls_lib", "//source/common/config:metadata_lib", + "//source/common/network:addr_family_aware_socket_option_lib", "//source/common/network:listen_socket_lib", "//source/common/network:socket_option_lib", "//source/common/network:utility_lib", @@ -147,16 +148,6 @@ envoy_cc_test( ], ) -envoy_cc_test( - name = "listener_socket_option_impl_test", - srcs = ["listener_socket_option_impl_test.cc"], - deps = [ - "//source/server:listener_socket_option_lib", - "//test/common/network:socket_option_test", - "//test/test_common:environment_lib", - ], -) - envoy_cc_fuzz_test( name = "server_fuzz_test", srcs = ["server_fuzz_test.cc"], diff --git a/test/server/listener_manager_impl_test.cc b/test/server/listener_manager_impl_test.cc index 2febf4b5b1c85..083d99f8ff898 100644 --- a/test/server/listener_manager_impl_test.cc +++ b/test/server/listener_manager_impl_test.cc @@ -1411,7 +1411,7 @@ TEST_F(ListenerManagerImplWithRealFiltersTest, TransparentFreebindListenerDisabl .WillOnce(Invoke([&](Network::Address::InstanceConstSharedPtr, const Network::Socket::OptionsSharedPtr& options, bool) -> Network::SocketSharedPtr { - EXPECT_NE(options.get(), nullptr); + EXPECT_EQ(options, nullptr); return listener_factory_.socket_; })); manager_->addOrUpdateListener(parseListenerFromV2Yaml(yaml), true); @@ -1443,15 +1443,15 @@ TEST_F(ListenerManagerImplWithRealFiltersTest, TransparentListenerEnabled) { 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)); + EXPECT_EQ(options->size(), 2); + EXPECT_TRUE(Network::Socket::applyOptions(options, *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))) + setsockopt_(_, ENVOY_SOCKET_IP_TRANSPARENT.value().first, + ENVOY_SOCKET_IP_TRANSPARENT.value().second, _, sizeof(int))) .Times(2) .WillRepeatedly(Invoke([](int, int, int, const void* optval, socklen_t) -> int { EXPECT_EQ(1, *static_cast(optval)); @@ -1494,13 +1494,12 @@ TEST_F(ListenerManagerImplWithRealFiltersTest, FreebindListenerEnabled) { 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)); + EXPECT_TRUE(Network::Socket::applyOptions(options, *listener_factory_.socket_, + Network::Socket::SocketState::PreBind)); return listener_factory_.socket_; })); - EXPECT_CALL(os_sys_calls, - setsockopt_(_, IPPROTO_IP, ENVOY_SOCKET_IP_FREEBIND.value(), _, sizeof(int))) + EXPECT_CALL(os_sys_calls, setsockopt_(_, ENVOY_SOCKET_IP_FREEBIND.value().first, + ENVOY_SOCKET_IP_FREEBIND.value().second, _, sizeof(int))) .WillOnce(Invoke([](int, int, int, const void* optval, socklen_t) -> int { EXPECT_EQ(1, *static_cast(optval)); return 0; diff --git a/test/server/listener_socket_option_impl_test.cc b/test/server/listener_socket_option_impl_test.cc deleted file mode 100644 index bdd2433bd90cf..0000000000000 --- a/test/server/listener_socket_option_impl_test.cc +++ /dev/null @@ -1,48 +0,0 @@ -#include "server/listener_socket_option_impl.h" - -#include "test/common/network/socket_option_test.h" -#include "test/test_common/environment.h" - -namespace Envoy { -namespace Server { - -class ListenerSocketOptionImplTest : public Network::SocketOptionTest { -public: - void testSetSocketOptionSuccess(ListenerSocketOptionImpl& socket_option, int socket_level, - Network::SocketOptionName option_name, int option_val, - const std::set& when) { - Network::SocketOptionTest::testSetSocketOptionSuccess(socket_option, socket_level, option_name, - option_val, when); - } -}; - -INSTANTIATE_TEST_CASE_P(IpVersions, ListenerSocketOptionImplTest, - testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), - TestUtility::ipTestParamsToString); - -// We fail to set the tcp-fastopen option when the underlying setsockopt syscall fails. -TEST_P(ListenerSocketOptionImplTest, SetOptionTcpFastopenFailure) { - if (ENVOY_SOCKET_TCP_FASTOPEN.has_value()) { - ListenerSocketOptionImpl socket_option{{}, {}, 1}; - EXPECT_CALL(os_sys_calls_, setsockopt_(_, IPPROTO_TCP, ENVOY_SOCKET_TCP_FASTOPEN.value(), _, _)) - .WillOnce(Return(-1)); - EXPECT_FALSE(socket_option.setOption(socket_, Network::Socket::SocketState::Listening)); - } -} - -// The happy path for setOption(); TCP_FASTOPEN is set to true. -TEST_P(ListenerSocketOptionImplTest, SetOptionTcpFastopenSuccessTrue) { - ListenerSocketOptionImpl socket_option{{}, {}, 42}; - testSetSocketOptionSuccess(socket_option, IPPROTO_TCP, ENVOY_SOCKET_TCP_FASTOPEN, 42, - {Network::Socket::SocketState::Listening}); -} - -// The happy path for setOption(); TCP_FASTOPEN is set to false. -TEST_P(ListenerSocketOptionImplTest, SetOptionTcpFastopenSuccessFalse) { - ListenerSocketOptionImpl socket_option{{}, {}, 0}; - testSetSocketOptionSuccess(socket_option, IPPROTO_TCP, ENVOY_SOCKET_TCP_FASTOPEN, 0, - {Network::Socket::SocketState::Listening}); -} - -} // namespace Server -} // namespace Envoy