Skip to content

Commit

Permalink
quic: support for server-preferred address behind DNAT (#33774)
Browse files Browse the repository at this point in the history
Signed-off-by: Greg Greenway <ggreenway@apple.com>
  • Loading branch information
ggreenway committed May 7, 2024
1 parent f4306ab commit c940f24
Show file tree
Hide file tree
Showing 16 changed files with 577 additions and 78 deletions.
Expand Up @@ -6,6 +6,7 @@ licenses(["notice"]) # Apache 2

api_proto_package(
deps = [
"//envoy/config/core/v3:pkg",
"@com_github_cncf_xds//udpa/annotations:pkg",
"@com_github_cncf_xds//xds/annotations/v3:pkg",
],
Expand Down
Expand Up @@ -2,6 +2,8 @@ syntax = "proto3";

package envoy.extensions.quic.server_preferred_address.v3;

import "envoy/config/core/v3/address.proto";

import "xds/annotations/v3/status.proto";

import "udpa/annotations/status.proto";
Expand All @@ -21,15 +23,41 @@ message FixedServerPreferredAddressConfig {

option (xds.annotations.v3.message_status).work_in_progress = true;

oneof ipv4_type {
// String representation of IPv4 address, i.e. "127.0.0.2".
// If not specified, none will be configured.
string ipv4_address = 1;
// Addresses for server preferred address for a single address family (IPv4 or IPv6).
message AddressFamilyConfig {
// The server preferred address sent to clients.
//
// Note: Envoy currently must receive all packets for a QUIC connection on the same port, so unless
// :ref:`dnat_address <envoy_v3_api_field_extensions.quic.server_preferred_address.v3.FixedServerPreferredAddressConfig.AddressFamilyConfig.dnat_address>`
// is configured, the port for this address must be zero, and the listener's
// port will be used instead.
config.core.v3.SocketAddress address = 1;

// If there is a DNAT between the client and Envoy, the address that Envoy will observe
// server preferred address packets being sent to. If this is not specified, it is assumed
// there is no DNAT and the server preferred address packets will be sent to the address advertised
// to clients for server preferred address.
//
// Note: Envoy currently must receive all packets for a QUIC connection on the same port, so the
// port for this address must be zero, and the listener's port will be used instead.
config.core.v3.SocketAddress dnat_address = 2;
}

oneof ipv6_type {
// String representation of IPv6 address, i.e. "::1".
// If not specified, none will be configured.
string ipv6_address = 2;
}
// String representation of IPv4 address, i.e. "127.0.0.2".
// If not specified, none will be configured.
string ipv4_address = 1;

// The IPv4 address to advertise to clients for Server Preferred Address.
// This field takes precedence over
// :ref:`ipv4_address <envoy_v3_api_field_extensions.quic.server_preferred_address.v3.FixedServerPreferredAddressConfig.ipv4_address>`.
AddressFamilyConfig ipv4_config = 3;

// String representation of IPv6 address, i.e. "::1".
// If not specified, none will be configured.
string ipv6_address = 2;

// The IPv6 address to advertise to clients for Server Preferred Address.
// This field takes precedence over
// :ref:`ipv6_address <envoy_v3_api_field_extensions.quic.server_preferred_address.v3.FixedServerPreferredAddressConfig.ipv6_address>`.
AddressFamilyConfig ipv6_config = 4;
}
5 changes: 5 additions & 0 deletions changelogs/current.yaml
Expand Up @@ -93,6 +93,11 @@ new_features:
change: |
Added :ref:`Filter State Input <envoy_v3_api_msg_extensions.matching.common_inputs.network.v3.FilterStateInput>`
for matching http input based on filter state objects.
- area: quic
change: |
Added support for QUIC server preferred address when there is a DNAT between the client and Envoy. See
:ref:`new config
<envoy_v3_api_field_extensions.quic.server_preferred_address.v3.FixedServerPreferredAddressConfig.AddressFamilyConfig.dnat_address>`.
- area: cares
change: |
Added :ref:`udp_max_queries<envoy_v3_api_field_extensions.network.dns_resolver.cares.v3.CaresDnsResolverConfig.udp_max_queries>`
Expand Down
37 changes: 26 additions & 11 deletions source/common/quic/active_quic_listener.cc
Expand Up @@ -344,22 +344,37 @@ Network::ConnectionHandler::ActiveUdpListenerPtr ActiveQuicListenerFactory::crea
Network::ListenerConfig& config) {
ASSERT(crypto_server_stream_factory_.has_value());
if (server_preferred_address_config_ != nullptr) {
std::pair<quic::QuicSocketAddress, quic::QuicSocketAddress> addresses =
const EnvoyQuicServerPreferredAddressConfig::Addresses addresses =
server_preferred_address_config_->getServerPreferredAddresses(
listen_socket_ptr->connectionInfoProvider().localAddress());
quic::QuicSocketAddress v4_address = addresses.first;
if (v4_address.IsInitialized()) {
ENVOY_BUG(v4_address.host().address_family() == quiche::IpAddressFamily::IP_V4,
if (addresses.ipv4_.IsInitialized()) {
ENVOY_BUG(addresses.ipv4_.host().address_family() == quiche::IpAddressFamily::IP_V4,
absl::StrCat("Configured IPv4 server's preferred address isn't a v4 address:",
v4_address.ToString()));
quic_config_.SetIPv4AlternateServerAddressToSend(v4_address);
addresses.ipv4_.ToString()));
if (addresses.dnat_ipv4_.IsInitialized()) {
ENVOY_BUG(
addresses.dnat_ipv4_.host().address_family() == quiche::IpAddressFamily::IP_V4,
absl::StrCat("Configured IPv4 server's preferred DNAT address isn't a v4 address:",
addresses.dnat_ipv4_.ToString()));
quic_config_.SetIPv4AlternateServerAddressForDNat(addresses.ipv4_, addresses.dnat_ipv4_);
} else {
quic_config_.SetIPv4AlternateServerAddressToSend(addresses.ipv4_);
}
}
quic::QuicSocketAddress v6_address = addresses.second;
if (v6_address.IsInitialized()) {
ENVOY_BUG(v6_address.host().address_family() == quiche::IpAddressFamily::IP_V6,

if (addresses.ipv6_.IsInitialized()) {
ENVOY_BUG(addresses.ipv6_.host().address_family() == quiche::IpAddressFamily::IP_V6,
absl::StrCat("Configured IPv6 server's preferred address isn't a v6 address:",
v4_address.ToString()));
quic_config_.SetIPv6AlternateServerAddressToSend(v6_address);
addresses.ipv6_.ToString()));
if (addresses.dnat_ipv6_.IsInitialized()) {
ENVOY_BUG(
addresses.dnat_ipv6_.host().address_family() == quiche::IpAddressFamily::IP_V6,
absl::StrCat("Configured IPv6 server's preferred DNAT address isn't a v6 address:",
addresses.dnat_ipv6_.ToString()));
quic_config_.SetIPv6AlternateServerAddressForDNat(addresses.ipv6_, addresses.dnat_ipv6_);
} else {
quic_config_.SetIPv6AlternateServerAddressToSend(addresses.ipv6_);
}
}
}

Expand Down
Expand Up @@ -15,14 +15,29 @@ class EnvoyQuicServerPreferredAddressConfig {
public:
virtual ~EnvoyQuicServerPreferredAddressConfig() = default;

// The set of addresses used to configure the server preferred addresses.
struct Addresses {
// Addresses that client is requested to use.
quic::QuicSocketAddress ipv4_;
quic::QuicSocketAddress ipv6_;

// If destination NAT is applied between the client and Envoy, the addresses that
// Envoy will see for client traffic to the server preferred address. If this is not
// set, Envoy will expect to receive server preferred address traffic on the above addresses.
//
// A DNAT address will be ignored if the corresponding SPA address is not set.
quic::QuicSocketAddress dnat_ipv4_;
quic::QuicSocketAddress dnat_ipv6_;
};

/**
* Called during config loading.
* @param local_address the configured default listening address.
* Returns a pair of the server preferred addresses in form of {IPv4, IPv6} which will be used for
* the entire life time of the QUIC listener. An uninitialized address value means no preferred
* address for that address family.
*/
virtual std::pair<quic::QuicSocketAddress, quic::QuicSocketAddress>
virtual Addresses
getServerPreferredAddresses(const Network::Address::InstanceConstSharedPtr& local_address) PURE;
};

Expand Down
1 change: 1 addition & 0 deletions source/extensions/quic/server_preferred_address/BUILD
Expand Up @@ -23,6 +23,7 @@ envoy_cc_library(
deps = [
"//envoy/registry",
"//source/common/quic:envoy_quic_server_preferred_address_config_factory_interface",
"//source/common/quic:envoy_quic_utils_lib",
"@envoy_api//envoy/extensions/quic/server_preferred_address/v3:pkg_cc_proto",
],
alwayslink = LEGACY_ALWAYSLINK,
Expand Down
@@ -1,13 +1,118 @@
#include "source/extensions/quic/server_preferred_address/fixed_server_preferred_address_config.h"

#include "source/common/network/utility.h"
#include "source/common/quic/envoy_quic_utils.h"

namespace Envoy {
namespace Quic {

std::pair<quic::QuicSocketAddress, quic::QuicSocketAddress>
namespace {

quic::QuicSocketAddress ipOrAddressToAddress(const quic::QuicSocketAddress& address, int32_t port) {
if (address.port() == 0) {
return quic::QuicSocketAddress(address.host(), port);
}

return address;
}

quic::QuicIpAddress parseIp(const std::string& addr, absl::string_view address_family,
const Protobuf::Message& message) {
quic::QuicIpAddress ip;
if (!ip.FromString(addr)) {
ProtoExceptionUtil::throwProtoValidationException(
absl::StrCat("bad ", address_family, " server preferred address: ", addr), message);
}
return ip;
}

quic::QuicSocketAddress parseSocketAddress(const envoy::config::core::v3::SocketAddress& addr,
Network::Address::IpVersion version,
absl::string_view version_str,
const Protobuf::Message& message) {
// There's no utility to convert from a `SocketAddress`, so wrap it in an `Address` to make use of
// existing helpers.
envoy::config::core::v3::Address outer;
*outer.mutable_socket_address() = addr;
auto envoy_addr = Network::Utility::protobufAddressToAddress(outer);
ASSERT(envoy_addr != nullptr,
"Network::Utility::protobufAddressToAddress throws on failure so this can't be nullptr");
if (envoy_addr->ip() == nullptr || envoy_addr->ip()->version() != version) {
ProtoExceptionUtil::throwProtoValidationException(
absl::StrCat("wrong address type for ", version_str, " server preferred address: ", addr),
message);
}

return envoyIpAddressToQuicSocketAddress(envoy_addr->ip());
}

quic::QuicIpAddress
parseIpAddressFromSocketAddress(const envoy::config::core::v3::SocketAddress& addr,
Network::Address::IpVersion version, absl::string_view version_str,
const Protobuf::Message& message) {
auto socket_addr = parseSocketAddress(addr, version, version_str, message);
if (socket_addr.port() != 0) {
ProtoExceptionUtil::throwProtoValidationException(
fmt::format("port must be 0 in this version of Envoy in address '{}'",
socket_addr.ToString()),
message);
}

return socket_addr.host();
}

FixedServerPreferredAddressConfig::FamilyAddresses
parseFamily(const std::string& addr_string,
const envoy::extensions::quic::server_preferred_address::v3::
FixedServerPreferredAddressConfig::AddressFamilyConfig* addresses,
Network::Address::IpVersion version, absl::string_view address_family,
const Protobuf::Message& message) {
FixedServerPreferredAddressConfig::FamilyAddresses ret;
if (addresses != nullptr) {
if (addresses->has_dnat_address() && !addresses->has_address()) {
ProtoExceptionUtil::throwProtoValidationException(
absl::StrCat("'dnat_address' but not 'address' is set in server preferred address for ",
address_family),
message);
}

if (addresses->has_address()) {
ret.spa_ = parseSocketAddress(addresses->address(), version, address_family, message);

if (!addresses->has_dnat_address() && ret.spa_.port() != 0) {
ProtoExceptionUtil::throwProtoValidationException(
fmt::format("'address' port must be zero unless 'dnat_address' is set in address {} "
"for address family {}",
ret.spa_.ToString(), address_family),
message);
}
}

if (addresses->has_dnat_address()) {
ret.dnat_ = parseIpAddressFromSocketAddress(addresses->dnat_address(), version,
address_family, message);
}
} else {
if (!addr_string.empty()) {
ret.spa_ = quic::QuicSocketAddress(parseIp(addr_string, address_family, message), 0);
}
}

return ret;
}

} // namespace

EnvoyQuicServerPreferredAddressConfig::Addresses
FixedServerPreferredAddressConfig::getServerPreferredAddresses(
const Network::Address::InstanceConstSharedPtr& local_address) {
int32_t port = local_address->ip()->port();
return {quic::QuicSocketAddress(ip_v4_, port), quic::QuicSocketAddress(ip_v6_, port)};
Addresses addresses;
addresses.ipv4_ = ipOrAddressToAddress(v4_.spa_, port);
addresses.ipv6_ = ipOrAddressToAddress(v6_.spa_, port);
addresses.dnat_ipv4_ = quic::QuicSocketAddress(v4_.dnat_, port);
addresses.dnat_ipv6_ = quic::QuicSocketAddress(v6_.dnat_, port);
return addresses;
}

Quic::EnvoyQuicServerPreferredAddressConfigPtr
Expand All @@ -18,20 +123,15 @@ FixedServerPreferredAddressConfigFactory::createServerPreferredAddressConfig(
MessageUtil::downcastAndValidate<const envoy::extensions::quic::server_preferred_address::v3::
FixedServerPreferredAddressConfig&>(message,
validation_visitor);
quic::QuicIpAddress ip_v4, ip_v6;
if (config.has_ipv4_address()) {
if (!ip_v4.FromString(config.ipv4_address())) {
ProtoExceptionUtil::throwProtoValidationException(
absl::StrCat("bad v4 server preferred address: ", config.ipv4_address()), message);
}
}
if (config.has_ipv6_address()) {
if (!ip_v6.FromString(config.ipv6_address())) {
ProtoExceptionUtil::throwProtoValidationException(
absl::StrCat("bad v6 server preferred address: ", config.ipv6_address()), message);
}
}
return std::make_unique<FixedServerPreferredAddressConfig>(ip_v4, ip_v6);

FixedServerPreferredAddressConfig::FamilyAddresses v4 =
parseFamily(config.ipv4_address(), config.has_ipv4_config() ? &config.ipv4_config() : nullptr,
Network::Address::IpVersion::v4, "v4", message);
FixedServerPreferredAddressConfig::FamilyAddresses v6 =
parseFamily(config.ipv6_address(), config.has_ipv6_config() ? &config.ipv6_config() : nullptr,
Network::Address::IpVersion::v6, "v6", message);

return std::make_unique<FixedServerPreferredAddressConfig>(v4, v6);
}

REGISTER_FACTORY(FixedServerPreferredAddressConfigFactory,
Expand Down
Expand Up @@ -11,16 +11,20 @@ namespace Quic {

class FixedServerPreferredAddressConfig : public Quic::EnvoyQuicServerPreferredAddressConfig {
public:
FixedServerPreferredAddressConfig(const quic::QuicIpAddress& ipv4,
const quic::QuicIpAddress& ipv6)
: ip_v4_(ipv4), ip_v6_(ipv6) {}
struct FamilyAddresses {
quic::QuicSocketAddress spa_;
quic::QuicIpAddress dnat_;
};

std::pair<quic::QuicSocketAddress, quic::QuicSocketAddress> getServerPreferredAddresses(
FixedServerPreferredAddressConfig(const FamilyAddresses& v4, const FamilyAddresses& v6)
: v4_(v4), v6_(v6) {}

Addresses getServerPreferredAddresses(
const Network::Address::InstanceConstSharedPtr& local_address) override;

private:
const quic::QuicIpAddress ip_v4_;
const quic::QuicIpAddress ip_v6_;
const FamilyAddresses v4_;
const FamilyAddresses v6_;
};

class FixedServerPreferredAddressConfigFactory
Expand Down
23 changes: 23 additions & 0 deletions test/extensions/quic/server_preferred_address/BUILD
@@ -0,0 +1,23 @@
load(
"//bazel:envoy_build_system.bzl",
"envoy_package",
)
load(
"//test/extensions:extensions_build_system.bzl",
"envoy_extension_cc_test",
)

licenses(["notice"]) # Apache 2

envoy_package()

envoy_extension_cc_test(
name = "fixed_server_preferred_address_test",
srcs = ["fixed_server_preferred_address_test.cc"],
extension_names = ["envoy.quic.server_preferred_address.fixed"],
tags = ["nofips"],
deps = [
"//source/extensions/quic/server_preferred_address:fixed_server_preferred_address_config_lib",
"//test/mocks/protobuf:protobuf_mocks",
],
)

0 comments on commit c940f24

Please sign in to comment.