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 9 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 @@ -16,20 +18,49 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
// [#extension: envoy.quic.server_preferred_address.fixed]

// Configuration for FixedServerPreferredAddressConfig.
// [#next-free-field: 7]
message FixedServerPreferredAddressConfig {
// [#comment:TODO(danzh2010): discuss with API shepherds before removing WiP status.]

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

// Addreses for server preferred address for a single address family (IPv4 or IPv6).
message Addresses {
Copy link
Contributor

Choose a reason for hiding this comment

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

nits: how about AddressPair?

// The server preferred address sent to clients.
//
// Note: Envoy currently must receive all packets for a QUIC connection on the same port, so
// unless `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 = 1;
}

oneof ipv4_type {
// String representation of IPv4 address, i.e. "127.0.0.2".
// The listener's port will be used.
// If not specified, none will be configured.
string ipv4_address = 1;

// The IPv4 address to advertise to clients for Server Preferred Address.
Addresses ipv4_config = 3;
Copy link
Contributor

Choose a reason for hiding this comment

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

nits: how about ipv4_addresses_with_dnat

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 if we allow setting a different port, without dnat, then this config will be used in a non-dnat case, and the dnat address in the message will be left unset. I'm trying to future-proof the config so we don't need to change it again.

}

oneof ipv6_type {
// String representation of IPv6 address, i.e. "::1".
// The listener's port will be used.
// If not specified, none will be configured.
string ipv6_address = 2;

// The IPv6 address to advertise to clients for Server Preferred Address.
Addresses ipv6_config = 4;
Copy link
Contributor

Choose a reason for hiding this comment

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

The structure of all this LGTM.

I'm unsure if we should try to move the new Address type fields out of the oneof though, in keeping with the new API style guidance discouraging oneof in favor of defining precedence rules (e.g. this PR does that for an existing API https://github.com/envoyproxy/envoy/pull/33749/files#diff-e18722c3190aeb921472834ba2f4725bcfa5dfae5c024d9d7aca8cd2ecefeaee).

That would mean keeping the oneof, but defining ipv4_config and ipv6_config as separate fields outside of the oneof and using precedence rules that if ipv6_config (or ipv4_config) is defined, use that, otherwise use ipv6_address (or ipv4_address).

Would that work?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pushed another version, which I think changes it as you're requesting. PTAL.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are any of the normal rules for changing protos relaxed due to option (xds.annotations.v3.message_status).work_in_progress = true;? Could we remove the oneof wrapper, since nothing else will ever go into it?

Copy link
Contributor

Choose a reason for hiding this comment

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

LGTM, thanks Greg!

If it's marked work_in_progress, we could remove the oneof AFAIU. In https://github.com/envoyproxy/envoy/blob/main/api/STYLE.md, we have this clause:

Use a (xds.annotations.v3.file_status).work_in_progress, (xds.annotations.v3.message_status).work_in_progress, or (xds.annotations.v3.field_status).work_in_progress option annotation for files, messages, or fields, respectively, that are considered work in progress and are not subject to the threat model or the breaking change policy.

So we should be able to change the oneof since it's marked as work_in_progress.

cc @envoyproxy/api-shepherds in case there is a differing opinion on this.

Copy link
Contributor

Choose a reason for hiding this comment

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

The current structure looks good, other than a nits for renaming. Having oneof also makes sense to me, is there a reason why we want to remove it?

Copy link
Contributor

Choose a reason for hiding this comment

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

oneof is now discouraged by the API style guide: https://github.com/envoyproxy/envoy/blob/main/api/STYLE.md?plain=1#L63-L90

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@danzh2010 I didn't want to change the names to be overly specific in case we need to add new fields, and they don't work well with those very specific names. For Addresses, maybe is could be AddressFamilyConfig?

Copy link
Contributor

Choose a reason for hiding this comment

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

AddressFamilyConfig sounds better.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed name to AddressFamilyConfig

}
}
4 changes: 4 additions & 0 deletions changelogs/current.yaml
Expand Up @@ -62,6 +62,10 @@ 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.ipv4_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
8 changes: 7 additions & 1 deletion configs/envoyproxy_io_proxy_http3_downstream.yaml
Expand Up @@ -56,8 +56,14 @@ static_resources:
address:
socket_address:
protocol: UDP
address: 0.0.0.0
address: 127.0.0.1
port_value: 10000
additional_addresses:
- address:
socket_address:
protocol: UDP
address: 127.0.0.1
port_value: 10001
udp_listener_config:
quic_options: {}
downstream_socket_config:
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,76 @@
#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 FixedServerPreferredAddressConfig::QuicSocketOrIpAddress& address,
int32_t port) {
return absl::visit(
[&](const auto& arg) -> quic::QuicSocketAddress {
using T = std::decay_t<decltype(arg)>;

if constexpr (std::is_same_v<T, quic::QuicSocketAddress>) {
return arg;
}

if constexpr (std::is_same_v<T, quic::QuicIpAddress>) {
return quic::QuicSocketAddress(arg, port);
}

IS_ENVOY_BUG(fmt::format("Unhandled type in variant visitor: {}", address.index()));
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());
}

} // 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(ip_v4_, port);
addresses.ipv6_ = ipOrAddressToAddress(ip_v6_, port);
addresses.dnat_ipv4_ = quic::QuicSocketAddress(dnat_ip_v4_, port);
addresses.dnat_ipv6_ = quic::QuicSocketAddress(dnat_ip_v6_, port);
return addresses;
}

