Skip to content
1 change: 1 addition & 0 deletions RAW_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ final version.
* Added support for dynamic response header values (`%CLIENT_IP%` and `%PROTOCOL%`).
* Added native DogStatsD support. :ref:`DogStatsdSink <envoy_api_msg_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<envoy_api_msg_LocalityLbEndpoints>` priorities.
* Added idle timeout to TCP proxy.
* Added support for dynamic headers generated from upstream host endpoint metadata
Expand Down
22 changes: 15 additions & 7 deletions source/common/network/address_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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<const struct sockaddr_in6*>(&ss);
ASSERT(AF_INET6 == sin6->sin6_family);
return std::make_shared<Address::Ipv6Instance>(*sin6);
return std::make_shared<Address::Ipv6Instance>(*sin6, v6only);
}
case AF_UNIX: {
const struct sockaddr_un* sun = reinterpret_cast<const struct sockaddr_un*>(&ss);
Expand All @@ -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<sockaddr*>(&ss), &ss_len);
int rc = ::getsockname(fd, reinterpret_cast<sockaddr*>(&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) {
Expand Down Expand Up @@ -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());
}

Expand Down Expand Up @@ -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;
}
Expand Down
10 changes: 8 additions & 2 deletions source/common/network/address_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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_;
Expand Down
4 changes: 2 additions & 2 deletions source/common/network/resolver_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}",
Expand Down
10 changes: 5 additions & 5 deletions source/common/network/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Address::Ipv6Instance>(sa6);
return std::make_shared<Address::Ipv6Instance>(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);
}
Expand All @@ -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<Address::Ipv6Instance>(sa6);
return std::make_shared<Address::Ipv6Instance>(sa6, v6only);
}
// Treat it as an IPv4 address followed by a port.
auto pos = ip_address.rfind(":");
Expand Down
9 changes: 6 additions & 3 deletions source/common/network/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion source/server/listener_manager_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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()))),
Expand Down
74 changes: 47 additions & 27 deletions test/common/network/address_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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

Expand All @@ -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) {
Expand Down
25 changes: 22 additions & 3 deletions test/common/network/resolver_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
28 changes: 28 additions & 0 deletions test/integration/integration_admin_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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