diff --git a/api/envoy/extensions/quic/server_preferred_address/v3/BUILD b/api/envoy/extensions/quic/server_preferred_address/v3/BUILD index d49202b74ab4..628f71321fba 100644 --- a/api/envoy/extensions/quic/server_preferred_address/v3/BUILD +++ b/api/envoy/extensions/quic/server_preferred_address/v3/BUILD @@ -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", ], diff --git a/api/envoy/extensions/quic/server_preferred_address/v3/fixed_server_preferred_address_config.proto b/api/envoy/extensions/quic/server_preferred_address/v3/fixed_server_preferred_address_config.proto index b500263a9a6c..28d6cd57873f 100644 --- a/api/envoy/extensions/quic/server_preferred_address/v3/fixed_server_preferred_address_config.proto +++ b/api/envoy/extensions/quic/server_preferred_address/v3/fixed_server_preferred_address_config.proto @@ -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"; @@ -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 ` + // 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 `. + 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 `. + AddressFamilyConfig ipv6_config = 4; } diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 666afa96a451..278e283ccf2c 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -62,6 +62,11 @@ new_features: change: | Added :ref:`Filter State Input ` 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 + `. - area: cares change: | Added :ref:`udp_max_queries` diff --git a/source/common/quic/active_quic_listener.cc b/source/common/quic/active_quic_listener.cc index 57db1441bc6f..079b18153ad4 100644 --- a/source/common/quic/active_quic_listener.cc +++ b/source/common/quic/active_quic_listener.cc @@ -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 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_); + } } } diff --git a/source/common/quic/envoy_quic_server_preferred_address_config_factory.h b/source/common/quic/envoy_quic_server_preferred_address_config_factory.h index 93a8d3401f85..3ec363968e08 100644 --- a/source/common/quic/envoy_quic_server_preferred_address_config_factory.h +++ b/source/common/quic/envoy_quic_server_preferred_address_config_factory.h @@ -15,6 +15,21 @@ 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. @@ -22,7 +37,7 @@ class EnvoyQuicServerPreferredAddressConfig { * the entire life time of the QUIC listener. An uninitialized address value means no preferred * address for that address family. */ - virtual std::pair + virtual Addresses getServerPreferredAddresses(const Network::Address::InstanceConstSharedPtr& local_address) PURE; }; diff --git a/source/extensions/quic/server_preferred_address/BUILD b/source/extensions/quic/server_preferred_address/BUILD index 9f2a46f28320..3e42f9ac7f9d 100644 --- a/source/extensions/quic/server_preferred_address/BUILD +++ b/source/extensions/quic/server_preferred_address/BUILD @@ -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, diff --git a/source/extensions/quic/server_preferred_address/fixed_server_preferred_address_config.cc b/source/extensions/quic/server_preferred_address/fixed_server_preferred_address_config.cc index ff34cf2c9eae..805771dcfeef 100644 --- a/source/extensions/quic/server_preferred_address/fixed_server_preferred_address_config.cc +++ b/source/extensions/quic/server_preferred_address/fixed_server_preferred_address_config.cc @@ -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 +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 @@ -18,20 +123,15 @@ FixedServerPreferredAddressConfigFactory::createServerPreferredAddressConfig( MessageUtil::downcastAndValidate(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(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(v4, v6); } REGISTER_FACTORY(FixedServerPreferredAddressConfigFactory, diff --git a/source/extensions/quic/server_preferred_address/fixed_server_preferred_address_config.h b/source/extensions/quic/server_preferred_address/fixed_server_preferred_address_config.h index c9e70f30e8a5..0ec0cc2bc7b8 100644 --- a/source/extensions/quic/server_preferred_address/fixed_server_preferred_address_config.h +++ b/source/extensions/quic/server_preferred_address/fixed_server_preferred_address_config.h @@ -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 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 diff --git a/test/extensions/quic/server_preferred_address/BUILD b/test/extensions/quic/server_preferred_address/BUILD new file mode 100644 index 000000000000..74eff89da95a --- /dev/null +++ b/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", + ], +) diff --git a/test/extensions/quic/server_preferred_address/fixed_server_preferred_address_test.cc b/test/extensions/quic/server_preferred_address/fixed_server_preferred_address_test.cc new file mode 100644 index 000000000000..75aafba17c2e --- /dev/null +++ b/test/extensions/quic/server_preferred_address/fixed_server_preferred_address_test.cc @@ -0,0 +1,151 @@ +#include "source/common/network/utility.h" +#include "source/extensions/quic/server_preferred_address/fixed_server_preferred_address_config.h" + +#include "test/mocks/protobuf/mocks.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Quic { + +class FixedServerPreferredAddressConfigTest : public ::testing::Test { +public: + FixedServerPreferredAddressConfigFactory factory_; + testing::NiceMock visitor_; +}; + +TEST_F(FixedServerPreferredAddressConfigTest, Validation) { + { + // Bad address_and_port. + envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig cfg; + cfg.mutable_ipv4_config()->mutable_address()->set_address("not an address"); + cfg.mutable_ipv4_config()->mutable_address()->set_port_value(1); + EXPECT_THROW_WITH_REGEX(factory_.createServerPreferredAddressConfig(cfg, visitor_, {}), + EnvoyException, ".*malformed IP address: not an address.*"); + } + { + // Bad address. + envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig cfg; + cfg.set_ipv4_address("not an address"); + EXPECT_THROW_WITH_REGEX(factory_.createServerPreferredAddressConfig(cfg, visitor_, {}), + EnvoyException, ".*bad v4 server preferred address: not an address.*"); + } + { + // Non-zero port not supported in dnat address. + envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig cfg; + cfg.mutable_ipv4_config()->mutable_address()->set_address("127.0.0.1"); + cfg.mutable_ipv4_config()->mutable_address()->set_port_value(1); + cfg.mutable_ipv4_config()->mutable_dnat_address()->set_address("127.0.0.1"); + cfg.mutable_ipv4_config()->mutable_dnat_address()->set_port_value(1); + EXPECT_THROW_WITH_REGEX(factory_.createServerPreferredAddressConfig(cfg, visitor_, {}), + EnvoyException, + ".*port must be 0 in this version of Envoy in address '127.0.0.1:1'.*"); + } + { + // Cannot set dnat address but not spa address. + envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig cfg; + cfg.mutable_ipv4_config()->mutable_dnat_address()->set_address("127.0.0.1"); + cfg.mutable_ipv4_config()->mutable_dnat_address()->set_port_value(1); + EXPECT_THROW_WITH_REGEX( + factory_.createServerPreferredAddressConfig(cfg, visitor_, {}), EnvoyException, + ".*'dnat_address' but not 'address' is set in server preferred address for v4.*"); + } + { + // Cannot set port on address if dnat address isn't set. + envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig cfg; + cfg.mutable_ipv4_config()->mutable_address()->set_address("127.0.0.1"); + cfg.mutable_ipv4_config()->mutable_address()->set_port_value(1); + EXPECT_THROW_WITH_REGEX(factory_.createServerPreferredAddressConfig(cfg, visitor_, {}), + EnvoyException, + ".*'address' port must be zero unless 'dnat_address' is set in address " + "127.0.0.1:1 for address family v4.*"); + } + { + // v6 address in v4 field. + envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig cfg; + cfg.mutable_ipv4_config()->mutable_address()->set_address("::1"); + cfg.mutable_ipv4_config()->mutable_address()->set_port_value(1); + EXPECT_THROW_WITH_REGEX(factory_.createServerPreferredAddressConfig(cfg, visitor_, {}), + EnvoyException, + ".*wrong address type for v4 server preferred address.*"); + } + { + // v4 address in v6 field. + envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig cfg; + cfg.mutable_ipv6_config()->mutable_address()->set_address("127.0.0.1"); + cfg.mutable_ipv6_config()->mutable_address()->set_port_value(1); + EXPECT_THROW_WITH_REGEX(factory_.createServerPreferredAddressConfig(cfg, visitor_, {}), + EnvoyException, + ".*wrong address type for v6 server preferred address.*"); + } +} + +TEST_F(FixedServerPreferredAddressConfigTest, AddressGetsCombinedWithPort) { + envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig cfg; + cfg.set_ipv4_address("1.2.3.4"); + auto obj = factory_.createServerPreferredAddressConfig(cfg, visitor_, {}); + auto addresses = obj->getServerPreferredAddresses( + Network::Utility::parseInternetAddressNoThrow("127.0.0.1", 1234)); + EXPECT_EQ(addresses.ipv4_.ToString(), "1.2.3.4:1234"); +} + +TEST_F(FixedServerPreferredAddressConfigTest, AddressAndPortIgnoresListenerPort) { + envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig cfg; + cfg.mutable_ipv4_config()->mutable_address()->set_address("1.2.3.4"); + cfg.mutable_ipv4_config()->mutable_address()->set_port_value(5); + cfg.mutable_ipv4_config()->mutable_dnat_address()->set_address("127.0.0.1"); + cfg.mutable_ipv4_config()->mutable_dnat_address()->set_port_value(0); + auto obj = factory_.createServerPreferredAddressConfig(cfg, visitor_, {}); + auto addresses = obj->getServerPreferredAddresses( + Network::Utility::parseInternetAddressNoThrow("127.0.0.1", 1234)); + EXPECT_EQ(addresses.ipv4_.ToString(), "1.2.3.4:5"); +} + +TEST_F(FixedServerPreferredAddressConfigTest, AddressAndZeroPortUsesListenerPort) { + envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig cfg; + cfg.mutable_ipv4_config()->mutable_address()->set_address("1.2.3.4"); + cfg.mutable_ipv4_config()->mutable_address()->set_port_value(0); + auto obj = factory_.createServerPreferredAddressConfig(cfg, visitor_, {}); + auto addresses = obj->getServerPreferredAddresses( + Network::Utility::parseInternetAddressNoThrow("127.0.0.1", 1234)); + EXPECT_EQ(addresses.ipv4_.ToString(), "1.2.3.4:1234"); +} + +TEST_F(FixedServerPreferredAddressConfigTest, DnatAddressAndZeroPortUsesListenerPort) { + envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig cfg; + cfg.mutable_ipv4_config()->mutable_address()->set_address("1.2.3.4"); + cfg.mutable_ipv4_config()->mutable_address()->set_port_value(0); + cfg.mutable_ipv4_config()->mutable_dnat_address()->set_address("127.0.0.1"); + cfg.mutable_ipv4_config()->mutable_dnat_address()->set_port_value(0); + auto obj = factory_.createServerPreferredAddressConfig(cfg, visitor_, {}); + auto addresses = obj->getServerPreferredAddresses( + Network::Utility::parseInternetAddressNoThrow("127.0.0.1", 1234)); + EXPECT_EQ(addresses.dnat_ipv4_.ToString(), "127.0.0.1:1234"); +} + +// `ipv4_config` is preferred over `ipv4_address` if both are set. +TEST_F(FixedServerPreferredAddressConfigTest, FieldPrecedence) { + envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig cfg; + cfg.set_ipv4_address("2.2.2.2"); + cfg.mutable_ipv4_config()->mutable_address()->set_address("1.2.3.4"); + cfg.mutable_ipv4_config()->mutable_address()->set_port_value(0); + auto obj = factory_.createServerPreferredAddressConfig(cfg, visitor_, {}); + auto addresses = obj->getServerPreferredAddresses( + Network::Utility::parseInternetAddressNoThrow("127.0.0.1", 1234)); + EXPECT_EQ(addresses.ipv4_.ToString(), "1.2.3.4:1234"); +} + +// If only `ipv4_address` is set, it is used. +TEST_F(FixedServerPreferredAddressConfigTest, LegacyField) { + envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig cfg; + cfg.set_ipv4_address("2.2.2.2"); + auto obj = factory_.createServerPreferredAddressConfig(cfg, visitor_, {}); + auto addresses = obj->getServerPreferredAddresses( + Network::Utility::parseInternetAddressNoThrow("127.0.0.1", 1234)); + EXPECT_EQ(addresses.ipv4_.ToString(), "2.2.2.2:1234"); +} + +} // namespace Quic +} // namespace Envoy diff --git a/test/integration/filters/test_socket_interface.cc b/test/integration/filters/test_socket_interface.cc index 02f2618ca24a..feec0dd4b585 100644 --- a/test/integration/filters/test_socket_interface.cc +++ b/test/integration/filters/test_socket_interface.cc @@ -13,24 +13,44 @@ namespace Envoy { namespace Network { +Api::IoCallUint64Result TestIoSocketHandle::recvmsg(Buffer::RawSlice* slices, + const uint64_t num_slice, uint32_t self_port, + RecvMsgOutput& output) { + + auto result = Test::IoSocketHandlePlatformImpl::recvmsg(slices, num_slice, self_port, output); + if (read_override_) { + read_override_(output); + } + + return result; +} + Api::IoCallUint64Result TestIoSocketHandle::sendmsg(const Buffer::RawSlice* slices, uint64_t num_slice, int flags, const Address::Ip* self_ip, const Address::Instance& peer_address) { + Address::InstanceConstSharedPtr dnat_peer_address; + if (write_override_) { peer_address_override_ = peer_address; - auto result = write_override_(this, slices, num_slice); + auto result = + write_override_(this, slices, num_slice, + dnat_peer_address); // `dnat_peer_address` can be changed in this call. peer_address_override_.reset(); if (result.has_value()) { return std::move(result).value(); } } - return Test::IoSocketHandlePlatformImpl::sendmsg(slices, num_slice, flags, self_ip, peer_address); + return Test::IoSocketHandlePlatformImpl::sendmsg( + slices, num_slice, flags, self_ip, + (dnat_peer_address != nullptr) ? *dnat_peer_address : peer_address); } Api::IoCallUint64Result TestIoSocketHandle::writev(const Buffer::RawSlice* slices, uint64_t num_slice) { + Address::InstanceConstSharedPtr dnat_peer_address; if (write_override_) { - auto result = write_override_(this, slices, num_slice); + auto result = write_override_(this, slices, num_slice, dnat_peer_address); + ENVOY_BUG(dnat_peer_address == nullptr, "Only works for sendmsg, not writev"); if (result.has_value()) { return std::move(result).value(); } @@ -44,8 +64,8 @@ IoHandlePtr TestIoSocketHandle::accept(struct sockaddr* addr, socklen_t* addrlen return nullptr; } - return std::make_unique(write_override_, result.return_value_, socket_v6only_, - domain_); + return std::make_unique(connect_override_, write_override_, read_override_, + result.return_value_, socket_v6only_, domain_); } IoHandlePtr TestIoSocketHandle::duplicate() { @@ -54,23 +74,24 @@ IoHandlePtr TestIoSocketHandle::duplicate() { throw EnvoyException(fmt::format("duplicate failed for '{}': ({}) {}", fd_, result.errno_, errorDetails(result.errno_))); } - return std::make_unique(write_override_, result.return_value_, socket_v6only_, - domain_); + return std::make_unique(connect_override_, write_override_, read_override_, + result.return_value_, socket_v6only_, domain_); } Api::SysCallIntResult TestIoSocketHandle::connect(Address::InstanceConstSharedPtr address) { - if (write_override_) { - auto result = write_override_(this, nullptr, 0); - if (result.has_value()) { + if (connect_override_) { + auto result = connect_override_(this); + if (result.has_value()) return Api::SysCallIntResult{-1, EINPROGRESS}; - } } + return Test::IoSocketHandlePlatformImpl::connect(address); } IoHandlePtr TestSocketInterface::makeSocket(int socket_fd, bool socket_v6only, absl::optional domain) const { - return std::make_unique(write_override_proc_, socket_fd, socket_v6only, + return std::make_unique(connect_override_proc_, write_override_proc_, + read_override_proc_, socket_fd, socket_v6only, domain); } diff --git a/test/integration/filters/test_socket_interface.h b/test/integration/filters/test_socket_interface.h index 850904a7ebd2..df6d1cf56cae 100644 --- a/test/integration/filters/test_socket_interface.h +++ b/test/integration/filters/test_socket_interface.h @@ -22,15 +22,21 @@ namespace Network { class TestIoSocketHandle : public Test::IoSocketHandlePlatformImpl { public: - using WriteOverrideType = absl::optional(TestIoSocketHandle* io_handle, - const Buffer::RawSlice* slices, - uint64_t num_slice); + using WriteOverrideType = absl::optional( + TestIoSocketHandle* io_handle, const Buffer::RawSlice* slices, uint64_t num_slice, + Address::InstanceConstSharedPtr& peer_address_override_out); using WriteOverrideProc = std::function; - - TestIoSocketHandle(WriteOverrideProc write_override_proc, os_fd_t fd = INVALID_SOCKET, - bool socket_v6only = false, absl::optional domain = absl::nullopt) + using ReadOverrideProc = std::function; + using ConnectOverrideProc = + std::function(TestIoSocketHandle* io_handle)>; + + TestIoSocketHandle(ConnectOverrideProc connect_override_proc, + WriteOverrideProc write_override_proc, ReadOverrideProc read_override_proc, + os_fd_t fd = INVALID_SOCKET, bool socket_v6only = false, + absl::optional domain = absl::nullopt) : Test::IoSocketHandlePlatformImpl(fd, socket_v6only, domain), - write_override_(write_override_proc) { + connect_override_(connect_override_proc), write_override_(write_override_proc), + read_override_(read_override_proc) { int type; socklen_t length = sizeof(int); EXPECT_EQ(0, getOption(SOL_SOCKET, SO_TYPE, &type, &length).return_value_); @@ -73,11 +79,15 @@ class TestIoSocketHandle : public Test::IoSocketHandlePlatformImpl { Api::IoCallUint64Result sendmsg(const Buffer::RawSlice* slices, uint64_t num_slice, int flags, const Address::Ip* self_ip, const Address::Instance& peer_address) override; + Api::IoCallUint64Result recvmsg(Buffer::RawSlice* slices, const uint64_t num_slice, + uint32_t self_port, RecvMsgOutput& output) override; IoHandlePtr duplicate() override; OptRef peer_address_override_; + const ConnectOverrideProc connect_override_; const WriteOverrideProc write_override_; + const ReadOverrideProc read_override_; absl::Mutex mutex_; Event::Dispatcher* dispatcher_ ABSL_GUARDED_BY(mutex_) = nullptr; Socket::Type socket_type_; @@ -102,14 +112,19 @@ class TestSocketInterface : public SocketInterfaceImpl { * write methods. Returning a Api::IoCallUint64Result from callback skips * the write methods with the returned result value. */ - TestSocketInterface(TestIoSocketHandle::WriteOverrideProc write) : write_override_proc_(write) {} + TestSocketInterface(TestIoSocketHandle::ConnectOverrideProc connect, + TestIoSocketHandle::WriteOverrideProc write, + TestIoSocketHandle::ReadOverrideProc read) + : connect_override_proc_(connect), write_override_proc_(write), read_override_proc_(read) {} private: // SocketInterfaceImpl IoHandlePtr makeSocket(int socket_fd, bool socket_v6only, absl::optional domain) const override; + const TestIoSocketHandle::ConnectOverrideProc connect_override_proc_; const TestIoSocketHandle::WriteOverrideProc write_override_proc_; + const TestIoSocketHandle::ReadOverrideProc read_override_proc_; }; } // namespace Network diff --git a/test/integration/quic_http_integration_test.cc b/test/integration/quic_http_integration_test.cc index e18042946688..b2c4e7a4cd05 100644 --- a/test/integration/quic_http_integration_test.cc +++ b/test/integration/quic_http_integration_test.cc @@ -1935,6 +1935,90 @@ TEST_P(QuicHttpIntegrationTest, UsesPreferredAddress) { } } +TEST_P(QuicHttpIntegrationTest, UsesPreferredAddressDNAT) { + autonomous_upstream_ = true; + config_helper_.addConfigModifier( + [=, this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) -> void { + auto* listen_address = bootstrap.mutable_static_resources() + ->mutable_listeners(0) + ->mutable_address() + ->mutable_socket_address(); + // Change listening address to Any. + listen_address->set_address(Network::Test::getAnyAddressString(version_)); + auto* preferred_address_config = bootstrap.mutable_static_resources() + ->mutable_listeners(0) + ->mutable_udp_listener_config() + ->mutable_quic_options() + ->mutable_server_preferred_address_config(); + + // Configure a loopback interface as the server's preferred address. + preferred_address_config->set_name("quic.server_preferred_address.fixed"); + envoy::extensions::quic::server_preferred_address::v3::FixedServerPreferredAddressConfig + server_preferred_address; + server_preferred_address.mutable_ipv4_config()->mutable_address()->set_address("1.2.3.4"); + server_preferred_address.mutable_ipv4_config()->mutable_address()->set_port_value(12345); + server_preferred_address.mutable_ipv4_config()->mutable_dnat_address()->set_address( + "127.0.0.2"); + server_preferred_address.mutable_ipv4_config()->mutable_dnat_address()->set_port_value(0); + + server_preferred_address.mutable_ipv6_config()->mutable_address()->set_address("::1"); + server_preferred_address.mutable_ipv6_config()->mutable_address()->set_port_value(12345); + server_preferred_address.mutable_ipv6_config()->mutable_dnat_address()->set_address("::2"); + server_preferred_address.mutable_ipv6_config()->mutable_dnat_address()->set_port_value(0); + preferred_address_config->mutable_typed_config()->PackFrom(server_preferred_address); + + // Configure a test listener filter which is incompatible with any server preferred + // addresses but with any matcher, which effectively disables the filter. + auto* listener_filter = + bootstrap.mutable_static_resources()->mutable_listeners(0)->add_listener_filters(); + listener_filter->set_name("dumb_filter"); + auto configuration = test::integration::filters::TestQuicListenerFilterConfig(); + configuration.set_added_value("foo"); + configuration.set_allow_server_migration(false); + configuration.set_allow_client_migration(false); + listener_filter->mutable_typed_config()->PackFrom(configuration); + listener_filter->mutable_filter_disabled()->set_any_match(true); + }); + + initialize(); + auto listener_port = lookupPort("http"); + + // Setup DNAT for 0.0.0.0:12345-->127.0.0.2:listener_port + SocketInterfaceSwap socket_swap(Network::Socket::Type::Datagram); + socket_swap.write_matcher_->setDnat( + Network::Utility::parseInternetAddress("1.2.3.4", 12345), + Network::Utility::parseInternetAddress("127.0.0.2", listener_port)); + + codec_client_ = makeHttpConnection(makeClientConnection(listener_port)); + EnvoyQuicClientSession* quic_session = + static_cast(codec_client_->connection()); + EXPECT_EQ(Network::Test::getLoopbackAddressString(version_), + quic_connection_->peer_address().host().ToString()); + ASSERT_TRUE((version_ == Network::Address::IpVersion::v4 && + quic_session->config()->HasReceivedIPv4AlternateServerAddress()) || + (version_ == Network::Address::IpVersion::v6 && + quic_session->config()->HasReceivedIPv6AlternateServerAddress())); + ASSERT_TRUE(quic_connection_->waitForHandshakeDone()); + EXPECT_TRUE(quic_connection_->IsValidatingServerPreferredAddress()); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, + {":path", "/test/long/url"}, + {":authority", "sni.lyft.com"}, + {":scheme", "http"}, + {AutonomousStream::RESPONSE_SIZE_BYTES, std::to_string(1024 * 1024)}}; + IntegrationStreamDecoderPtr response = + codec_client_->makeHeaderOnlyRequest(default_request_headers_); + EXPECT_TRUE(response->waitForEndStream()); + ASSERT_TRUE(response->complete()); + + if (version_ == Network::Address::IpVersion::v4) { + // Most v6 platform doesn't support two loopback interfaces. + EXPECT_EQ("1.2.3.4", quic_connection_->peer_address().host().ToString()); + test_server_->waitForCounterGe( + "listener.0.0.0.0_0.quic.connection.num_packets_rx_on_preferred_address", 2u); + } +} + TEST_P(QuicHttpIntegrationTest, PreferredAddressRuntimeFlag) { autonomous_upstream_ = true; config_helper_.addRuntimeOverride( diff --git a/test/integration/socket_interface_swap.cc b/test/integration/socket_interface_swap.cc index b441ebd09a16..9ea29fa7ad3b 100644 --- a/test/integration/socket_interface_swap.cc +++ b/test/integration/socket_interface_swap.cc @@ -7,23 +7,27 @@ SocketInterfaceSwap::SocketInterfaceSwap(Network::Socket::Type socket_type) Envoy::Network::SocketInterfaceSingleton::clear(); test_socket_interface_loader_ = std::make_unique( std::make_unique( + [write_matcher = write_matcher_](Envoy::Network::TestIoSocketHandle* io_handle) + -> absl::optional { + Api::IoErrorPtr error_override = write_matcher->returnConnectOverride(io_handle); + if (error_override) { + return Api::IoCallUint64Result(0, std::move(error_override)); + } + return absl::nullopt; + }, [write_matcher = write_matcher_]( - Envoy::Network::TestIoSocketHandle* io_handle, const Buffer::RawSlice* slices, - uint64_t size) -> absl::optional { - // TODO(yanavlasov): refactor into separate method after CVE is public. - if (slices == nullptr && size == 0) { - // This is connect override check - Api::IoErrorPtr error_override = write_matcher->returnConnectOverride(io_handle); - if (error_override) { - return Api::IoCallUint64Result(0, std::move(error_override)); - } - } else { - Api::IoErrorPtr error_override = write_matcher->returnOverride(io_handle); - if (error_override) { - return Api::IoCallUint64Result(0, std::move(error_override)); - } + Envoy::Network::TestIoSocketHandle* io_handle, const Buffer::RawSlice*, uint64_t, + Network::Address::InstanceConstSharedPtr& peer_address_override_out) + -> absl::optional { + Api::IoErrorPtr error_override = + write_matcher->returnOverride(io_handle, peer_address_override_out); + if (error_override) { + return Api::IoCallUint64Result(0, std::move(error_override)); } return absl::nullopt; + }, + [write_matcher = write_matcher_](Network::IoHandle::RecvMsgOutput& output) { + write_matcher->readOverride(output); })); } diff --git a/test/integration/socket_interface_swap.h b/test/integration/socket_interface_swap.h index 87127e4c71c2..57e82087de8f 100644 --- a/test/integration/socket_interface_swap.h +++ b/test/integration/socket_interface_swap.h @@ -16,7 +16,9 @@ class SocketInterfaceSwap { struct IoHandleMatcher { explicit IoHandleMatcher(Network::Socket::Type type) : socket_type_(type) {} - Api::IoErrorPtr returnOverride(Envoy::Network::TestIoSocketHandle* io_handle) { + Api::IoErrorPtr + returnOverride(Envoy::Network::TestIoSocketHandle* io_handle, + Network::Address::InstanceConstSharedPtr& peer_address_override_out) { absl::MutexLock lock(&mutex_); if (socket_type_ == io_handle->getSocketType() && error_ && (io_handle->localAddress()->ip()->port() == src_port_ || @@ -28,6 +30,12 @@ class SocketInterfaceSwap { ? Envoy::Network::IoSocketError::getIoSocketEagainError() : Envoy::Network::IoSocketError::create(error_->getSystemErrorCode()); } + + if (orig_dnat_address_ != nullptr && *orig_dnat_address_ == *io_handle->peerAddress()) { + ASSERT(translated_dnat_address_ != nullptr); + peer_address_override_out = translated_dnat_address_; + } + return Api::IoError::none(); } @@ -41,6 +49,19 @@ class SocketInterfaceSwap { return Api::IoError::none(); } + void readOverride(Network::IoHandle::RecvMsgOutput& output) { + absl::MutexLock lock(&mutex_); + if (translated_dnat_address_ != nullptr) { + for (auto& pkt : output.msg_) { + // Reverse DNAT when receiving packets. + if (pkt.peer_address_ != nullptr && *pkt.peer_address_ == *translated_dnat_address_) { + ASSERT(orig_dnat_address_ != nullptr); + pkt.peer_address_ = orig_dnat_address_; + } + } + } + } + // Source port to match. The port specified should be associated with a listener. void setSourcePort(uint32_t port) { absl::WriterMutexLock lock(&mutex_); @@ -72,6 +93,13 @@ class SocketInterfaceSwap { block_connect_ = block; } + void setDnat(Network::Address::InstanceConstSharedPtr orig_address, + Network::Address::InstanceConstSharedPtr translated_address) { + absl::WriterMutexLock lock(&mutex_); + orig_dnat_address_ = orig_address; + translated_dnat_address_ = translated_address; + } + void setResumeWrites(); private: @@ -82,6 +110,8 @@ class SocketInterfaceSwap { Network::TestIoSocketHandle* matched_iohandle_{}; Network::Socket::Type socket_type_; bool block_connect_ ABSL_GUARDED_BY(mutex_) = false; + Network::Address::InstanceConstSharedPtr orig_dnat_address_ ABSL_GUARDED_BY(mutex_); + Network::Address::InstanceConstSharedPtr translated_dnat_address_ ABSL_GUARDED_BY(mutex_); }; explicit SocketInterfaceSwap(Network::Socket::Type socket_type); diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index 11d36cb233ce..9cceb9943183 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -1503,3 +1503,5 @@ DLB PCIE EDNS CNAME +NAT +DNAT