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 @@ -14,3 +14,4 @@ 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 ::.
5 changes: 5 additions & 0 deletions include/envoy/network/address.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ class Ip {
* @return the version of IP address.
*/
virtual IpVersion version() const PURE;

/**
* @return IPv4-IPv6 mapping disabled for IPv6 addresses?
*/
virtual bool v6only() const PURE;

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.

Should this be on class Ipv6 instead of here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Maybe? Need to do some dynamic casting to make use of where I needed it, but I agree it's cleaner at the interface level.

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.

Can't you pivot off of ipv6() being not-null in the cases that you need?

};

enum class Type { Ip, Pipe };
Expand Down
14 changes: 8 additions & 6 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 @@ -47,14 +48,14 @@ Address::InstanceConstSharedPtr addressFromSockAddr(const sockaddr_storage& ss,
NOT_REACHED;
}

InstanceConstSharedPtr addressFromFd(int fd) {
InstanceConstSharedPtr addressFromFd(int fd, bool v6only) {

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.

In this case, shouldn't we getsockopt to determine if it is set, instead of having this as a parameter?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yep, this makes sense.

sockaddr_storage ss;
socklen_t ss_len = sizeof ss;
const int rc = ::getsockname(fd, reinterpret_cast<sockaddr*>(&ss), &ss_len);
if (rc != 0) {
throw EnvoyException(fmt::format("getsockname failed for '{}': {}", fd, strerror(errno)));
}
return addressFromSockAddr(ss, ss_len);
return addressFromSockAddr(ss, ss_len, v6only);
}

InstanceConstSharedPtr peerAddressFromFd(int fd) {
Expand Down Expand Up @@ -179,9 +180,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 +221,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
15 changes: 12 additions & 3 deletions source/common/network/address_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,19 @@ 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.
* @param fd file descriptor.
* @param v6only disable IPv4-IPv6 mapping for IPv6 addresses?
* @return InstanceConstSharedPtr for bound address.
*/
InstanceConstSharedPtr addressFromFd(int fd);
InstanceConstSharedPtr addressFromFd(int fd, bool v6only = true);

/**
* Obtain the address of the peer of the socket with the specified file descriptor.
Expand Down Expand Up @@ -113,6 +116,7 @@ class Ipv4Instance : public InstanceBase {
const Ipv6* ipv6() const override { return nullptr; }
uint32_t port() const override { return ntohs(ipv4_.address_.sin_port); }
IpVersion version() const override { return IpVersion::v4; }
bool v6only() const override { return false; }

Ipv4Helper ipv4_;
std::string friendly_address_;
Expand All @@ -129,7 +133,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 @@ -175,9 +179,14 @@ class Ipv6Instance : public InstanceBase {
const Ipv6* ipv6() const override { return &ipv6_; }
uint32_t port() const override { return ipv6_.port(); }
IpVersion version() const override { return IpVersion::v6; }
bool v6only() const override { return v6only_; }

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
75 changes: 48 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,50 @@ 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);
EXPECT_EQ(v6only, addr_port->ip()->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 +109,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
27 changes: 24 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,36 @@ 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());
EXPECT_TRUE(resolved_addr->ip()->v6only());
}
{
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());
EXPECT_FALSE(resolved_addr->ip()->v6only());
}
}

class TestResolver : public Resolver {
public:
InstanceConstSharedPtr resolve(const envoy::api::v2::SocketAddress& socket_address) override {
Expand Down
2 changes: 1 addition & 1 deletion test/test_common/network_utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Address::InstanceConstSharedPtr findOrCheckFreePort(Address::InstanceConstShared
// If the port we bind is zero, then the OS will pick a free port for us (assuming there are
// any), and we need to find out the port number that the OS picked so we can return it.
if (addr_port->ip()->port() == 0) {
return Address::addressFromFd(fd);
return Address::addressFromFd(fd, addr_port->ip()->v6only());
}
return addr_port;
}
Expand Down