diff --git a/api/envoy/extensions/filters/udp/udp_proxy/v3/BUILD b/api/envoy/extensions/filters/udp/udp_proxy/v3/BUILD index 1c1a6f6b44235..a1775bbe6f513 100644 --- a/api/envoy/extensions/filters/udp/udp_proxy/v3/BUILD +++ b/api/envoy/extensions/filters/udp/udp_proxy/v3/BUILD @@ -6,6 +6,7 @@ licenses(["notice"]) # Apache 2 api_proto_package( deps = [ + "//envoy/annotations:pkg", "//envoy/config/core/v3:pkg", "@com_github_cncf_udpa//udpa/annotations:pkg", ], diff --git a/api/envoy/extensions/filters/udp/udp_proxy/v3/route.proto b/api/envoy/extensions/filters/udp/udp_proxy/v3/route.proto new file mode 100644 index 0000000000000..6af25bdf25fae --- /dev/null +++ b/api/envoy/extensions/filters/udp/udp_proxy/v3/route.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; + +package envoy.extensions.filters.udp.udp_proxy.v3; + +import "envoy/config/core/v3/address.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.udp.udp_proxy.v3"; +option java_outer_classname = "RouteProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: UDP proxy route configuration] +// UDP proxy :ref:`configuration overview `. + +message RouteConfiguration { + // The list of routes that will be matched, in order, against incoming requests. The first route + // that matches will be used. + repeated Route routes = 1; +} + +message Route { + // Route matching parameters. + RouteMatch match = 1 [(validate.rules).message = {required: true}]; + + // Route request to some upstream cluster. + RouteAction route = 2 [(validate.rules).message = {required: true}]; +} + +message RouteMatch { + // The criteria is satisfied if the source IP address of the downstream + // connection is contained in at least one of the specified subnets. If the + // parameter is not specified or the list is empty, the source IP address is + // ignored. + repeated config.core.v3.CidrRange source_prefix_ranges = 1; +} + +message RouteAction { + oneof cluster_specifier { + option (validate.required) = true; + + // Indicates the upstream cluster to which the request should be routed. + string cluster = 1; + } +} diff --git a/api/envoy/extensions/filters/udp/udp_proxy/v3/udp_proxy.proto b/api/envoy/extensions/filters/udp/udp_proxy/v3/udp_proxy.proto index 9d410e28afe3d..d914ee96f563d 100644 --- a/api/envoy/extensions/filters/udp/udp_proxy/v3/udp_proxy.proto +++ b/api/envoy/extensions/filters/udp/udp_proxy/v3/udp_proxy.proto @@ -3,9 +3,11 @@ syntax = "proto3"; package envoy.extensions.filters.udp.udp_proxy.v3; import "envoy/config/core/v3/udp_socket_config.proto"; +import "envoy/extensions/filters/udp/udp_proxy/v3/route.proto"; import "google/protobuf/duration.proto"; +import "envoy/annotations/deprecation.proto"; import "udpa/annotations/status.proto"; import "udpa/annotations/versioning.proto"; import "validate/validate.proto"; @@ -20,7 +22,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#extension: envoy.filters.udp_listener.udp_proxy] // Configuration for the UDP proxy filter. -// [#next-free-field: 7] +// [#next-free-field: 8] message UdpProxyConfig { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.udp.udp_proxy.v2alpha.UdpProxyConfig"; @@ -50,7 +52,14 @@ message UdpProxyConfig { option (validate.required) = true; // The upstream cluster to connect to. - string cluster = 2 [(validate.rules).string = {min_len: 1}]; + string cluster = 2 [ + deprecated = true, + (validate.rules).string = {min_len: 1}, + (envoy.annotations.deprecated_at_minor_version) = "3.0" + ]; + + // The route table for the connection manager is static and is specified in this property. + RouteConfiguration route_config = 7; } // The idle timeout for sessions. Idle is defined as no datagrams between received or sent by diff --git a/generated_api_shadow/envoy/extensions/filters/udp/udp_proxy/v3/BUILD b/generated_api_shadow/envoy/extensions/filters/udp/udp_proxy/v3/BUILD index 1c1a6f6b44235..a1775bbe6f513 100644 --- a/generated_api_shadow/envoy/extensions/filters/udp/udp_proxy/v3/BUILD +++ b/generated_api_shadow/envoy/extensions/filters/udp/udp_proxy/v3/BUILD @@ -6,6 +6,7 @@ licenses(["notice"]) # Apache 2 api_proto_package( deps = [ + "//envoy/annotations:pkg", "//envoy/config/core/v3:pkg", "@com_github_cncf_udpa//udpa/annotations:pkg", ], diff --git a/generated_api_shadow/envoy/extensions/filters/udp/udp_proxy/v3/route.proto b/generated_api_shadow/envoy/extensions/filters/udp/udp_proxy/v3/route.proto new file mode 100644 index 0000000000000..6af25bdf25fae --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/udp/udp_proxy/v3/route.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; + +package envoy.extensions.filters.udp.udp_proxy.v3; + +import "envoy/config/core/v3/address.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.udp.udp_proxy.v3"; +option java_outer_classname = "RouteProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: UDP proxy route configuration] +// UDP proxy :ref:`configuration overview `. + +message RouteConfiguration { + // The list of routes that will be matched, in order, against incoming requests. The first route + // that matches will be used. + repeated Route routes = 1; +} + +message Route { + // Route matching parameters. + RouteMatch match = 1 [(validate.rules).message = {required: true}]; + + // Route request to some upstream cluster. + RouteAction route = 2 [(validate.rules).message = {required: true}]; +} + +message RouteMatch { + // The criteria is satisfied if the source IP address of the downstream + // connection is contained in at least one of the specified subnets. If the + // parameter is not specified or the list is empty, the source IP address is + // ignored. + repeated config.core.v3.CidrRange source_prefix_ranges = 1; +} + +message RouteAction { + oneof cluster_specifier { + option (validate.required) = true; + + // Indicates the upstream cluster to which the request should be routed. + string cluster = 1; + } +} diff --git a/generated_api_shadow/envoy/extensions/filters/udp/udp_proxy/v3/udp_proxy.proto b/generated_api_shadow/envoy/extensions/filters/udp/udp_proxy/v3/udp_proxy.proto index 9d410e28afe3d..d914ee96f563d 100644 --- a/generated_api_shadow/envoy/extensions/filters/udp/udp_proxy/v3/udp_proxy.proto +++ b/generated_api_shadow/envoy/extensions/filters/udp/udp_proxy/v3/udp_proxy.proto @@ -3,9 +3,11 @@ syntax = "proto3"; package envoy.extensions.filters.udp.udp_proxy.v3; import "envoy/config/core/v3/udp_socket_config.proto"; +import "envoy/extensions/filters/udp/udp_proxy/v3/route.proto"; import "google/protobuf/duration.proto"; +import "envoy/annotations/deprecation.proto"; import "udpa/annotations/status.proto"; import "udpa/annotations/versioning.proto"; import "validate/validate.proto"; @@ -20,7 +22,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#extension: envoy.filters.udp_listener.udp_proxy] // Configuration for the UDP proxy filter. -// [#next-free-field: 7] +// [#next-free-field: 8] message UdpProxyConfig { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.udp.udp_proxy.v2alpha.UdpProxyConfig"; @@ -50,7 +52,14 @@ message UdpProxyConfig { option (validate.required) = true; // The upstream cluster to connect to. - string cluster = 2 [(validate.rules).string = {min_len: 1}]; + string cluster = 2 [ + deprecated = true, + (validate.rules).string = {min_len: 1}, + (envoy.annotations.deprecated_at_minor_version) = "3.0" + ]; + + // The route table for the connection manager is static and is specified in this property. + RouteConfiguration route_config = 7; } // The idle timeout for sessions. Idle is defined as no datagrams between received or sent by diff --git a/source/extensions/filters/udp/udp_proxy/BUILD b/source/extensions/filters/udp/udp_proxy/BUILD index 331e28323819e..95872527d5b99 100644 --- a/source/extensions/filters/udp/udp_proxy/BUILD +++ b/source/extensions/filters/udp/udp_proxy/BUILD @@ -33,10 +33,12 @@ envoy_cc_library( "//envoy/network:listener_interface", "//envoy/upstream:cluster_manager_interface", "//source/common/api:os_sys_calls_lib", + "//source/common/common:empty_string", "//source/common/network:socket_lib", "//source/common/network:socket_option_factory_lib", "//source/common/network:utility_lib", "//source/common/upstream:load_balancer_lib", + "//source/extensions/filters/udp/udp_proxy/router:router_lib", "@envoy_api//envoy/extensions/filters/udp/udp_proxy/v3:pkg_cc_proto", ], ) diff --git a/source/extensions/filters/udp/udp_proxy/router/BUILD b/source/extensions/filters/udp/udp_proxy/router/BUILD new file mode 100644 index 0000000000000..a58ae7182b198 --- /dev/null +++ b/source/extensions/filters/udp/udp_proxy/router/BUILD @@ -0,0 +1,26 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "router_interface", + hdrs = ["router.h"], +) + +envoy_cc_library( + name = "router_lib", + srcs = ["router_impl.cc"], + hdrs = ["router_impl.h"], + deps = [ + ":router_interface", + "//source/common/network:cidr_range_lib", + "//source/common/network:lc_trie_lib", + "@envoy_api//envoy/extensions/filters/udp/udp_proxy/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/filters/udp/udp_proxy/router/router.h b/source/extensions/filters/udp/udp_proxy/router/router.h new file mode 100644 index 0000000000000..aab65a6f1f436 --- /dev/null +++ b/source/extensions/filters/udp/udp_proxy/router/router.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include + +#include "envoy/common/pure.h" +#include "envoy/network/address.h" + +namespace Envoy { +namespace Extensions { +namespace UdpFilters { +namespace UdpProxy { +namespace Router { + +/** + * RouteEntry is an individual resolved route entry. + */ +class RouteEntry { +public: + virtual ~RouteEntry() = default; + + /** + * @return const std::string& the upstream cluster that owns the route. + */ + virtual const std::string& clusterName() const PURE; +}; + +using RouteEntryConstSharedPtr = std::shared_ptr; + +/** + * Route holds the RouteEntry for a request. + */ +class Route { +public: + virtual ~Route() = default; + + /** + * @return the route entry or nullptr if there is no matching route for the request. + */ + virtual const RouteEntry* routeEntry() const PURE; +}; + +using RouteConstSharedPtr = std::shared_ptr; + +/** + * The router configuration. + */ +class Config { +public: + virtual ~Config() = default; + + virtual RouteConstSharedPtr route(Network::Address::InstanceConstSharedPtr address) const PURE; + virtual const std::vector& entries() const PURE; +}; + +using ConfigConstSharedPtr = std::shared_ptr; + +} // namespace Router +} // namespace UdpProxy +} // namespace UdpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/udp/udp_proxy/router/router_impl.cc b/source/extensions/filters/udp/udp_proxy/router/router_impl.cc new file mode 100644 index 0000000000000..ca934418e1c48 --- /dev/null +++ b/source/extensions/filters/udp/udp_proxy/router/router_impl.cc @@ -0,0 +1,90 @@ +#include "source/extensions/filters/udp/udp_proxy/router/router_impl.h" + +#include "absl/container/flat_hash_set.h" + +namespace Envoy { +namespace Extensions { +namespace UdpFilters { +namespace UdpProxy { +namespace Router { + +ClusterRouteEntry::ClusterRouteEntry( + const envoy::extensions::filters::udp::udp_proxy::v3::Route& route) + : cluster_name_(route.route().cluster()) {} + +ClusterRouteEntry::ClusterRouteEntry(const std::string& cluster) : cluster_name_(cluster) {} + +ConfigImpl::ConfigImpl(const envoy::extensions::filters::udp::udp_proxy::v3::UdpProxyConfig& config) + : cluster_(std::make_shared(config.cluster())), + source_ips_trie_(buildRouteTrie(config.route_config())), + entries_(buildEntryList(config.cluster(), config.route_config())) {} + +RouteConstSharedPtr ConfigImpl::route(Network::Address::InstanceConstSharedPtr address) const { + if (!cluster_->routeEntry()->clusterName().empty()) { + return cluster_; + } + + const auto& data = source_ips_trie_.getData(address); + if (!data.empty()) { + ASSERT(data.size() == 1); + return data.back(); + } + + return nullptr; +} + +ConfigImpl::SourceIPsTrie ConfigImpl::buildRouteTrie(const RouteConfiguration& config) { + std::vector>> + source_ips_list; + source_ips_list.reserve(config.routes().size()); + + auto convertAddress = [](const auto& prefix_ranges) -> std::vector { + std::vector ips; + ips.reserve(prefix_ranges.size()); + for (const auto& ip : prefix_ranges) { + const auto& cidr_range = Network::Address::CidrRange::create(ip); + ips.push_back(cidr_range); + } + return ips; + }; + + for (auto& route : config.routes()) { + auto ranges = route.match().source_prefix_ranges(); + auto route_entry = std::make_shared(route); + + source_ips_list.push_back(make_pair(route_entry, convertAddress(ranges))); + } + + return {source_ips_list, true}; +} + +std::vector ConfigImpl::buildEntryList(const std::string& cluster, + const RouteConfiguration& config) { + auto set = absl::flat_hash_set(); + + if (!cluster.empty()) { + set.emplace(std::make_shared(cluster)); + } + + for (const auto& route : config.routes()) { + auto route_entry = std::make_shared(route); + auto cluster_name = route_entry->routeEntry()->clusterName(); + if (!cluster_name.empty()) { + set.emplace(std::make_shared(cluster_name)); + } + } + + auto list = std::vector(); + list.reserve(set.size()); + for (const auto& entry : set) { + list.push_back(entry); + } + + return list; +} + +} // namespace Router +} // namespace UdpProxy +} // namespace UdpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/udp/udp_proxy/router/router_impl.h b/source/extensions/filters/udp/udp_proxy/router/router_impl.h new file mode 100644 index 0000000000000..22ea69d28d48c --- /dev/null +++ b/source/extensions/filters/udp/udp_proxy/router/router_impl.h @@ -0,0 +1,57 @@ +#pragma once + +#include "envoy/extensions/filters/udp/udp_proxy/v3/route.pb.h" +#include "envoy/extensions/filters/udp/udp_proxy/v3/udp_proxy.pb.h" + +#include "source/common/network/lc_trie.h" +#include "source/extensions/filters/udp/udp_proxy/router/router.h" + +namespace Envoy { +namespace Extensions { +namespace UdpFilters { +namespace UdpProxy { +namespace Router { + +class ClusterRouteEntry : public RouteEntry, public Route { +public: + ClusterRouteEntry(const envoy::extensions::filters::udp::udp_proxy::v3::Route& route); + ClusterRouteEntry(const std::string& cluster); + ~ClusterRouteEntry() override = default; + + // Router::RouteEntry + const std::string& clusterName() const override { return cluster_name_; } + + // Router::Route + const RouteEntry* routeEntry() const override { return this; } + +private: + const std::string cluster_name_; +}; + +class ConfigImpl : public Config { +public: + ConfigImpl(const envoy::extensions::filters::udp::udp_proxy::v3::UdpProxyConfig& config); + ~ConfigImpl() override = default; + + // Router::Config + RouteConstSharedPtr route(Network::Address::InstanceConstSharedPtr address) const override; + const std::vector& entries() const override { return entries_; } + +private: + using SourceIPsTrie = Network::LcTrie::LcTrie; + using RouteConfiguration = envoy::extensions::filters::udp::udp_proxy::v3::RouteConfiguration; + + RouteConstSharedPtr cluster_; + const SourceIPsTrie source_ips_trie_; + const std::vector entries_; + + SourceIPsTrie buildRouteTrie(const RouteConfiguration& config); + std::vector buildEntryList(const std::string& cluster, + const RouteConfiguration& config); +}; + +} // namespace Router +} // namespace UdpProxy +} // namespace UdpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/udp/udp_proxy/udp_proxy_filter.cc b/source/extensions/filters/udp/udp_proxy/udp_proxy_filter.cc index 4ee826eeb3f77..2858063afa4a4 100644 --- a/source/extensions/filters/udp/udp_proxy/udp_proxy_filter.cc +++ b/source/extensions/filters/udp/udp_proxy/udp_proxy_filter.cc @@ -14,39 +14,36 @@ UdpProxyFilter::UdpProxyFilter(Network::UdpReadFilterCallbacks& callbacks, : UdpListenerReadFilter(callbacks), config_(config), cluster_update_callbacks_( config->clusterManager().addThreadLocalClusterUpdateCallbacks(*this)) { - Upstream::ThreadLocalCluster* cluster = - config->clusterManager().getThreadLocalCluster(config->cluster()); - if (cluster != nullptr) { - onClusterAddOrUpdate(*cluster); + for (const auto& entry : config_->entries()) { + Upstream::ThreadLocalCluster* cluster = + config->clusterManager().getThreadLocalCluster(entry->clusterName()); + if (cluster != nullptr) { + onClusterAddOrUpdate(*cluster); + } } } void UdpProxyFilter::onClusterAddOrUpdate(Upstream::ThreadLocalCluster& cluster) { - if (cluster.info()->name() != config_->cluster()) { - return; - } - - ENVOY_LOG(debug, "udp proxy: attaching to cluster {}", cluster.info()->name()); - ASSERT(cluster_info_ == absl::nullopt || &cluster_info_.value().cluster_ != &cluster); - cluster_info_.emplace(*this, cluster); + auto cluster_name = cluster.info()->name(); + ENVOY_LOG(debug, "udp proxy: attaching to cluster {}", cluster_name); + ASSERT((!cluster_infos_.contains(cluster_name)) || + &cluster_infos_[cluster_name]->cluster_ != &cluster); + cluster_infos_.emplace(cluster_name, std::make_shared(*this, cluster)); } void UdpProxyFilter::onClusterRemoval(const std::string& cluster) { - if (cluster != config_->cluster()) { - return; - } - ENVOY_LOG(debug, "udp proxy: detaching from cluster {}", cluster); - cluster_info_.reset(); + cluster_infos_.erase(cluster); } void UdpProxyFilter::onData(Network::UdpRecvData& data) { - if (!cluster_info_.has_value()) { + auto route = config_->cluster(data.addresses_.peer_); + if (!cluster_infos_.contains(route)) { config_->stats().downstream_sess_no_route_.inc(); return; } - cluster_info_.value().onData(data); + cluster_infos_[route]->onData(data); } void UdpProxyFilter::onReceiveError(Api::IoError::IoErrorCode) { diff --git a/source/extensions/filters/udp/udp_proxy/udp_proxy_filter.h b/source/extensions/filters/udp/udp_proxy/udp_proxy_filter.h index fd20e7025dd2f..fe06faf3e2fd1 100644 --- a/source/extensions/filters/udp/udp_proxy/udp_proxy_filter.h +++ b/source/extensions/filters/udp/udp_proxy/udp_proxy_filter.h @@ -7,13 +7,16 @@ #include "envoy/upstream/cluster_manager.h" #include "source/common/api/os_sys_calls_impl.h" +#include "source/common/common/empty_string.h" #include "source/common/network/socket_impl.h" #include "source/common/network/socket_interface.h" #include "source/common/network/utility.h" #include "source/common/protobuf/utility.h" #include "source/common/upstream/load_balancer_impl.h" #include "source/extensions/filters/udp/udp_proxy/hash_policy_impl.h" +#include "source/extensions/filters/udp/udp_proxy/router/router_impl.h" +#include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" // TODO(mattklein123): UDP session access logging. @@ -67,7 +70,8 @@ class UdpProxyFilterConfig { UdpProxyFilterConfig(Upstream::ClusterManager& cluster_manager, TimeSource& time_source, Stats::Scope& root_scope, const envoy::extensions::filters::udp::udp_proxy::v3::UdpProxyConfig& config) - : cluster_manager_(cluster_manager), time_source_(time_source), cluster_(config.cluster()), + : cluster_manager_(cluster_manager), time_source_(time_source), + router_config_(std::make_shared(config)), session_timeout_(PROTOBUF_GET_MS_OR_DEFAULT(config, idle_timeout, 60 * 1000)), use_original_src_ip_(config.use_original_src_ip()), stats_(generateStats(config.stat_prefix(), root_scope)), @@ -83,7 +87,17 @@ class UdpProxyFilterConfig { } } - const std::string& cluster() const { return cluster_; } + const std::string& cluster(Network::Address::InstanceConstSharedPtr address) const { + auto route = router_config_->route(address); + if (!route) { + return EMPTY_STRING; + } + + return route->routeEntry()->clusterName(); + } + const std::vector& entries() const { + return router_config_->entries(); + } Upstream::ClusterManager& clusterManager() const { return cluster_manager_; } std::chrono::milliseconds sessionTimeout() const { return session_timeout_; } bool usingOriginalSrcIp() const { return use_original_src_ip_; } @@ -104,7 +118,7 @@ class UdpProxyFilterConfig { Upstream::ClusterManager& cluster_manager_; TimeSource& time_source_; - const std::string cluster_; + Router::ConfigConstSharedPtr router_config_; const std::chrono::milliseconds session_timeout_; const bool use_original_src_ip_; std::unique_ptr hash_policy_; @@ -280,10 +294,7 @@ class UdpProxyFilter : public Network::UdpListenerReadFilter, const UdpProxyFilterConfigSharedPtr config_; const Upstream::ClusterUpdateCallbacksHandlePtr cluster_update_callbacks_; - // Right now we support a single cluster to route to. It is highly likely in the future that - // we will support additional routing options either using filter chain matching, weighting, - // etc. - absl::optional cluster_info_; + absl::flat_hash_map> cluster_infos_; }; } // namespace UdpProxy diff --git a/test.yaml b/test.yaml new file mode 100644 index 0000000000000..a96cdf43788f4 --- /dev/null +++ b/test.yaml @@ -0,0 +1,43 @@ +static_resources: + listeners: + - name: listener_0 + address: + socket_address: + protocol: UDP + address: 0.0.0.0 + port_value: 10000 + listener_filters: + - name: envoy.filters.udp_listener.udp_proxy + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.UdpProxyConfig + stat_prefix: service + # cluster: service_udp + route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + - address_prefix: 0::0 + route: + cluster: service_udp + + clusters: + - name: service_udp + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: service_udp + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 5005 + +admin: + access_log_path: "/dev/null" + address: + socket_address: + address: 0.0.0.0 + port_value: 10001 diff --git a/test/extensions/filters/udp/udp_proxy/BUILD b/test/extensions/filters/udp/udp_proxy/BUILD index b1619e62a6b1d..3cf1ef2c500b8 100644 --- a/test/extensions/filters/udp/udp_proxy/BUILD +++ b/test/extensions/filters/udp/udp_proxy/BUILD @@ -42,6 +42,19 @@ envoy_extension_cc_test( ], ) +envoy_extension_cc_test( + name = "router_impl_test", + srcs = ["router_impl_test.cc"], + extension_names = ["envoy.filters.udp_listener.udp_proxy"], + deps = [ + "//source/common/network:address_lib", + "//source/common/network:utility_lib", + "//source/extensions/filters/udp/udp_proxy/router:router_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/filters/udp/udp_proxy/v3:pkg_cc_proto", + ], +) + envoy_extension_cc_test( name = "udp_proxy_integration_test", srcs = ["udp_proxy_integration_test.cc"], diff --git a/test/extensions/filters/udp/udp_proxy/router_impl_test.cc b/test/extensions/filters/udp/udp_proxy/router_impl_test.cc new file mode 100644 index 0000000000000..a5aa2f2416d2d --- /dev/null +++ b/test/extensions/filters/udp/udp_proxy/router_impl_test.cc @@ -0,0 +1,89 @@ +#include "envoy/extensions/filters/udp/udp_proxy/v3/udp_proxy.pb.h" +#include "envoy/extensions/filters/udp/udp_proxy/v3/udp_proxy.pb.validate.h" + +#include "source/common/network/utility.h" +#include "source/common/protobuf/protobuf.h" +#include "source/extensions/filters/udp/udp_proxy/router/router_impl.h" + +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace UdpFilters { +namespace UdpProxy { +namespace Router { +namespace { + +envoy::extensions::filters::udp::udp_proxy::v3::UdpProxyConfig +parseUdpProxyConfigFromYaml(const std::string& yaml) { + envoy::extensions::filters::udp::udp_proxy::v3::UdpProxyConfig config; + TestUtility::loadFromYaml(yaml, config); + TestUtility::validate(config); + return config; +} + +Network::Address::InstanceConstSharedPtr parseAddress(const std::string& address) { + return Network::Utility::parseInternetAddressAndPort(address); +} + +} // namespace + +// Basic UDP proxy flow to a single cluster. +TEST(RouterImplTest, RouteToSingleCluster) { + const std::string yaml = R"EOF( +stat_prefix: foo +cluster: udp_service + )EOF"; + + auto config = parseUdpProxyConfigFromYaml(yaml); + auto router = std::make_shared(config); + + EXPECT_EQ("udp_service", + router->route(parseAddress("10.0.0.1:10000"))->routeEntry()->clusterName()); + EXPECT_EQ("udp_service", + router->route(parseAddress("172.16.0.1:10000"))->routeEntry()->clusterName()); + EXPECT_EQ("udp_service", + router->route(parseAddress("192.168.0.1:10000"))->routeEntry()->clusterName()); + EXPECT_EQ("udp_service", + router->route(parseAddress("[fc00::1]:10000"))->routeEntry()->clusterName()); +} + +// Route UDP packets to clusters based on source_prefix_range. +TEST(RouterImplTest, RouteBySourcePrefix) { + const std::string yaml = R"EOF( +stat_prefix: foo +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 10.0.0.0 + prefix_len: 8 + route: + cluster: udp_service + - match: + source_prefix_ranges: + - address_prefix: 172.16.0.0 + prefix_len: 16 + route: + cluster: udp_service2 + )EOF"; + + auto config = parseUdpProxyConfigFromYaml(yaml); + auto router = std::make_shared(config); + + EXPECT_EQ("udp_service", + router->route(parseAddress("10.0.0.1:10000"))->routeEntry()->clusterName()); + EXPECT_EQ("udp_service2", + router->route(parseAddress("172.16.0.1:10000"))->routeEntry()->clusterName()); + EXPECT_EQ(nullptr, router->route(parseAddress("192.168.0.1:10000"))); + EXPECT_EQ(nullptr, router->route(parseAddress("[fc00::1]:10000"))); +} + +} // namespace Router +} // namespace UdpProxy +} // namespace UdpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/udp/udp_proxy/udp_proxy_filter_test.cc b/test/extensions/filters/udp/udp_proxy/udp_proxy_filter_test.cc index f59a51f7a833c..05bd1fcb55905 100644 --- a/test/extensions/filters/udp/udp_proxy/udp_proxy_filter_test.cc +++ b/test/extensions/filters/udp/udp_proxy/udp_proxy_filter_test.cc @@ -242,15 +242,25 @@ class UdpProxyFilterTest : public testing::Test { EXPECT_EQ(ipv6_expect, session.sock_opts_[ipv6_option.level()][ipv6_option.option()]); } - void - ensureIpTransparentSocketOptions(const Network::Address::InstanceConstSharedPtr& upstream_address, - const std::string& local_address, int ipv4_expect, - int ipv6_expect) { + virtual void setupDefault() { setup(R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster use_original_src_ip: true )EOF"); + } + + void + ensureIpTransparentSocketOptions(const Network::Address::InstanceConstSharedPtr& upstream_address, + const std::string& local_address, int ipv4_expect, + int ipv6_expect) { + setupDefault(); expectSessionCreate(upstream_address); test_sessions_[0].expectSetIpTransparentSocketOption(); @@ -309,6 +319,20 @@ class UdpProxyFilterIpv6Test : public UdpProxyFilterTest { .WillRepeatedly(Return(upstream_address_v6_)); } + void setupDefault() override { + setup(R"EOF( +stat_prefix: foo +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0::0 + route: + cluster: fake_cluster +use_original_src_ip: true + )EOF"); + } + const Network::Address::InstanceConstSharedPtr upstream_address_v6_; inline static const std::string upstream_ipv6_address_ = "[2001:db8:85a3::8a2e:370:7334]:443"; inline static const std::string peer_ipv6_address_ = "[2001:db8:85a3::9a2e:370:7334]:1000"; @@ -343,6 +367,66 @@ TEST_F(UdpProxyFilterTest, BasicFlow) { setup(R"EOF( stat_prefix: foo +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster +upstream_socket_config: + prefer_gro: false + )EOF", + true, false); + + expectSessionCreate(upstream_address_); + test_sessions_[0].expectWriteToUpstream("hello"); + recvDataFromDownstream("10.0.0.1:1000", "10.0.0.2:80", "hello"); + EXPECT_EQ(1, config_->stats().downstream_sess_total_.value()); + EXPECT_EQ(1, config_->stats().downstream_sess_active_.value()); + checkTransferStats(5 /*rx_bytes*/, 1 /*rx_datagrams*/, 0 /*tx_bytes*/, 0 /*tx_datagrams*/); + test_sessions_[0].recvDataFromUpstream("world"); + checkTransferStats(5 /*rx_bytes*/, 1 /*rx_datagrams*/, 5 /*tx_bytes*/, 1 /*tx_datagrams*/); + + test_sessions_[0].expectWriteToUpstream("hello2"); + test_sessions_[0].expectWriteToUpstream("hello3"); + recvDataFromDownstream("10.0.0.1:1000", "10.0.0.2:80", "hello2"); + checkTransferStats(11 /*rx_bytes*/, 2 /*rx_datagrams*/, 5 /*tx_bytes*/, 1 /*tx_datagrams*/); + recvDataFromDownstream("10.0.0.1:1000", "10.0.0.2:80", "hello3"); + checkTransferStats(17 /*rx_bytes*/, 3 /*rx_datagrams*/, 5 /*tx_bytes*/, 1 /*tx_datagrams*/); + + test_sessions_[0].recvDataFromUpstream("world2"); + checkTransferStats(17 /*rx_bytes*/, 3 /*rx_datagrams*/, 11 /*tx_bytes*/, 2 /*tx_datagrams*/); + test_sessions_[0].recvDataFromUpstream("world3"); + checkTransferStats(17 /*rx_bytes*/, 3 /*rx_datagrams*/, 17 /*tx_bytes*/, 3 /*tx_datagrams*/); +} + +// No route. +TEST_F(UdpProxyFilterTest, NoRoute) { + InSequence s; + + setup(R"EOF( +stat_prefix: foo +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 10.0.0.3 + prefix_len: 32 + route: + cluster: fake_cluster + )EOF"); + + recvDataFromDownstream("10.0.0.1:1000", "10.0.0.2:80", "hello"); + EXPECT_EQ(1, config_->stats().downstream_sess_no_route_.value()); +} + +// Single cluster without route config. +TEST_F(UdpProxyFilterTest, SingleCluster) { + InSequence s; + + setup(R"EOF( +stat_prefix: foo cluster: fake_cluster upstream_socket_config: prefer_gro: false @@ -377,7 +461,13 @@ TEST_F(UdpProxyFilterTest, IdleTimeout) { setup(R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster )EOF"); expectSessionCreate(upstream_address_); @@ -403,7 +493,13 @@ TEST_F(UdpProxyFilterTest, SendReceiveErrorHandling) { setup(R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster )EOF"); filter_->onReceiveError(Api::IoError::IoErrorCode::UnknownError); @@ -448,7 +544,13 @@ TEST_F(UdpProxyFilterTest, NoUpstreamHost) { setup(R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster )EOF"); EXPECT_CALL(cluster_manager_.thread_local_cluster_.lb_, chooseHost(_)).WillOnce(Return(nullptr)); @@ -463,7 +565,13 @@ TEST_F(UdpProxyFilterTest, NoUpstreamClusterAtCreation) { setup(R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster )EOF", false); @@ -477,7 +585,13 @@ TEST_F(UdpProxyFilterTest, ClusterDynamicAddAndRemoval) { setup(R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster )EOF", false); @@ -518,7 +632,13 @@ TEST_F(UdpProxyFilterTest, MaxSessionsCircuitBreaker) { setup(R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster )EOF"); // Allow only a single session. @@ -555,7 +675,13 @@ TEST_F(UdpProxyFilterTest, RemoveHostSessions) { setup(R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster )EOF"); expectSessionCreate(upstream_address_); @@ -583,7 +709,13 @@ TEST_F(UdpProxyFilterTest, HostUnhealthyPickSameHost) { setup(R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster )EOF"); expectSessionCreate(upstream_address_); @@ -605,7 +737,13 @@ TEST_F(UdpProxyFilterTest, HostUnhealthyPickDifferentHost) { setup(R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster )EOF"); expectSessionCreate(upstream_address_); @@ -663,7 +801,14 @@ TEST_F(UdpProxyFilterIpv4Ipv6Test, NoSocketOptionIfUseOriginalSrcIpIsNotSet) { setup(R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + - address_prefix: 0::0 + route: + cluster: fake_cluster use_original_src_ip: false )EOF"); @@ -681,7 +826,14 @@ TEST_F(UdpProxyFilterIpv4Ipv6Test, NoSocketOptionIfUseOriginalSrcIpIsNotMentione setup(R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + - address_prefix: 0::0 + route: + cluster: fake_cluster )EOF"); ensureNoIpTransparentSocketOptions(); @@ -696,7 +848,13 @@ TEST_F(UdpProxyFilterTest, ExitIpTransparentNoPlatformSupport) { auto config = R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster use_original_src_ip: true )EOF"; @@ -712,7 +870,13 @@ TEST_F(UdpProxyFilterTest, HashPolicyWithSourceIp) { setup(R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster hash_policies: - source_ip: true )EOF"); @@ -725,7 +889,13 @@ TEST_F(UdpProxyFilterTest, ValidateHashPolicyWithSourceIp) { InSequence s; auto config = R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster hash_policies: - source_ip: false )EOF"; @@ -740,7 +910,13 @@ TEST_F(UdpProxyFilterTest, NoHashPolicy) { setup(R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster )EOF"); EXPECT_EQ(nullptr, config_->hashPolicy()); @@ -752,7 +928,13 @@ TEST_F(UdpProxyFilterTest, HashWithSourceIp) { setup(R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster hash_policies: - source_ip: true )EOF"); @@ -779,7 +961,13 @@ TEST_F(UdpProxyFilterTest, NullHashWithoutHashPolicy) { setup(R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster )EOF"); auto host = createHost(upstream_address_); @@ -802,7 +990,13 @@ TEST_F(UdpProxyFilterTest, HashPolicyWithKey) { setup(R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster hash_policies: - key: "key" )EOF"); @@ -815,7 +1009,13 @@ TEST_F(UdpProxyFilterTest, ValidateHashPolicyWithKey) { InSequence s; auto config = R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster hash_policies: - key: "" )EOF"; @@ -830,7 +1030,13 @@ TEST_F(UdpProxyFilterTest, HashWithKey) { setup(R"EOF( stat_prefix: foo -cluster: fake_cluster +route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + route: + cluster: fake_cluster hash_policies: - key: "key" )EOF"); diff --git a/test/extensions/filters/udp/udp_proxy/udp_proxy_integration_test.cc b/test/extensions/filters/udp/udp_proxy/udp_proxy_integration_test.cc index a2a3231e37c0b..44fecad9153ef 100644 --- a/test/extensions/filters/udp/udp_proxy/udp_proxy_integration_test.cc +++ b/test/extensions/filters/udp/udp_proxy/udp_proxy_integration_test.cc @@ -55,7 +55,14 @@ name: udp_proxy typed_config: '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.UdpProxyConfig stat_prefix: foo - cluster: cluster_0 + route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + - address_prefix: 0::0 + route: + cluster: cluster_0 upstream_socket_config: max_rx_datagram_size: {} )EOF", @@ -66,7 +73,14 @@ name: udp_proxy typed_config: '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.UdpProxyConfig stat_prefix: foo - cluster: cluster_0 + route_config: + routes: + - match: + source_prefix_ranges: + - address_prefix: 0.0.0.0 + - address_prefix: 0::0 + route: + cluster: cluster_0 )EOF"); }