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
24 changes: 17 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,19 @@ 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;
socklen_t size_int = sizeof(socket_v6only);
rc = ::getsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &socket_v6only, &size_int);
if (rc != 0 && errno != EOPNOTSUPP) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this safe on all platforms, ie do all platforms that don't support this option return EOPNOTSUPP? As much as it is distasteful, it's tempting to ignore all errors from this getsockopt and just assume the default value if it fails.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, there is a RELEASE_ASSERT that setting this sockopt does not fail, so I think you can treat any failure (ie ignore value of errno) as a failure here.

throw EnvoyException(
fmt::format("getsockopt failed for '{}': ({}) {}", fd, errno, strerror(errno)));
}
return addressFromSockAddr(ss, ss_len, rc == 0 && socket_v6only);
}

InstanceConstSharedPtr peerAddressFromFd(int fd) {
Expand Down Expand Up @@ -179,9 +188,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 +229,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 @@ -214,4 +214,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