diff --git a/RAW_RELEASE_NOTES.md b/RAW_RELEASE_NOTES.md index 919075a11db07..402f7a12cfa8e 100644 --- a/RAW_RELEASE_NOTES.md +++ b/RAW_RELEASE_NOTES.md @@ -21,6 +21,7 @@ final version. * Added support for dynamic response header values (`%CLIENT_IP%` and `%PROTOCOL%`). * Added native DogStatsD support. :ref:`DogStatsdSink ` * grpc-json: Added support inline descriptor in config. +* Added support for listening for both IPv4 and IPv6 when binding to ::. * Added support for :ref:`LocalityLbEndpoints` priorities. * Added idle timeout to TCP proxy. * Added support for dynamic headers generated from upstream host endpoint metadata diff --git a/source/common/network/address_impl.cc b/source/common/network/address_impl.cc index 2629b59bc17e3..240a4ff1ce246 100644 --- a/source/common/network/address_impl.cc +++ b/source/common/network/address_impl.cc @@ -20,7 +20,8 @@ namespace Envoy { namespace Network { namespace Address { -Address::InstanceConstSharedPtr addressFromSockAddr(const sockaddr_storage& ss, socklen_t ss_len) { +Address::InstanceConstSharedPtr addressFromSockAddr(const sockaddr_storage& ss, socklen_t ss_len, + bool v6only) { RELEASE_ASSERT(ss_len == 0 || ss_len >= sizeof(sa_family_t)); switch (ss.ss_family) { case AF_INET: { @@ -33,7 +34,7 @@ Address::InstanceConstSharedPtr addressFromSockAddr(const sockaddr_storage& ss, RELEASE_ASSERT(ss_len == 0 || ss_len == sizeof(sockaddr_in6)); const struct sockaddr_in6* sin6 = reinterpret_cast(&ss); ASSERT(AF_INET6 == sin6->sin6_family); - return std::make_shared(*sin6); + return std::make_shared(*sin6, v6only); } case AF_UNIX: { const struct sockaddr_un* sun = reinterpret_cast(&ss); @@ -50,11 +51,17 @@ Address::InstanceConstSharedPtr addressFromSockAddr(const sockaddr_storage& ss, InstanceConstSharedPtr addressFromFd(int fd) { sockaddr_storage ss; socklen_t ss_len = sizeof ss; - const int rc = ::getsockname(fd, reinterpret_cast(&ss), &ss_len); + int rc = ::getsockname(fd, reinterpret_cast(&ss), &ss_len); if (rc != 0) { - throw EnvoyException(fmt::format("getsockname failed for '{}': {}", fd, strerror(errno))); + throw EnvoyException( + fmt::format("getsockname failed for '{}': ({}) {}", fd, errno, strerror(errno))); } - return addressFromSockAddr(ss, ss_len); + int socket_v6only = 0; + if (ss.ss_family == AF_INET6) { + socklen_t size_int = sizeof(socket_v6only); + RELEASE_ASSERT(::getsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &socket_v6only, &size_int) == 0); + } + return addressFromSockAddr(ss, ss_len, rc == 0 && socket_v6only); } InstanceConstSharedPtr peerAddressFromFd(int fd) { @@ -179,9 +186,10 @@ std::string Ipv6Instance::Ipv6Helper::makeFriendlyAddress() const { return ptr; } -Ipv6Instance::Ipv6Instance(const sockaddr_in6& address) : InstanceBase(Type::Ip) { +Ipv6Instance::Ipv6Instance(const sockaddr_in6& address, bool v6only) : InstanceBase(Type::Ip) { ip_.ipv6_.address_ = address; ip_.friendly_address_ = ip_.ipv6_.makeFriendlyAddress(); + ip_.v6only_ = v6only; friendly_name_ = fmt::format("[{}]:{}", ip_.friendly_address_, ip_.port()); } @@ -219,7 +227,7 @@ int Ipv6Instance::socket(SocketType type) const { const int fd = socketFromSocketType(type); // Setting IPV6_V6ONLY resticts the IPv6 socket to IPv6 connections only. - const int v6only = 1; + const int v6only = ip_.v6only_; RELEASE_ASSERT(::setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != -1); return fd; } diff --git a/source/common/network/address_impl.h b/source/common/network/address_impl.h index 57343a2e57d4c..cc729a6e766ae 100644 --- a/source/common/network/address_impl.h +++ b/source/common/network/address_impl.h @@ -21,9 +21,11 @@ namespace Address { * @param ss a valid address with family AF_INET, AF_INET6 or AF_UNIX. * @param len length of the address (e.g. from accept, getsockname or getpeername). If len > 0, * it is used to validate the structure contents; else if len == 0, it is ignored. + * @param v6only disable IPv4-IPv6 mapping for IPv6 addresses? * @return InstanceConstSharedPtr the address. */ -Address::InstanceConstSharedPtr addressFromSockAddr(const sockaddr_storage& ss, socklen_t len); +Address::InstanceConstSharedPtr addressFromSockAddr(const sockaddr_storage& ss, socklen_t len, + bool v6only = true); /** * Obtain an address from a bound file descriptor. Raises an EnvoyException on failure. @@ -129,7 +131,7 @@ class Ipv6Instance : public InstanceBase { /** * Construct from an existing unix IPv6 socket address (IP v6 address and port). */ - explicit Ipv6Instance(const sockaddr_in6& address); + Ipv6Instance(const sockaddr_in6& address, bool v6only = true); /** * Construct from a string IPv6 address such as "12:34::5". Port will be unset/0. @@ -178,6 +180,10 @@ class Ipv6Instance : public InstanceBase { Ipv6Helper ipv6_; std::string friendly_address_; + // Is IPv4 compatibility (https://tools.ietf.org/html/rfc3493#page-11) disabled? + // Default initialized to true to preserve extant Envoy behavior where we don't explicitly set + // this in the constructor. + bool v6only_{true}; }; IpHelper ip_; diff --git a/source/common/network/resolver_impl.cc b/source/common/network/resolver_impl.cc index 64bec7b5b2135..eef0dc4ba3c0a 100644 --- a/source/common/network/resolver_impl.cc +++ b/source/common/network/resolver_impl.cc @@ -26,8 +26,8 @@ class IpResolver : public Resolver { case envoy::api::v2::SocketAddress::kPortValue: // Default to port 0 if no port value is specified. case envoy::api::v2::SocketAddress::PORT_SPECIFIER_NOT_SET: - return Network::Utility::parseInternetAddress(socket_address.address(), - socket_address.port_value()); + return Network::Utility::parseInternetAddress( + socket_address.address(), socket_address.port_value(), !socket_address.ipv4_compat()); default: throw EnvoyException(fmt::format("IP resolver can't handle port specifier type {}", diff --git a/source/common/network/utility.cc b/source/common/network/utility.cc index c5df8e148d906..48ceb96ba717b 100644 --- a/source/common/network/utility.cc +++ b/source/common/network/utility.cc @@ -81,7 +81,7 @@ uint32_t Utility::portFromTcpUrl(const std::string& url) { } Address::InstanceConstSharedPtr Utility::parseInternetAddress(const std::string& ip_address, - uint16_t port) { + uint16_t port, bool v6only) { sockaddr_in sa4; if (inet_pton(AF_INET, ip_address.c_str(), &sa4.sin_addr) == 1) { sa4.sin_family = AF_INET; @@ -92,14 +92,14 @@ Address::InstanceConstSharedPtr Utility::parseInternetAddress(const std::string& if (inet_pton(AF_INET6, ip_address.c_str(), &sa6.sin6_addr) == 1) { sa6.sin6_family = AF_INET6; sa6.sin6_port = htons(port); - return std::make_shared(sa6); + return std::make_shared(sa6, v6only); } throwWithMalformedIp(ip_address); NOT_REACHED; } -Address::InstanceConstSharedPtr -Utility::parseInternetAddressAndPort(const std::string& ip_address) { +Address::InstanceConstSharedPtr Utility::parseInternetAddressAndPort(const std::string& ip_address, + bool v6only) { if (ip_address.empty()) { throwWithMalformedIp(ip_address); } @@ -121,7 +121,7 @@ Utility::parseInternetAddressAndPort(const std::string& ip_address) { } sa6.sin6_family = AF_INET6; sa6.sin6_port = htons(port64); - return std::make_shared(sa6); + return std::make_shared(sa6, v6only); } // Treat it as an IPv4 address followed by a port. auto pos = ip_address.rfind(":"); diff --git a/source/common/network/utility.h b/source/common/network/utility.h index 050de469f3e32..9caacb87fe735 100644 --- a/source/common/network/utility.h +++ b/source/common/network/utility.h @@ -76,10 +76,11 @@ class Utility { * not include a port number. Throws EnvoyException if unable to parse the address. * @param ip_address string to be parsed as an internet address. * @param port optional port to include in Instance created from ip_address, 0 by default. + * @param v6only disable IPv4-IPv6 mapping for IPv6 addresses? * @return pointer to the Instance, or nullptr if unable to parse the address. */ - static Address::InstanceConstSharedPtr parseInternetAddress(const std::string& ip_address, - uint16_t port = 0); + static Address::InstanceConstSharedPtr + parseInternetAddress(const std::string& ip_address, uint16_t port = 0, bool v6only = true); /** * Parse an internet host address (IPv4 or IPv6) AND port, and create an Instance from it. Throws @@ -95,9 +96,11 @@ class Utility { * @param ip_addr string to be parsed as an internet address and port. Examples: * - "1.2.3.4:80" * - "[1234:5678::9]:443" + * @param v6only disable IPv4-IPv6 mapping for IPv6 addresses? * @return pointer to the Instance. */ - static Address::InstanceConstSharedPtr parseInternetAddressAndPort(const std::string& ip_address); + static Address::InstanceConstSharedPtr parseInternetAddressAndPort(const std::string& ip_address, + bool v6only = true); /** * Get the local address of the first interface address that is of type diff --git a/source/server/listener_manager_impl.cc b/source/server/listener_manager_impl.cc index acc2effde0f9e..20bcbfd519540 100644 --- a/source/server/listener_manager_impl.cc +++ b/source/server/listener_manager_impl.cc @@ -76,7 +76,8 @@ ListenerImpl::ListenerImpl(const envoy::api::v2::Listener& config, ListenerManag // TODO(htuch): Validate not pipe when doing v2. address_( Network::Utility::parseInternetAddress(config.address().socket_address().address(), - config.address().socket_address().port_value())), + config.address().socket_address().port_value(), + !config.address().socket_address().ipv4_compat())), global_scope_(parent_.server_.stats().createScope("")), listener_scope_( parent_.server_.stats().createScope(fmt::format("listener.{}.", address_->asString()))), diff --git a/test/common/network/address_impl_test.cc b/test/common/network/address_impl_test.cc index 9af1fc78d4094..a905ece52b586 100644 --- a/test/common/network/address_impl_test.cc +++ b/test/common/network/address_impl_test.cc @@ -38,9 +38,11 @@ void makeFdBlocking(int fd) { ASSERT_EQ(::fcntl(fd, F_SETFL, flags & (~O_NONBLOCK)), 0); } -void testSocketBindAndConnect(const std::string& addr_port_str) { - auto addr_port = Network::Utility::parseInternetAddressAndPort(addr_port_str); +void testSocketBindAndConnect(Network::Address::IpVersion ip_version, bool v6only) { + auto addr_port = Network::Utility::parseInternetAddressAndPort( + fmt::format("{}:0", Network::Test::getAnyAddressUrlString(ip_version)), v6only); ASSERT_NE(addr_port, nullptr); + if (addr_port->ip()->port() == 0) { addr_port = Network::Test::findOrCheckFreePort(addr_port, SocketType::Stream); } @@ -54,36 +56,49 @@ void testSocketBindAndConnect(const std::string& addr_port_str) { // Check that IPv6 sockets accept IPv6 connections only. if (addr_port->ip()->version() == IpVersion::v6) { - int v6only = 0; - socklen_t size_int = sizeof(v6only); - ASSERT_GE(::getsockopt(listen_fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, &size_int), 0); - EXPECT_EQ(v6only, 1); + int socket_v6only = 0; + socklen_t size_int = sizeof(socket_v6only); + ASSERT_GE(::getsockopt(listen_fd, IPPROTO_IPV6, IPV6_V6ONLY, &socket_v6only, &size_int), 0); + EXPECT_EQ(v6only, socket_v6only); } // Bind the socket to the desired address and port. - int rc = addr_port->bind(listen_fd); - int err = errno; + const int rc = addr_port->bind(listen_fd); + const int err = errno; ASSERT_EQ(rc, 0) << addr_port->asString() << "\nerror: " << strerror(err) << "\nerrno: " << err; // Do a bare listen syscall. Not bothering to accept connections as that would // require another thread. - ASSERT_EQ(::listen(listen_fd, 1), 0); - - // Create a client socket and connect to the server. - const int client_fd = addr_port->socket(SocketType::Stream); - ASSERT_GE(client_fd, 0) << addr_port->asString(); - ScopedFdCloser closer2(client_fd); - - // Instance::socket creates a non-blocking socket, which that extends all the way to the - // operation of ::connect(), so connect returns with errno==EWOULDBLOCK before the tcp - // handshake can complete. For testing convenience, re-enable blocking on the socket - // so that connect will wait for the handshake to complete. - makeFdBlocking(client_fd); - - // Connect to the server. - rc = addr_port->connect(client_fd); - err = errno; - ASSERT_EQ(rc, 0) << addr_port->asString() << "\nerror: " << strerror(err) << "\nerrno: " << err; + ASSERT_EQ(::listen(listen_fd, 128), 0); + + auto client_connect = [](Address::InstanceConstSharedPtr addr_port) { + // Create a client socket and connect to the server. + const int client_fd = addr_port->socket(SocketType::Stream); + ASSERT_GE(client_fd, 0) << addr_port->asString(); + ScopedFdCloser closer2(client_fd); + + // Instance::socket creates a non-blocking socket, which that extends all the way to the + // operation of ::connect(), so connect returns with errno==EWOULDBLOCK before the tcp + // handshake can complete. For testing convenience, re-enable blocking on the socket + // so that connect will wait for the handshake to complete. + makeFdBlocking(client_fd); + + // Connect to the server. + const int rc = addr_port->connect(client_fd); + const int err = errno; + ASSERT_EQ(rc, 0) << addr_port->asString() << "\nerror: " << strerror(err) << "\nerrno: " << err; + }; + + client_connect(addr_port); + + if (!v6only) { + ASSERT_EQ(IpVersion::v6, addr_port->ip()->version()); + auto v4_addr_port = Network::Utility::parseInternetAddress( + Network::Test::getLoopbackAddressUrlString(Network::Address::IpVersion::v4), + addr_port->ip()->port(), true); + ASSERT_NE(v4_addr_port, nullptr); + client_connect(v4_addr_port); + } } } // namespace @@ -93,8 +108,13 @@ INSTANTIATE_TEST_CASE_P(IpVersions, AddressImplSocketTest, TEST_P(AddressImplSocketTest, SocketBindAndConnect) { // Test listening on and connecting to an unused port with an IP loopback address. - testSocketBindAndConnect( - fmt::format("{}:0", Network::Test::getLoopbackAddressUrlString(GetParam()))); + testSocketBindAndConnect(GetParam(), true); +} + +TEST(Ipv4CompatAddressImplSocktTest, SocketBindAndConnect) { + if (TestEnvironment::shouldRunTestForIpVersion(Network::Address::IpVersion::v6)) { + testSocketBindAndConnect(Network::Address::IpVersion::v6, false); + } } TEST(Ipv4InstanceTest, SocketAddress) { diff --git a/test/common/network/resolver_impl_test.cc b/test/common/network/resolver_impl_test.cc index 4358a5e37b549..248015ed6f042 100644 --- a/test/common/network/resolver_impl_test.cc +++ b/test/common/network/resolver_impl_test.cc @@ -51,15 +51,34 @@ TEST(ResolverTest, FromProtoAddress) { EXPECT_EQ("1.2.3.4:5", resolveProtoAddress(ipv4_address)->asString()); envoy::api::v2::Address ipv6_address; - ipv4_address.mutable_socket_address()->set_address("1::1"); - ipv4_address.mutable_socket_address()->set_port_value(2); - EXPECT_EQ("[1::1]:2", resolveProtoAddress(ipv4_address)->asString()); + ipv6_address.mutable_socket_address()->set_address("1::1"); + ipv6_address.mutable_socket_address()->set_port_value(2); + EXPECT_EQ("[1::1]:2", resolveProtoAddress(ipv6_address)->asString()); envoy::api::v2::Address pipe_address; pipe_address.mutable_pipe()->set_path("/foo/bar"); EXPECT_EQ("/foo/bar", resolveProtoAddress(pipe_address)->asString()); } +// Validate correct handling of ipv4_compat field. +TEST(ResolverTest, FromProtoAddressV4Compat) { + { + envoy::api::v2::Address ipv6_address; + ipv6_address.mutable_socket_address()->set_address("1::1"); + ipv6_address.mutable_socket_address()->set_port_value(2); + auto resolved_addr = resolveProtoAddress(ipv6_address); + EXPECT_EQ("[1::1]:2", resolved_addr->asString()); + } + { + envoy::api::v2::Address ipv6_address; + ipv6_address.mutable_socket_address()->set_address("1::1"); + ipv6_address.mutable_socket_address()->set_port_value(2); + ipv6_address.mutable_socket_address()->set_ipv4_compat(true); + auto resolved_addr = resolveProtoAddress(ipv6_address); + EXPECT_EQ("[1::1]:2", resolved_addr->asString()); + } +} + class TestResolver : public Resolver { public: InstanceConstSharedPtr resolve(const envoy::api::v2::SocketAddress& socket_address) override { diff --git a/test/integration/integration_admin_test.cc b/test/integration/integration_admin_test.cc index 98e28ca2610c3..8b5bfb1476a73 100644 --- a/test/integration/integration_admin_test.cc +++ b/test/integration/integration_admin_test.cc @@ -254,4 +254,32 @@ TEST_P(IntegrationAdminTest, AdminCpuProfilerStart) { } #endif +class IntegrationAdminIpv4Ipv6Test : public HttpIntegrationTest, public testing::Test { +public: + IntegrationAdminIpv4Ipv6Test() + : HttpIntegrationTest(Http::CodecClient::Type::HTTP1, Network::Address::IpVersion::v4) {} + + void initialize() override { + config_helper_.addConfigModifier([&](envoy::api::v2::Bootstrap& bootstrap) -> void { + auto* socket_address = bootstrap.mutable_admin()->mutable_address()->mutable_socket_address(); + socket_address->set_ipv4_compat(true); + socket_address->set_address("::"); + }); + HttpIntegrationTest::initialize(); + } +}; + +// Verify an IPv4 client can connect to the admin interface listening on :: when +// IPv4 compat mode is enabled. +TEST_F(IntegrationAdminIpv4Ipv6Test, Ipv4Ipv6Listen) { + if (TestEnvironment::shouldRunTestForIpVersion(Network::Address::IpVersion::v4) && + TestEnvironment::shouldRunTestForIpVersion(Network::Address::IpVersion::v6)) { + initialize(); + BufferingStreamDecoderPtr response = IntegrationUtil::makeSingleRequest( + lookupPort("admin"), "GET", "/server_info", "", downstreamProtocol(), version_); + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("200", response->headers().Status()->value().c_str()); + } +} + } // namespace Envoy