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 6 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,51 @@ 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;

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.
config.core.v3.SocketAddress ipv4_address_and_port = 3;
Copy link
Contributor

Choose a reason for hiding this comment

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

Out of curiosity, why do we need a string and a struct version of this field?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The string version does not allow setting the port. I think it was done this way because we don't yet support multiple sockets on a quic listener; it has to get the original and spa traffic on the same socket, bound to 0.0.0.0:443 or similar, so the SPA address must use the same port, and so it isn't allowed to be configured here.

I am hoping to allow multiple sockets in the future, but this is what we have for now.

But with DNAT, the client's view of the destination may not be what the server receives, so if the DNAT also does port translation, we can now support having a different port in the address advertised to clients.

Copy link
Contributor

Choose a reason for hiding this comment

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

We had a string version because we don't want to make port part of the config as the port is supposed to be the same as the listening port.

With DNAT do we need to advertise a different port in transport parameter?

Copy link
Contributor

@danzh2010 danzh2010 Apr 30, 2024

Choose a reason for hiding this comment

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

Since you added a new knob to allow a different port. I think we can keep the old behavior of the string form with a comment "if it is desired to advertise a different port, use the struct form".

As to the new knob, how about defining a struct

DNatAddressPair {
// Port will be used.
config.core.v3.SocketAddress address_to_advertise;
// Only the IP part. Different port is not supported yet. 
string dnat_internal_address;
};

And then

 oneof ipv4_type {
    // String representation of IPv4 address, i.e. "127.0.0.2".
    //  Port will not be used.
    string ipv4_address = 1;

    DNatAddressPair ipv4_addresses_with_dnat = 2;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Given that the current restriction on the port being equal is an implementation restriction, that may be alleviated in the future, would it be better to use SocketAddress for both addresses, and document that the port is currently ignored, so that we don't have to change proto in the future to allow setting the port if/when that happens?

Copy link
Contributor

Choose a reason for hiding this comment

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

SG. And please also emit an error during config validation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I pushed a commit with what I think we've agreed on for the config. @danzh2010 @abeyad can you please take a look and confirm this looks roughly correct; then I'll make the related code changes (and add the validation that the port == 0 in the cases where it isn't/can't be used).

}

oneof ipv4_dnat_type {
// If there is a DNAT between the client and Envoy, the address that Envoy will observe
// server preferred address IPv4 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.
//
// String representation of IPv4 address, i.e. "127.0.0.2"
// The listener's port will be used.
string ipv4_dnat_address = 5;
}

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.
config.core.v3.SocketAddress ipv6_address_and_port = 4;
}

oneof ipv6_dnat_type {
// If there is a DNAT between the client and Envoy, the address that Envoy will observe
// server preferred address IPv6 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.
//
// String representation of IPv6 address, i.e. "::1"
// The listener's port will be used.
string ipv6_dnat_address = 6;
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:

* Prefer multiple fields with defined precedence over boolean overloads of fields or

Is it necessary in this case? Do we anticipate more fields to select from?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm trying to mirror the other field here. It'll probably need a similar set of options as ipv6_address is getting in this PR. See #33774 (comment) for discussion on why it is this way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thinking about this a little further, I think using the config.core.v3.Address is a better type for this data, and in the cases where we currently cannot specify a port, ignore it (or throw a validation error if it isn't zero). Then we can deprecate the existing string version, and stop using the oneof's at all. Wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@abeyad and @RyanTheOptimist would it be better like this (plus the equivalent for ipv6)?

  oneof ipv4_type {
    // Deprecated
    string ipv4_address = 1;

  }

  // The IPv4 address to advertise to clients for Server Preferred Address.
  config.core.v3.SocketAddress ipv4_address_and_port = 3;

  // Port isn't yet available; must be zero or ignored
  config.core.v3.SocketAddress ipv4_dnat_address = 5;

Copy link
Contributor

Choose a reason for hiding this comment

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

to make sure I understand, ipv4_dnat_type and ipv6_dnat_type would go away, and we'd just have:

config.core.v3.Address ipv4_dnat_address;
config.core.v3.Address ipv6_dnat_address;

is that correct?

Copy link
Contributor

Choose a reason for hiding this comment

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

That SGTM, thanks @ggreenway !

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we really need port in non-DNAT case? Even in DNAT case, the client doesn't see a different port right? Only the translation unit will translate [SPA:443] to something else [configured_address:port], is this correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the DNAT is translating both IP and port, then the client-advertised address may need a different port than the listener.

Also, I'm considering adding support for quic listeners to support additional_addresses; if that were true then in the non-DNAT case, the client-advertised address may also need a different port, and the DNAT address may need a different port.

Copy link
Contributor

Choose a reason for hiding this comment

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

QUICHE doesn't support multiple writers for a server connection. Is there a use case for advertising a different port for SPA?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The use case is a host in AWS with a single NIC and IP, with both an NLB for initial connection load balancing, and an external EIP for SPA traffic. If the two paths go to different ports, envoy/quiche can easily differentiate which path the traffic is coming from. If they go to the same port, envoy/quiche sees the client attempting SPA as the client trying to do a voluntary migration during the initial handshake.

}
}
4 changes: 4 additions & 0 deletions changelogs/current.yaml
Expand Up @@ -47,5 +47,9 @@ 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>`.

deprecated:
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
24 changes: 24 additions & 0 deletions test/extensions/quic/server_preferred_address/BUILD
@@ -0,0 +1,24 @@
load(
"//bazel:envoy_build_system.bzl",
"envoy_cc_test_library",
"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",
],
)