From 276b39a8a052da2bc31996a9abaf5eb4f66a79b7 Mon Sep 17 00:00:00 2001 From: Mike Schore Date: Mon, 11 Oct 2021 12:31:34 -0700 Subject: [PATCH] network: support conditionally binding active alt interface (#1834) Description: Adds utility support for filtering for a viable WIFI or cellular interface to select as a potential alternate to the OS-supplied default, and an accessor for the platform-specific socket options to bind to it. Risk Level: Low Testing: Added unit test Signed-off-by: Mike Schore --- library/common/network/configurator.cc | 69 +++++++++++++++++++++--- library/common/network/configurator.h | 12 ++++- test/common/network/configurator_test.cc | 18 +++++++ 3 files changed, 92 insertions(+), 7 deletions(-) diff --git a/library/common/network/configurator.cc b/library/common/network/configurator.cc index 6d23e881b4..26fe041be5 100644 --- a/library/common/network/configurator.cc +++ b/library/common/network/configurator.cc @@ -1,5 +1,7 @@ #include "library/common/network/configurator.h" +#include + #include "envoy/common/platform.h" #include "source/common/common/assert.h" @@ -96,14 +98,18 @@ void Configurator::refreshDns(envoy_network_t network) { } std::vector Configurator::enumerateV4Interfaces() { - return enumerateInterfaces(AF_INET); + return enumerateInterfaces(AF_INET, 0, 0); } std::vector Configurator::enumerateV6Interfaces() { - return enumerateInterfaces(AF_INET6); + return enumerateInterfaces(AF_INET6, 0, 0); } -Socket::OptionsSharedPtr Configurator::getUpstreamSocketOptions(envoy_network_t network, bool) { +Socket::OptionsSharedPtr Configurator::getUpstreamSocketOptions(envoy_network_t network, + bool override_interface) { + if (override_interface && network != ENVOY_NET_GENERIC) { + return getAlternateInterfaceSocketOptions(network); + } // Envoy uses the hash signature of overridden socket options to choose a connection pool. // Setting a dummy socket option is a hack that allows us to select a different // connection pool without materially changing the socket configuration. @@ -116,7 +122,54 @@ Socket::OptionsSharedPtr Configurator::getUpstreamSocketOptions(envoy_network_t return options; } -std::vector Configurator::enumerateInterfaces([[maybe_unused]] unsigned short family) { +Socket::OptionsSharedPtr Configurator::getAlternateInterfaceSocketOptions(envoy_network_t network) { + auto& v4_interface = getActiveAlternateInterface(network, AF_INET); + auto& v6_interface = getActiveAlternateInterface(network, AF_INET6); + + auto options = std::make_shared(); + + // Android +#ifdef SO_BINDTODEVICE + options->push_back(std::make_shared( + envoy::config::core::v3::SocketOption::STATE_PREBIND, ENVOY_SOCKET_SO_BINDTODEVICE, + v4_interface, ENVOY_SOCKET_SO_BINDTODEVICE, v6_interface)); +#endif // SO_BINDTODEVICE + + // iOS +#ifdef IP_BOUND_IF + int v4_idx = if_nametoindex(v4_interface.c_str()); + int v6_idx = if_nametoindex(v6_interface.c_str()); + options->push_back(std::make_shared( + envoy::config::core::v3::SocketOption::STATE_PREBIND, ENVOY_SOCKET_IP_BOUND_IF, v4_idx, + ENVOY_SOCKET_IPV6_BOUND_IF, v6_idx)); +#endif // IP_BOUND_IF + + return options; +} + +const std::string Configurator::getActiveAlternateInterface(envoy_network_t network, + unsigned short family) { + // Attempt to derive an active interface that differs from the passed network parameter. + if (network == ENVOY_NET_WWAN) { + // Network is cellular, so look for a WiFi interface. + // WiFi should always support multicast, and will not be point-to-point. + auto interfaces = + enumerateInterfaces(family, IFF_UP | IFF_MULTICAST, IFF_LOOPBACK | IFF_POINTOPOINT); + return interfaces.size() > 0 ? interfaces[0] : ""; + } else if (network == ENVOY_NET_WLAN) { + // Network is WiFi, so look for a cellular interface. + // Cellular networks should be point-to-point. + auto interfaces = enumerateInterfaces(family, IFF_UP | IFF_POINTOPOINT, IFF_LOOPBACK); + return interfaces.size() > 0 ? interfaces[0] : ""; + } else { + return ""; + } +} + +std::vector +Configurator::enumerateInterfaces([[maybe_unused]] unsigned short family, + [[maybe_unused]] unsigned int select_flags, + [[maybe_unused]] unsigned int reject_flags) { std::vector names{}; #ifdef SUPPORTS_GETIFADDRS @@ -127,9 +180,13 @@ std::vector Configurator::enumerateInterfaces([[maybe_unused]] unsi RELEASE_ASSERT(!rc, "getifaddrs failed"); for (ifa = interfaces; ifa != nullptr; ifa = ifa->ifa_next) { - if (ifa->ifa_addr && ifa->ifa_addr->sa_family == family) { - names.push_back(std::string{ifa->ifa_name}); + if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != family) { + continue; + } + if ((ifa->ifa_flags & (select_flags ^ reject_flags)) != select_flags) { + continue; } + names.push_back(std::string{ifa->ifa_name}); } freeifaddrs(interfaces); diff --git a/library/common/network/configurator.h b/library/common/network/configurator.h index fa7189353a..63d824be7a 100644 --- a/library/common/network/configurator.h +++ b/library/common/network/configurator.h @@ -33,6 +33,15 @@ class Configurator : public Logger::Loggable, public Singl */ std::vector enumerateV6Interfaces(); + /** + * @param family, network family of the interface. + * @param select_flags, flags which MUST be set for each returned interface. + * @param reject_flags, flags which MUST NOT be set for any returned interface. + * @returns a list of local network interfaces filtered by the providered flags. + */ + std::vector enumerateInterfaces(unsigned short family, unsigned int select_flags, + unsigned int reject_flags); + /** * @returns the current OS default/preferred network class. */ @@ -62,7 +71,8 @@ class Configurator : public Logger::Loggable, public Singl bool override_interface); private: - std::vector enumerateInterfaces(unsigned short family); + Socket::OptionsSharedPtr getAlternateInterfaceSocketOptions(envoy_network_t network); + const std::string getActiveAlternateInterface(envoy_network_t network, unsigned short family); DnsCacheManagerSharedPtr dns_cache_manager_; static std::atomic preferred_network_; }; diff --git a/test/common/network/configurator_test.cc b/test/common/network/configurator_test.cc index 2e999626db..326f044c6e 100644 --- a/test/common/network/configurator_test.cc +++ b/test/common/network/configurator_test.cc @@ -1,3 +1,5 @@ +#include + #include "test/extensions/common/dynamic_forward_proxy/mocks.h" #include "gtest/gtest.h" @@ -37,5 +39,21 @@ TEST_F(ConfiguratorTest, RefreshDnsForOtherNetworkDoesntTriggerDnsRefresh) { configurator_->refreshDns(ENVOY_NET_WWAN); } +TEST_F(ConfiguratorTest, EnumerateInterfacesFiltersByFlags) { + // Select loopback. + auto loopbacks = configurator_->enumerateInterfaces(AF_INET, IFF_LOOPBACK, 0); + EXPECT_EQ(loopbacks[0].rfind("lo", 0), 0); + + // Reject loopback. + auto nonloopbacks = configurator_->enumerateInterfaces(AF_INET, 0, IFF_LOOPBACK); + for (const auto& interface : nonloopbacks) { + EXPECT_NE(interface.rfind("lo", 0), 0); + } + + // Select AND reject loopback. + auto empty = configurator_->enumerateInterfaces(AF_INET, IFF_LOOPBACK, IFF_LOOPBACK); + EXPECT_EQ(empty.size(), 0); +} + } // namespace Network } // namespace Envoy