Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

quic: support for server-preferred address behind DNAT #33774

Merged
merged 23 commits into from May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -62,6 +62,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 @@ -343,22 +343,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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Brief comment, please.

// 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment is super clear. Thanks!

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
Copy link
Contributor

Choose a reason for hiding this comment

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

Why this interface needs to be changed? And why the proto struct is more preferrable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because it is now returning 4 addresses instead of two. And a std::tuple of 4 elements is harder to read than a struct with four elements.

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",
],
)