diff --git a/include/envoy/network/listen_socket.h b/include/envoy/network/listen_socket.h index a72ac116da0c1..6b475c318e13c 100644 --- a/include/envoy/network/listen_socket.h +++ b/include/envoy/network/listen_socket.h @@ -9,10 +9,15 @@ #include "envoy/network/address.h" #include "absl/strings/string_view.h" +#include "absl/types/optional.h" namespace Envoy { 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; + /** * Base class for Sockets */ @@ -57,7 +62,29 @@ class Socket { * not be modified. */ virtual void hashKey(std::vector& key) const PURE; + + /** + * Contains details about what this option applies to a socket. + */ + struct Details { + SocketOptionName name_; + std::string value_; ///< Binary string representation of an option's value. + + bool operator==(const Details& other) const { + return name_ == other.name_ && value_ == other.value_; + } + }; + + /** + * @param socket The socket for which we want to know the options that would be applied. + * @param state The state at which we would apply the options. + * @return What we would apply to the socket at the provided state. Empty if we'd apply nothing. + */ + virtual absl::optional
+ getOptionDetails(const Socket& socket, + envoy::api::v2::core::SocketOption::SocketState state) const PURE; }; + typedef std::shared_ptr OptionConstSharedPtr; typedef std::vector Options; typedef std::shared_ptr OptionsSharedPtr; diff --git a/source/common/network/addr_family_aware_socket_option_impl.cc b/source/common/network/addr_family_aware_socket_option_impl.cc index 872a5304230d9..1f86a8b5bc648 100644 --- a/source/common/network/addr_family_aware_socket_option_impl.cc +++ b/source/common/network/addr_family_aware_socket_option_impl.cc @@ -10,50 +10,78 @@ namespace Envoy { namespace Network { -bool AddrFamilyAwareSocketOptionImpl::setOption( - Socket& socket, envoy::api::v2::core::SocketOption::SocketState state) const { - return setIpSocketOption(socket, state, ipv4_option_, ipv6_option_); -} - -bool AddrFamilyAwareSocketOptionImpl::setIpSocketOption( - Socket& socket, envoy::api::v2::core::SocketOption::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; +namespace { +absl::optional getVersionFromSocket(const Socket& socket) { 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(); + return absl::optional(socket.localAddress()->ip()->version()); } else { - address = Address::addressFromFd(socket.fd()); - ip = address->ip(); + return absl::optional( + Address::addressFromFd(socket.fd())->ip()->version()); } } 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; + + return absl::nullopt; +} + +absl::optional> +getOptionForSocket(const Socket& socket, SocketOptionImpl& ipv4_option, + SocketOptionImpl& ipv6_option) { + auto version = getVersionFromSocket(socket); + if (!version.has_value()) { + return absl::nullopt; } // 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 (*version == Network::Address::IpVersion::v4) { + return absl::optional>(ipv4_option); } - // If the FD is v6, we first try the IPv6 variant if the platform 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); + ASSERT(*version == Network::Address::IpVersion::v6); + if (ipv6_option.isSupported()) { + return absl::optional>(ipv6_option); } - return ipv4_option->setOption(socket, state); + return absl::optional>(ipv4_option); +} + +} // namespace + +bool AddrFamilyAwareSocketOptionImpl::setOption( + Socket& socket, envoy::api::v2::core::SocketOption::SocketState state) const { + return setIpSocketOption(socket, state, ipv4_option_, ipv6_option_); +} + +absl::optional AddrFamilyAwareSocketOptionImpl::getOptionDetails( + const Socket& socket, envoy::api::v2::core::SocketOption::SocketState state) const { + auto option = getOptionForSocket(socket, *ipv4_option_, *ipv6_option_); + + if (!option.has_value()) { + return absl::nullopt; + } + + return option->get().getOptionDetails(socket, state); +} + +bool AddrFamilyAwareSocketOptionImpl::setIpSocketOption( + Socket& socket, envoy::api::v2::core::SocketOption::SocketState state, + const std::unique_ptr& ipv4_option, + const std::unique_ptr& ipv6_option) { + auto option = getOptionForSocket(socket, *ipv4_option, *ipv6_option); + + if (!option.has_value()) { + ENVOY_LOG(warn, "Failed to set IP socket option on non-IP socket"); + return false; + } + + return option->get().setOption(socket, state); } } // namespace Network diff --git a/source/common/network/addr_family_aware_socket_option_impl.h b/source/common/network/addr_family_aware_socket_option_impl.h index ea62e099afd8c..c99ac03de556f 100644 --- a/source/common/network/addr_family_aware_socket_option_impl.h +++ b/source/common/network/addr_family_aware_socket_option_impl.h @@ -29,6 +29,10 @@ class AddrFamilyAwareSocketOptionImpl : public Socket::Option, // The common socket options don't require a hash key. void hashKey(std::vector&) const override {} + absl::optional
+ getOptionDetails(const Socket& socket, + envoy::api::v2::core::SocketOption::SocketState state) 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 diff --git a/source/common/network/socket_option_impl.cc b/source/common/network/socket_option_impl.cc index a3d9ced31524a..f9b69d6d15a18 100644 --- a/source/common/network/socket_option_impl.cc +++ b/source/common/network/socket_option_impl.cc @@ -23,6 +23,19 @@ bool SocketOptionImpl::setOption(Socket& socket, return true; } +absl::optional +SocketOptionImpl::getOptionDetails(const Socket&, + envoy::api::v2::core::SocketOption::SocketState state) const { + if (state != in_state_ || !isSupported()) { + return absl::nullopt; + } + + Socket::Option::Details info; + info.name_ = optname_; + info.value_ = value_; + return absl::optional(std::move(info)); +} + bool SocketOptionImpl::isSupported() const { return optname_.has_value(); } Api::SysCallIntResult SocketOptionImpl::setSocketOption(Socket& socket, diff --git a/source/common/network/socket_option_impl.h b/source/common/network/socket_option_impl.h index cd233e109c5bf..4a23153d1fa86 100644 --- a/source/common/network/socket_option_impl.h +++ b/source/common/network/socket_option_impl.h @@ -10,15 +10,9 @@ #include "common/common/logger.h" -#include "absl/types/optional.h" - namespace Envoy { 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; - #ifdef IP_TRANSPARENT #define ENVOY_SOCKET_IP_TRANSPARENT \ Network::SocketOptionName(std::make_pair(IPPROTO_IP, IP_TRANSPARENT)) @@ -101,6 +95,10 @@ class SocketOptionImpl : public Socket::Option, Logger::Loggable&) const override {} + absl::optional
+ getOptionDetails(const Socket& socket, + envoy::api::v2::core::SocketOption::SocketState state) const override; + bool isSupported() const; /** 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 index ddf9292bd644f..c4722a4a2f1da 100644 --- a/test/common/network/addr_family_aware_socket_option_impl_test.cc +++ b/test/common/network/addr_family_aware_socket_option_impl_test.cc @@ -1,4 +1,5 @@ #include "common/network/addr_family_aware_socket_option_impl.h" +#include "common/network/utility.h" #include "test/common/network/socket_option_test.h" @@ -119,6 +120,59 @@ TEST_F(AddrFamilyAwareSocketOptionImplTest, V6Precedence) { {envoy::api::v2::core::SocketOption::STATE_PREBIND}); } +// GetSocketOptionName returns the v4 information for a v4 address +TEST_F(AddrFamilyAwareSocketOptionImplTest, V4GetSocketOptionName) { + socket_.local_address_ = Utility::parseInternetAddress("1.2.3.4", 5678); + + AddrFamilyAwareSocketOptionImpl socket_option{envoy::api::v2::core::SocketOption::STATE_PREBIND, + Network::SocketOptionName(std::make_pair(5, 10)), + Network::SocketOptionName(std::make_pair(6, 11)), + 1}; + auto result = + socket_option.getOptionDetails(socket_, envoy::api::v2::core::SocketOption::STATE_PREBIND); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), makeDetails(std::make_pair(5, 10), 1)); +} + +// GetSocketOptionName returns the v4 information for a v6 address +TEST_F(AddrFamilyAwareSocketOptionImplTest, V6GetSocketOptionName) { + socket_.local_address_ = Utility::parseInternetAddress("2::1", 5678); + + AddrFamilyAwareSocketOptionImpl socket_option{envoy::api::v2::core::SocketOption::STATE_PREBIND, + Network::SocketOptionName(std::make_pair(5, 10)), + Network::SocketOptionName(std::make_pair(6, 11)), + 5}; + auto result = + socket_option.getOptionDetails(socket_, envoy::api::v2::core::SocketOption::STATE_PREBIND); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), makeDetails(std::make_pair(6, 11), 5)); +} + +// GetSocketOptionName returns nullopt if the state is wrong +TEST_F(AddrFamilyAwareSocketOptionImplTest, GetSocketOptionWrongState) { + socket_.local_address_ = Utility::parseInternetAddress("2::1", 5678); + + AddrFamilyAwareSocketOptionImpl socket_option{envoy::api::v2::core::SocketOption::STATE_PREBIND, + Network::SocketOptionName(std::make_pair(5, 10)), + Network::SocketOptionName(std::make_pair(6, 11)), + 5}; + auto result = + socket_option.getOptionDetails(socket_, envoy::api::v2::core::SocketOption::STATE_BOUND); + EXPECT_FALSE(result.has_value()); +} + +// GetSocketOptionName returns nullopt if the version could not be determined +TEST_F(AddrFamilyAwareSocketOptionImplTest, GetSocketOptionCannotDetermineVersion) { + AddrFamilyAwareSocketOptionImpl socket_option{envoy::api::v2::core::SocketOption::STATE_PREBIND, + Network::SocketOptionName(std::make_pair(5, 10)), + Network::SocketOptionName(std::make_pair(6, 11)), + 5}; + + EXPECT_CALL(socket_, fd()).WillOnce(Return(-1)); + auto result = + socket_option.getOptionDetails(socket_, envoy::api::v2::core::SocketOption::STATE_PREBIND); + EXPECT_FALSE(result.has_value()); +} } // 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 d3fffd89b35b7..f477438b5c7a2 100644 --- a/test/common/network/socket_option_impl_test.cc +++ b/test/common/network/socket_option_impl_test.cc @@ -24,6 +24,43 @@ TEST_F(SocketOptionImplTest, SetOptionSuccessTrue) { EXPECT_TRUE(socket_option.setOption(socket_, envoy::api::v2::core::SocketOption::STATE_PREBIND)); } +TEST_F(SocketOptionImplTest, GetOptionDetailsCorrectState) { + SocketOptionImpl socket_option{envoy::api::v2::core::SocketOption::STATE_PREBIND, + Network::SocketOptionName(std::make_pair(5, 10)), 1}; + + auto result = + socket_option.getOptionDetails(socket_, envoy::api::v2::core::SocketOption::STATE_PREBIND); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, makeDetails(std::make_pair(5, 10), 1)); +} + +TEST_F(SocketOptionImplTest, GetMoreOptionDetailsCorrectState) { + SocketOptionImpl socket_option{envoy::api::v2::core::SocketOption::STATE_LISTENING, + Network::SocketOptionName(std::make_pair(7, 9)), 5}; + + auto result = + socket_option.getOptionDetails(socket_, envoy::api::v2::core::SocketOption::STATE_LISTENING); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, makeDetails(std::make_pair(7, 9), 5)); +} + +TEST_F(SocketOptionImplTest, GetOptionDetailsFailureWrongState) { + SocketOptionImpl socket_option{envoy::api::v2::core::SocketOption::STATE_LISTENING, + Network::SocketOptionName(std::make_pair(7, 9)), 5}; + + auto result = + socket_option.getOptionDetails(socket_, envoy::api::v2::core::SocketOption::STATE_BOUND); + EXPECT_FALSE(result.has_value()); +} + +TEST_F(SocketOptionImplTest, GetUnsupportedOptReturnsNullopt) { + SocketOptionImpl socket_option{envoy::api::v2::core::SocketOption::STATE_LISTENING, + Network::SocketOptionName(absl::nullopt), 5}; + + auto result = + socket_option.getOptionDetails(socket_, envoy::api::v2::core::SocketOption::STATE_LISTENING); + EXPECT_FALSE(result.has_value()); +} } // namespace } // namespace Network } // namespace Envoy diff --git a/test/common/network/socket_option_test.h b/test/common/network/socket_option_test.h index ee1803913dd47..fb218b015c3e1 100644 --- a/test/common/network/socket_option_test.h +++ b/test/common/network/socket_option_test.h @@ -67,6 +67,16 @@ class SocketOptionTest : public testing::Test { EXPECT_TRUE(socket_option.setOption(socket_, state)); } } + + Socket::Option::Details makeDetails(Network::SocketOptionName name, int value) { + absl::string_view value_as_bstr(reinterpret_cast(&value), sizeof(value)); + + Socket::Option::Details expected_info; + expected_info.name_ = name; + expected_info.value_ = std::string(value_as_bstr); + + return expected_info; + } }; } // namespace diff --git a/test/mocks/network/mocks.h b/test/mocks/network/mocks.h index eed4c18b5eaa9..46352cfdca111 100644 --- a/test/mocks/network/mocks.h +++ b/test/mocks/network/mocks.h @@ -210,6 +210,9 @@ class MockSocketOption : public Socket::Option { MOCK_CONST_METHOD2(setOption, bool(Socket&, envoy::api::v2::core::SocketOption::SocketState state)); MOCK_CONST_METHOD1(hashKey, void(std::vector&)); + MOCK_CONST_METHOD2(getOptionDetails, + absl::optional( + const Socket&, envoy::api::v2::core::SocketOption::SocketState state)); }; class MockConnectionSocket : public ConnectionSocket {