Quic::EnvoyQuicServerPreferredAddressConfigPtr
Expand All @@ -18,20 +81,47 @@ 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);
}
FixedServerPreferredAddressConfig::QuicSocketOrIpAddress ip_v4, ip_v6;
switch (config.ipv4_type_case()) {
case envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig::
kIpv4Address:
ip_v4 = parseIp(config.ipv4_address(), "v4", message);
break;
case envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig::
kIpv4AddressAndPort:
ip_v4 = parseSocketAddress(config.ipv4_address_and_port(), Network::Address::IpVersion::v4,
"v4", message);
break;
case envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig::
IPV4_TYPE_NOT_SET:
break;
}

switch (config.ipv6_type_case()) {
case envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig::
kIpv6Address:
ip_v6 = parseIp(config.ipv6_address(), "v6", message);
break;
case envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig::
kIpv6AddressAndPort:
ip_v6 = parseSocketAddress(config.ipv6_address_and_port(), Network::Address::IpVersion::v6,
"v6", message);
break;
case envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig::
IPV6_TYPE_NOT_SET:
break;
}
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);
}

quic::QuicIpAddress dnat_v4, dnat_v6;
if (config.has_ipv4_dnat_address()) {
dnat_v4 = parseIp(config.ipv4_dnat_address(), "dnat v4", message);
}
return std::make_unique<FixedServerPreferredAddressConfig>(ip_v4, ip_v6);

if (config.has_ipv6_dnat_address()) {
dnat_v6 = parseIp(config.ipv6_dnat_address(), "dnat v6", message);
}

return std::make_unique<FixedServerPreferredAddressConfig>(ip_v4, ip_v6, dnat_v4, dnat_v6);
}

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

class FixedServerPreferredAddressConfig : public Quic::EnvoyQuicServerPreferredAddressConfig {
public:
FixedServerPreferredAddressConfig(const quic::QuicIpAddress& ipv4,
const quic::QuicIpAddress& ipv6)
: ip_v4_(ipv4), ip_v6_(ipv6) {}
using QuicSocketOrIpAddress = absl::variant<quic::QuicSocketAddress, quic::QuicIpAddress>;

std::pair<quic::QuicSocketAddress, quic::QuicSocketAddress> getServerPreferredAddresses(
FixedServerPreferredAddressConfig(const QuicSocketOrIpAddress& ipv4,
const QuicSocketOrIpAddress& ipv6,
const quic::QuicIpAddress ipv4_dnat,
const quic::QuicIpAddress ipv6_dnat)
: ip_v4_(ipv4), ip_v6_(ipv6), dnat_ip_v4_(ipv4_dnat), dnat_ip_v6_(ipv6_dnat) {}

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

private:
const quic::QuicIpAddress ip_v4_;
const quic::QuicIpAddress ip_v6_;
const QuicSocketOrIpAddress ip_v4_;
const QuicSocketOrIpAddress ip_v6_;
const quic::QuicIpAddress dnat_ip_v4_;
const quic::QuicIpAddress dnat_ip_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",
],
)