diff --git a/CODEOWNERS b/CODEOWNERS index b6a061193d2e2..02c5e788425cf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -402,6 +402,7 @@ extensions/upstreams/tcp @ggreenway @mattklein123 /*/extensions/filters/http/dynamic_modules @mattklein123 @mathetake @wbpcode @agrawroh /*/extensions/filters/network/dynamic_modules @agrawroh @mathetake @wbpcode /*/extensions/filters/listener/dynamic_modules @agrawroh @mathetake @wbpcode +/*/extensions/filters/udp/dynamic_modules @agrawroh @mathetake @wbpcode # Linux network namespace override /*/extensions/local_address_selectors/filter_state_override @tonya11en @kyessenov diff --git a/api/BUILD b/api/BUILD index a89d9fa5e88c9..7518538d869a6 100644 --- a/api/BUILD +++ b/api/BUILD @@ -279,6 +279,7 @@ proto_library( "//envoy/extensions/filters/network/wasm/v3:pkg", "//envoy/extensions/filters/network/zookeeper_proxy/v3:pkg", "//envoy/extensions/filters/udp/dns_filter/v3:pkg", + "//envoy/extensions/filters/udp/dynamic_modules/v3:pkg", "//envoy/extensions/filters/udp/udp_proxy/session/dynamic_forward_proxy/v3:pkg", "//envoy/extensions/filters/udp/udp_proxy/session/http_capsule/v3:pkg", "//envoy/extensions/filters/udp/udp_proxy/v3:pkg", diff --git a/api/envoy/extensions/filters/udp/dynamic_modules/v3/BUILD b/api/envoy/extensions/filters/udp/dynamic_modules/v3/BUILD new file mode 100644 index 0000000000000..cc519056dc158 --- /dev/null +++ b/api/envoy/extensions/filters/udp/dynamic_modules/v3/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/extensions/dynamic_modules/v3:pkg", + "@com_github_cncf_xds//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/filters/udp/dynamic_modules/v3/dynamic_modules.proto b/api/envoy/extensions/filters/udp/dynamic_modules/v3/dynamic_modules.proto new file mode 100644 index 0000000000000..7f0defc717bfc --- /dev/null +++ b/api/envoy/extensions/filters/udp/dynamic_modules/v3/dynamic_modules.proto @@ -0,0 +1,65 @@ +syntax = "proto3"; + +package envoy.extensions.filters.udp.dynamic_modules.v3; + +import "envoy/extensions/dynamic_modules/v3/dynamic_modules.proto"; + +import "google/protobuf/any.proto"; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.udp.dynamic_modules.v3"; +option java_outer_classname = "DynamicModulesProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/udp/dynamic_modules/v3;dynamic_modulesv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Dynamic Modules UDP Listener Filter] +// [#extension: envoy.filters.udp_listener.dynamic_modules] + +// Configuration for the Dynamic Modules UDP listener filter. This filter allows loading shared object +// files that can be loaded via ``dlopen`` to extend the UDP listener filter chain. +// +// A module can be loaded by multiple UDP listener filters; the module is loaded only once and shared +// across multiple filters. +message DynamicModuleUdpListenerFilter { + // Specifies the shared-object level configuration. + envoy.extensions.dynamic_modules.v3.DynamicModuleConfig dynamic_module_config = 1; + + // The name for this filter configuration. + // + // This can be used to distinguish between different filter implementations inside a dynamic + // module. For example, a module can have completely different filter implementations. When Envoy + // receives this configuration, it passes the ``filter_name`` to the dynamic module's UDP listener + // filter config init function together with the ``filter_config``. That way a module can decide + // which in-module filter implementation to use based on the name at load time. + string filter_name = 2; + + // The configuration for the filter chosen by ``filter_name``. + // + // This is passed to the module's UDP listener filter initialization function. Together with the + // ``filter_name``, the module can decide which in-module filter implementation to use and + // fine-tune the behavior of the filter. + // + // For example, if a module has two filter implementations, one for echo and one for rate + // limiting, ``filter_name`` is used to choose either echo or rate limiting. The + // ``filter_config`` can be used to configure the echo behavior or the rate limiting parameters. + // + // ``google.protobuf.Struct`` is serialized as JSON before passing it to the module. + // ``google.protobuf.BytesValue`` and ``google.protobuf.StringValue`` are passed directly + // without the wrapper. + // + // .. code-block:: yaml + // + // # Passing a string value + // filter_config: + // "@type": "type.googleapis.com/google.protobuf.StringValue" + // value: hello + // + // # Passing raw bytes + // filter_config: + // "@type": "type.googleapis.com/google.protobuf.BytesValue" + // value: aGVsbG8= # echo -n "hello" | base64 + // + google.protobuf.Any filter_config = 3; +} diff --git a/api/versioning/BUILD b/api/versioning/BUILD index 052c1e913d06d..d66c43e580633 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -218,6 +218,7 @@ proto_library( "//envoy/extensions/filters/network/wasm/v3:pkg", "//envoy/extensions/filters/network/zookeeper_proxy/v3:pkg", "//envoy/extensions/filters/udp/dns_filter/v3:pkg", + "//envoy/extensions/filters/udp/dynamic_modules/v3:pkg", "//envoy/extensions/filters/udp/udp_proxy/session/dynamic_forward_proxy/v3:pkg", "//envoy/extensions/filters/udp/udp_proxy/session/http_capsule/v3:pkg", "//envoy/extensions/filters/udp/udp_proxy/v3:pkg", diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 05c122ec6eba2..199f6728f6043 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -322,6 +322,10 @@ new_features: change: | Added :ref:`listener filter ` support for dynamic modules, enabling connection inspection and protocol detection before connection establishment. +- area: dynamic modules + change: | + Added :ref:`UDP listener filter ` + support for dynamic modules, enabling UDP datagram processing with dynamic modules. - area: http filter change: | Added :ref:`transform http filter ` to modify request and response bodies in any diff --git a/docs/root/configuration/listeners/udp_filters/dynamic_modules.rst b/docs/root/configuration/listeners/udp_filters/dynamic_modules.rst new file mode 100644 index 0000000000000..7a031f8857191 --- /dev/null +++ b/docs/root/configuration/listeners/udp_filters/dynamic_modules.rst @@ -0,0 +1,32 @@ +.. _config_udp_listener_filters_dynamic_modules: + +Dynamic Modules +=============== + +* :ref:`v3 API reference ` + +The Dynamic Modules UDP listener filter allows you to write UDP listener filters in a dynamic module. +This can be used to implement custom UDP handling logic, such as: + +* Inspecting UDP datagrams. +* Modifying UDP datagrams. +* Dropping UDP datagrams. +* Sending responses directly from the filter (e.g., for DNS). + +The filter is configured using the :ref:`DynamicModuleUdpListenerFilter ` message. + +.. code-block:: yaml + + listener_filters: + - name: envoy.filters.udp_listener.dynamic_modules + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.udp.dynamic_modules.v3.DynamicModuleUdpListenerFilter + dynamic_module_config: + name: my_module + entry_point: envoy_dynamic_module_on_program_init + filter_name: my_udp_filter + filter_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: "my_config" + +For more details on dynamic modules, see the :ref:`architecture overview `. diff --git a/docs/root/configuration/listeners/udp_filters/udp_filters.rst b/docs/root/configuration/listeners/udp_filters/udp_filters.rst index 0a9a2017987de..f627ff527b28b 100644 --- a/docs/root/configuration/listeners/udp_filters/udp_filters.rst +++ b/docs/root/configuration/listeners/udp_filters/udp_filters.rst @@ -10,4 +10,4 @@ Envoy has the following builtin UDP listener filters. udp_proxy dns_filter - + dynamic_modules diff --git a/docs/root/intro/arch_overview/advanced/dynamic_modules.rst b/docs/root/intro/arch_overview/advanced/dynamic_modules.rst index 828899308abc3..c1db5e3761aaf 100644 --- a/docs/root/intro/arch_overview/advanced/dynamic_modules.rst +++ b/docs/root/intro/arch_overview/advanced/dynamic_modules.rst @@ -22,6 +22,7 @@ Future development may include support for other languages. Currently, dynamic modules are supported at the following extension points: * As a :ref:`listener filter `. +* As a :ref:`UDP listener filter `. * As a :ref:`network filter `. * As an :ref:`HTTP filter `. diff --git a/source/extensions/dynamic_modules/abi.h b/source/extensions/dynamic_modules/abi.h index 35f5e01572fa2..0473a90cf4d7e 100644 --- a/source/extensions/dynamic_modules/abi.h +++ b/source/extensions/dynamic_modules/abi.h @@ -3280,6 +3280,163 @@ bool envoy_dynamic_module_callback_listener_filter_set_dynamic_metadata_string( size_t envoy_dynamic_module_callback_listener_filter_max_read_bytes( envoy_dynamic_module_type_listener_filter_envoy_ptr filter_envoy_ptr); +// ----------------------------------------------------------------------------- +// UDP Listener Filter +// ----------------------------------------------------------------------------- + +/** + * envoy_dynamic_module_type_udp_listener_filter_config_envoy_ptr is a raw pointer to + * the DynamicModuleUdpListenerFilterConfig class in Envoy. + * + * OWNERSHIP: Envoy owns the pointer. + */ +typedef void* envoy_dynamic_module_type_udp_listener_filter_config_envoy_ptr; + +/** + * envoy_dynamic_module_type_udp_listener_filter_config_module_ptr is a pointer to an in-module UDP + * listener filter configuration. + * + * OWNERSHIP: The module is responsible for managing the lifetime of the pointer. + */ +typedef const void* envoy_dynamic_module_type_udp_listener_filter_config_module_ptr; + +/** + * envoy_dynamic_module_type_udp_listener_filter_envoy_ptr is a raw pointer to the + * DynamicModuleUdpListenerFilter class in Envoy. + * + * OWNERSHIP: Envoy owns the pointer. + */ +typedef void* envoy_dynamic_module_type_udp_listener_filter_envoy_ptr; + +/** + * envoy_dynamic_module_type_udp_listener_filter_module_ptr is a pointer to an in-module UDP + * listener filter. + * + * OWNERSHIP: The module is responsible for managing the lifetime of the pointer. + */ +typedef const void* envoy_dynamic_module_type_udp_listener_filter_module_ptr; + +/** + * envoy_dynamic_module_type_on_udp_listener_filter_status represents the status of the UDP + * listener filter execution. + */ +typedef enum envoy_dynamic_module_type_on_udp_listener_filter_status { + envoy_dynamic_module_type_on_udp_listener_filter_status_Continue, + envoy_dynamic_module_type_on_udp_listener_filter_status_StopIteration, +} envoy_dynamic_module_type_on_udp_listener_filter_status; + +/** + * envoy_dynamic_module_on_udp_listener_filter_config_new is called when a new UDP listener filter + * configuration is created. + */ +envoy_dynamic_module_type_udp_listener_filter_config_module_ptr +envoy_dynamic_module_on_udp_listener_filter_config_new( + envoy_dynamic_module_type_udp_listener_filter_config_envoy_ptr filter_config_envoy_ptr, + const char* name_ptr, size_t name_size, const char* config_ptr, size_t config_size); + +/** + * envoy_dynamic_module_on_udp_listener_filter_config_destroy is called when the UDP listener filter + * configuration is destroyed. + */ +void envoy_dynamic_module_on_udp_listener_filter_config_destroy( + envoy_dynamic_module_type_udp_listener_filter_config_module_ptr filter_config_ptr); + +/** + * envoy_dynamic_module_on_udp_listener_filter_new is called when a new UDP listener filter is + * created. + */ +envoy_dynamic_module_type_udp_listener_filter_module_ptr +envoy_dynamic_module_on_udp_listener_filter_new( + envoy_dynamic_module_type_udp_listener_filter_config_module_ptr filter_config_ptr, + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr); + +/** + * envoy_dynamic_module_on_udp_listener_filter_on_data is called when a UDP packet is received. + */ +envoy_dynamic_module_type_on_udp_listener_filter_status +envoy_dynamic_module_on_udp_listener_filter_on_data( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_udp_listener_filter_module_ptr filter_module_ptr); + +/** + * envoy_dynamic_module_on_udp_listener_filter_destroy is called when the UDP listener filter is + * destroyed. + */ +void envoy_dynamic_module_on_udp_listener_filter_destroy( + envoy_dynamic_module_type_udp_listener_filter_module_ptr filter_module_ptr); + +// Callbacks + +/** + * envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks_size is called by the + * module to get the number of chunks in the current datagram data. Combined with + * envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks, this can be used to + * iterate over all chunks in the datagram. This is only valid during the + * envoy_dynamic_module_on_udp_listener_filter_on_data callback. + */ +bool envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks_size( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, + size_t* chunks_size_out); + +/** + * envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks is called by the + * module to get the current datagram data as chunks. The module must ensure the provided buffer + * array has enough capacity to store all chunks, which can be obtained via + * envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks_size. This is only + * valid during the envoy_dynamic_module_on_udp_listener_filter_on_data callback. + * + * @return true if the datagram data is available and chunks_out is populated, false otherwise. + */ +bool envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_envoy_buffer* chunks_out); + +/** + * envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_size is called by the module + * to get the total length in bytes of the current datagram data. This is only valid during the + * envoy_dynamic_module_on_udp_listener_filter_on_data callback. + * + * @param size_out is the output pointer to the total length of the datagram data in bytes. + * @return true if the datagram data is available, false otherwise. + */ +bool envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_size( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, size_t* size_out); + +/** + * envoy_dynamic_module_callback_udp_listener_filter_set_datagram_data is called by the module to + * set the current datagram data. + */ +bool envoy_dynamic_module_callback_udp_listener_filter_set_datagram_data( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_module_buffer data); + +/** + * envoy_dynamic_module_callback_udp_listener_filter_get_peer_address is called by the module to + * get the peer address. + */ +bool envoy_dynamic_module_callback_udp_listener_filter_get_peer_address( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_envoy_buffer* address_out, uint32_t* port_out); + +/** + * envoy_dynamic_module_callback_udp_listener_filter_get_local_address is called by the module to + * get the local address. + */ +bool envoy_dynamic_module_callback_udp_listener_filter_get_local_address( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_envoy_buffer* address_out, uint32_t* port_out); + +/** + * envoy_dynamic_module_callback_udp_listener_filter_send_datagram is called by the module to + * send a datagram. + * + * @return true if the datagram was sent, false otherwise. + */ +bool envoy_dynamic_module_callback_udp_listener_filter_send_datagram( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_module_buffer data, + envoy_dynamic_module_type_module_buffer peer_address, uint32_t peer_port); + #ifdef __cplusplus } #endif diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index d97ff7fe9ef37..0d148c4a31feb 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -230,6 +230,7 @@ EXTENSIONS = { "envoy.filters.listener.proxy_protocol": "//source/extensions/filters/listener/proxy_protocol:config", "envoy.filters.listener.tls_inspector": "//source/extensions/filters/listener/tls_inspector:config", "envoy.filters.listener.dynamic_modules": "//source/extensions/filters/listener/dynamic_modules:config", + "envoy.filters.udp_listener.dynamic_modules": "//source/extensions/filters/udp/dynamic_modules:config", # # Network filters diff --git a/source/extensions/extensions_metadata.yaml b/source/extensions/extensions_metadata.yaml index 49e705de4948e..2d96348f48b1e 100644 --- a/source/extensions/extensions_metadata.yaml +++ b/source/extensions/extensions_metadata.yaml @@ -945,6 +945,13 @@ envoy.filters.udp.dns_filter: status: stable type_urls: - envoy.extensions.filters.udp.dns_filter.v3.DnsFilterConfig +envoy.filters.udp_listener.dynamic_modules: + categories: + - envoy.filters.udp_listener + security_posture: requires_trusted_downstream_and_upstream + status: alpha + type_urls: + - envoy.extensions.filters.udp.dynamic_modules.v3.DynamicModuleUdpListenerFilter envoy.filters.udp_listener.udp_proxy: categories: - envoy.filters.udp_listener diff --git a/source/extensions/filters/udp/dynamic_modules/BUILD b/source/extensions/filters/udp/dynamic_modules/BUILD new file mode 100644 index 0000000000000..4913cf4685c88 --- /dev/null +++ b/source/extensions/filters/udp/dynamic_modules/BUILD @@ -0,0 +1,55 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "filter_config_lib", + srcs = ["filter_config.cc"], + hdrs = ["filter_config.h"], + deps = [ + "//source/common/config:utility_lib", + "//source/extensions/dynamic_modules:dynamic_modules_lib", + "@envoy_api//envoy/extensions/filters/udp/dynamic_modules/v3:pkg_cc_proto", + ], +) + +envoy_cc_library( + name = "filter_lib", + srcs = [ + "abi_impl.cc", + "filter.cc", + ], + hdrs = [ + "abi_impl.h", + "filter.h", + ], + deps = [ + ":filter_config_lib", + "//envoy/network:filter_interface", + "//envoy/network:listener_interface", + "//source/common/buffer:buffer_lib", + "//source/common/common:logger_lib", + "//source/common/network:utility_lib", + "//source/common/protobuf", + "//source/extensions/dynamic_modules:dynamic_modules_lib", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["factory.cc"], + hdrs = ["factory.h"], + deps = [ + ":filter_config_lib", + ":filter_lib", + "//source/common/protobuf:utility_lib", + "//source/extensions/filters/network/common:factory_base_lib", + ], +) diff --git a/source/extensions/filters/udp/dynamic_modules/abi_impl.cc b/source/extensions/filters/udp/dynamic_modules/abi_impl.cc new file mode 100644 index 0000000000000..72f748006afc0 --- /dev/null +++ b/source/extensions/filters/udp/dynamic_modules/abi_impl.cc @@ -0,0 +1,181 @@ +// NOLINT(namespace-envoy) +#include "source/extensions/filters/udp/dynamic_modules/abi_impl.h" + +#include "envoy/network/address.h" + +#include "source/common/network/utility.h" +#include "source/extensions/filters/udp/dynamic_modules/filter.h" + +using Envoy::Extensions::UdpFilters::DynamicModules::DynamicModuleUdpListenerFilter; + +namespace { + +void fillBufferChunks(const Envoy::Buffer::Instance& buffer, + envoy_dynamic_module_type_envoy_buffer* result_buffer_vector) { + Envoy::Buffer::RawSliceVector raw_slices = buffer.getRawSlices(); + size_t counter = 0; + for (const auto& slice : raw_slices) { + result_buffer_vector[counter].ptr = static_cast(slice.mem_); + result_buffer_vector[counter].length = slice.len_; + counter++; + } +} + +} // namespace + +extern "C" { + +bool envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks_size( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, + size_t* chunks_size_out) { + auto* filter = static_cast(filter_envoy_ptr); + auto* data = filter->currentData(); + if (!data || !data->buffer_) { + if (chunks_size_out != nullptr) { + *chunks_size_out = 0; + } + return false; + } + + if (chunks_size_out != nullptr) { + *chunks_size_out = data->buffer_->getRawSlices().size(); + } + return true; +} + +bool envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_envoy_buffer* chunks_out) { + auto* filter = static_cast(filter_envoy_ptr); + auto* data = filter->currentData(); + if (!data || !data->buffer_) { + return false; + } + + if (chunks_out == nullptr) { + return false; + } + + fillBufferChunks(*data->buffer_, chunks_out); + return true; +} + +bool envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_size( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, size_t* size_out) { + auto* filter = static_cast(filter_envoy_ptr); + auto* data = filter->currentData(); + if (!data || !data->buffer_) { + return false; + } + if (size_out != nullptr) { + *size_out = data->buffer_->length(); + } + return true; +} + +bool envoy_dynamic_module_callback_udp_listener_filter_set_datagram_data( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_module_buffer data) { + auto* filter = static_cast(filter_envoy_ptr); + auto* current_data = filter->currentData(); + if (!current_data) { + return false; + } + + current_data->buffer_->drain(current_data->buffer_->length()); + if (data.ptr != nullptr && data.length > 0) { + current_data->buffer_->add(data.ptr, data.length); + } + return true; +} + +bool envoy_dynamic_module_callback_udp_listener_filter_get_peer_address( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_envoy_buffer* address_out, uint32_t* port_out) { + auto* filter = static_cast(filter_envoy_ptr); + auto* current_data = filter->currentData(); + if (!current_data || !current_data->addresses_.peer_) { + return false; + } + + const auto& addr = *current_data->addresses_.peer_; + if (addr.type() != Envoy::Network::Address::Type::Ip) { + return false; + } + + const std::string& ip_str = addr.ip()->addressAsString(); + address_out->ptr = const_cast(ip_str.data()); + address_out->length = ip_str.size(); + *port_out = addr.ip()->port(); + return true; +} + +bool envoy_dynamic_module_callback_udp_listener_filter_get_local_address( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_envoy_buffer* address_out, uint32_t* port_out) { + auto* filter = static_cast(filter_envoy_ptr); + auto* current_data = filter->currentData(); + const Envoy::Network::Address::Instance* addr = nullptr; + + if (current_data && current_data->addresses_.local_) { + addr = current_data->addresses_.local_.get(); + } else if (filter->callbacks()) { + addr = filter->callbacks()->udpListener().localAddress().get(); + } + + if (!addr || addr->type() != Envoy::Network::Address::Type::Ip) { + return false; + } + + const std::string& ip_str = addr->ip()->addressAsString(); + address_out->ptr = const_cast(ip_str.data()); + address_out->length = ip_str.size(); + *port_out = addr->ip()->port(); + return true; +} + +bool envoy_dynamic_module_callback_udp_listener_filter_send_datagram( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_module_buffer data, + envoy_dynamic_module_type_module_buffer peer_address, uint32_t peer_port) { + auto* filter = static_cast(filter_envoy_ptr); + + Envoy::Buffer::OwnedImpl buffer; + if (data.ptr && data.length > 0) { + buffer.add(data.ptr, data.length); + } + + Envoy::Network::Address::InstanceConstSharedPtr peer_addr; + if (peer_address.ptr && peer_address.length > 0) { + std::string ip_str(peer_address.ptr, peer_address.length); + peer_addr = Envoy::Network::Utility::parseInternetAddressNoThrow(ip_str, peer_port); + if (!peer_addr) { + return false; + } + } else { + if (filter->currentData()) { + peer_addr = filter->currentData()->addresses_.peer_; + } + } + + if (!peer_addr) { + return false; + } + + const Envoy::Network::Address::Instance* local_addr = nullptr; + if (filter->currentData()) { + local_addr = filter->currentData()->addresses_.local_.get(); + } + if (!local_addr && filter->callbacks()) { + local_addr = filter->callbacks()->udpListener().localAddress().get(); + } + + if (local_addr && filter->callbacks()) { + Envoy::Network::UdpSendData udp_data{local_addr->ip(), *peer_addr, buffer}; + filter->callbacks()->udpListener().send(udp_data); + return true; + } + return false; +} + +} // extern "C" diff --git a/source/extensions/filters/udp/dynamic_modules/abi_impl.h b/source/extensions/filters/udp/dynamic_modules/abi_impl.h new file mode 100644 index 0000000000000..a47538b9c53b4 --- /dev/null +++ b/source/extensions/filters/udp/dynamic_modules/abi_impl.h @@ -0,0 +1,5 @@ +#pragma once + +// NOLINT(namespace-envoy) + +#include "source/extensions/dynamic_modules/abi.h" diff --git a/source/extensions/filters/udp/dynamic_modules/factory.cc b/source/extensions/filters/udp/dynamic_modules/factory.cc new file mode 100644 index 0000000000000..1c4da93b403ca --- /dev/null +++ b/source/extensions/filters/udp/dynamic_modules/factory.cc @@ -0,0 +1,44 @@ +#include "source/extensions/filters/udp/dynamic_modules/factory.h" + +#include "source/extensions/filters/udp/dynamic_modules/filter.h" + +namespace Envoy { +namespace Extensions { +namespace UdpFilters { +namespace DynamicModules { + +Network::UdpListenerFilterFactoryCb +DynamicModuleUdpListenerFilterConfigFactory::createFilterFactoryFromProto( + const Protobuf::Message& config, Server::Configuration::ListenerFactoryContext&) { + const auto& proto_config = dynamic_cast< + const envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter&>( + config); + + auto dynamic_module_or_error = Extensions::DynamicModules::newDynamicModuleByName( + proto_config.dynamic_module_config().name(), + proto_config.dynamic_module_config().do_not_close(), + proto_config.dynamic_module_config().load_globally()); + + if (!dynamic_module_or_error.ok()) { + throw EnvoyException(std::string(dynamic_module_or_error.status().message())); + } + + auto dynamic_module = std::move(dynamic_module_or_error.value()); + + auto filter_config = std::make_shared( + proto_config, std::move(dynamic_module)); + + return [filter_config](Network::UdpListenerFilterManager& filter_manager, + Network::UdpReadFilterCallbacks& callbacks) -> void { + filter_manager.addReadFilter( + std::make_unique(callbacks, filter_config)); + }; +} + +REGISTER_FACTORY(DynamicModuleUdpListenerFilterConfigFactory, + Server::Configuration::NamedUdpListenerFilterConfigFactory); + +} // namespace DynamicModules +} // namespace UdpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/udp/dynamic_modules/factory.h b/source/extensions/filters/udp/dynamic_modules/factory.h new file mode 100644 index 0000000000000..f53fa84b002c2 --- /dev/null +++ b/source/extensions/filters/udp/dynamic_modules/factory.h @@ -0,0 +1,30 @@ +#pragma once + +#include "envoy/server/filter_config.h" + +#include "source/extensions/filters/udp/dynamic_modules/filter_config.h" + +namespace Envoy { +namespace Extensions { +namespace UdpFilters { +namespace DynamicModules { + +class DynamicModuleUdpListenerFilterConfigFactory + : public Server::Configuration::NamedUdpListenerFilterConfigFactory { +public: + Network::UdpListenerFilterFactoryCb + createFilterFactoryFromProto(const Protobuf::Message& config, + Server::Configuration::ListenerFactoryContext& context) override; + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique< + envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter>(); + } + + std::string name() const override { return "envoy.filters.udp_listener.dynamic_modules"; } +}; + +} // namespace DynamicModules +} // namespace UdpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/udp/dynamic_modules/filter.cc b/source/extensions/filters/udp/dynamic_modules/filter.cc new file mode 100644 index 0000000000000..04f17ac35269a --- /dev/null +++ b/source/extensions/filters/udp/dynamic_modules/filter.cc @@ -0,0 +1,44 @@ +#include "source/extensions/filters/udp/dynamic_modules/filter.h" + +#include "source/extensions/filters/udp/dynamic_modules/abi_impl.h" + +namespace Envoy { +namespace Extensions { +namespace UdpFilters { +namespace DynamicModules { + +DynamicModuleUdpListenerFilter::DynamicModuleUdpListenerFilter( + Network::UdpReadFilterCallbacks& callbacks, + DynamicModuleUdpListenerFilterConfigSharedPtr config) + : UdpListenerReadFilter(callbacks), config_(config) { + in_module_filter_ = config_->on_filter_new_(config_->in_module_config_, thisAsVoidPtr()); +} + +DynamicModuleUdpListenerFilter::~DynamicModuleUdpListenerFilter() { + if (in_module_filter_ != nullptr) { + config_->on_filter_destroy_(in_module_filter_); + } +} + +Network::FilterStatus DynamicModuleUdpListenerFilter::onData(Network::UdpRecvData& data) { + if (in_module_filter_ == nullptr) { + return Network::FilterStatus::Continue; + } + current_data_ = &data; + auto status = config_->on_filter_on_data_(thisAsVoidPtr(), in_module_filter_); + current_data_ = nullptr; + + if (status == envoy_dynamic_module_type_on_udp_listener_filter_status_StopIteration) { + return Network::FilterStatus::StopIteration; + } + return Network::FilterStatus::Continue; +} + +Network::FilterStatus DynamicModuleUdpListenerFilter::onReceiveError(Api::IoError::IoErrorCode) { + return Network::FilterStatus::Continue; +} + +} // namespace DynamicModules +} // namespace UdpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/udp/dynamic_modules/filter.h b/source/extensions/filters/udp/dynamic_modules/filter.h new file mode 100644 index 0000000000000..cf5e645eb0caf --- /dev/null +++ b/source/extensions/filters/udp/dynamic_modules/filter.h @@ -0,0 +1,47 @@ +#pragma once + +#include "envoy/network/filter.h" + +#include "source/common/common/logger.h" +#include "source/extensions/filters/udp/dynamic_modules/filter_config.h" + +namespace Envoy { +namespace Extensions { +namespace UdpFilters { +namespace DynamicModules { + +class DynamicModuleUdpListenerFilter : public Network::UdpListenerReadFilter, + public Logger::Loggable { +public: + DynamicModuleUdpListenerFilter(Network::UdpReadFilterCallbacks& callbacks, + DynamicModuleUdpListenerFilterConfigSharedPtr config); + ~DynamicModuleUdpListenerFilter() override; + + // Network::UdpListenerReadFilter + Network::FilterStatus onData(Network::UdpRecvData& data) override; + Network::FilterStatus onReceiveError(Api::IoError::IoErrorCode error_code) override; + + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr thisAsVoidPtr() { + return static_cast(this); + } + + Network::UdpRecvData* currentData() { return current_data_; } + Network::UdpReadFilterCallbacks* callbacks() { return read_callbacks_; } + +#ifdef ENVOY_ENABLE_FULL_PROTOS + // Test-only method to set current_data_ for ABI callback testing. + void setCurrentDataForTest(Network::UdpRecvData* data) { current_data_ = data; } +#endif + +private: + const DynamicModuleUdpListenerFilterConfigSharedPtr config_; + envoy_dynamic_module_type_udp_listener_filter_module_ptr in_module_filter_{nullptr}; + Network::UdpRecvData* current_data_{nullptr}; +}; + +using DynamicModuleUdpListenerFilterSharedPtr = std::shared_ptr; + +} // namespace DynamicModules +} // namespace UdpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/udp/dynamic_modules/filter_config.cc b/source/extensions/filters/udp/dynamic_modules/filter_config.cc new file mode 100644 index 0000000000000..4cb4d6e27ec6c --- /dev/null +++ b/source/extensions/filters/udp/dynamic_modules/filter_config.cc @@ -0,0 +1,74 @@ +#include "source/extensions/filters/udp/dynamic_modules/filter_config.h" + +#include "source/common/config/utility.h" +#include "source/common/protobuf/utility.h" + +namespace Envoy { +namespace Extensions { +namespace UdpFilters { +namespace DynamicModules { + +DynamicModuleUdpListenerFilterConfig::DynamicModuleUdpListenerFilterConfig( + const envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter& + config, + Extensions::DynamicModules::DynamicModulePtr dynamic_module) + : filter_name_(config.filter_name()), + filter_config_(MessageUtil::getJsonStringFromMessageOrError(config.filter_config())), + dynamic_module_(std::move(dynamic_module)) { + + auto config_new_or_error = dynamic_module_->getFunctionPointer( + "envoy_dynamic_module_on_udp_listener_filter_config_new"); + if (!config_new_or_error.ok()) { + throw EnvoyException("Dynamic module does not support UDP listener filters: " + + std::string(config_new_or_error.status().message())); + } + on_filter_config_new_ = config_new_or_error.value(); + + auto config_destroy_or_error = + dynamic_module_->getFunctionPointer( + "envoy_dynamic_module_on_udp_listener_filter_config_destroy"); + if (!config_destroy_or_error.ok()) { + throw EnvoyException("Dynamic module does not support UDP listener filters: " + + std::string(config_destroy_or_error.status().message())); + } + on_filter_config_destroy_ = config_destroy_or_error.value(); + + auto filter_new_or_error = dynamic_module_->getFunctionPointer( + "envoy_dynamic_module_on_udp_listener_filter_new"); + if (!filter_new_or_error.ok()) { + throw EnvoyException("Dynamic module does not support UDP listener filters: " + + std::string(filter_new_or_error.status().message())); + } + on_filter_new_ = filter_new_or_error.value(); + + auto filter_on_data_or_error = dynamic_module_->getFunctionPointer( + "envoy_dynamic_module_on_udp_listener_filter_on_data"); + if (!filter_on_data_or_error.ok()) { + throw EnvoyException("Dynamic module does not support UDP listener filters: " + + std::string(filter_on_data_or_error.status().message())); + } + on_filter_on_data_ = filter_on_data_or_error.value(); + + auto filter_destroy_or_error = dynamic_module_->getFunctionPointer( + "envoy_dynamic_module_on_udp_listener_filter_destroy"); + if (!filter_destroy_or_error.ok()) { + throw EnvoyException("Dynamic module does not support UDP listener filters: " + + std::string(filter_destroy_or_error.status().message())); + } + on_filter_destroy_ = filter_destroy_or_error.value(); + + in_module_config_ = + on_filter_config_new_(static_cast(this), filter_name_.c_str(), filter_name_.size(), + filter_config_.data(), filter_config_.size()); +} + +DynamicModuleUdpListenerFilterConfig::~DynamicModuleUdpListenerFilterConfig() { + if (in_module_config_ != nullptr) { + on_filter_config_destroy_(in_module_config_); + } +} + +} // namespace DynamicModules +} // namespace UdpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/udp/dynamic_modules/filter_config.h b/source/extensions/filters/udp/dynamic_modules/filter_config.h new file mode 100644 index 0000000000000..b2b7c0d50207c --- /dev/null +++ b/source/extensions/filters/udp/dynamic_modules/filter_config.h @@ -0,0 +1,42 @@ +#pragma once + +#include "envoy/extensions/filters/udp/dynamic_modules/v3/dynamic_modules.pb.h" + +#include "source/extensions/dynamic_modules/abi.h" +#include "source/extensions/dynamic_modules/dynamic_modules.h" + +namespace Envoy { +namespace Extensions { +namespace UdpFilters { +namespace DynamicModules { + +class DynamicModuleUdpListenerFilterConfig { +public: + DynamicModuleUdpListenerFilterConfig( + const envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter& + config, + Extensions::DynamicModules::DynamicModulePtr dynamic_module); + + ~DynamicModuleUdpListenerFilterConfig(); + + const std::string filter_name_; + const std::string filter_config_; + Extensions::DynamicModules::DynamicModulePtr dynamic_module_; + + envoy_dynamic_module_type_udp_listener_filter_config_module_ptr in_module_config_{nullptr}; + + decltype(envoy_dynamic_module_on_udp_listener_filter_config_new)* on_filter_config_new_{nullptr}; + decltype(envoy_dynamic_module_on_udp_listener_filter_config_destroy)* on_filter_config_destroy_{ + nullptr}; + decltype(envoy_dynamic_module_on_udp_listener_filter_new)* on_filter_new_{nullptr}; + decltype(envoy_dynamic_module_on_udp_listener_filter_on_data)* on_filter_on_data_{nullptr}; + decltype(envoy_dynamic_module_on_udp_listener_filter_destroy)* on_filter_destroy_{nullptr}; +}; + +using DynamicModuleUdpListenerFilterConfigSharedPtr = + std::shared_ptr; + +} // namespace DynamicModules +} // namespace UdpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/dynamic_modules/test_data/c/BUILD b/test/extensions/dynamic_modules/test_data/c/BUILD index ecd4ce147b952..778f0c82335cd 100644 --- a/test/extensions/dynamic_modules/test_data/c/BUILD +++ b/test/extensions/dynamic_modules/test_data/c/BUILD @@ -7,6 +7,7 @@ package(default_visibility = [ "//test/extensions/dynamic_modules/http:__pkg__", "//test/extensions/dynamic_modules/listener:__pkg__", "//test/extensions/dynamic_modules/network:__pkg__", + "//test/extensions/dynamic_modules/udp:__pkg__", ]) test_program(name = "no_op") @@ -62,3 +63,15 @@ test_program(name = "listener_no_op") test_program(name = "listener_config_new_fail") test_program(name = "listener_stop_iteration") + +test_program(name = "udp_no_op") + +test_program(name = "udp_stop_iteration") + +test_program(name = "udp_no_config_destroy") + +test_program(name = "udp_no_filter_new") + +test_program(name = "udp_no_on_data") + +test_program(name = "udp_no_filter_destroy") diff --git a/test/extensions/dynamic_modules/test_data/c/udp_no_config_destroy.c b/test/extensions/dynamic_modules/test_data/c/udp_no_config_destroy.c new file mode 100644 index 0000000000000..265be962d055b --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/c/udp_no_config_destroy.c @@ -0,0 +1,36 @@ +#include + +#include "source/extensions/dynamic_modules/abi.h" +#include "source/extensions/dynamic_modules/abi_version.h" + +static int some_variable = 0; + +envoy_dynamic_module_type_abi_version_module_ptr envoy_dynamic_module_on_program_init(void) { + return kAbiVersion; +} + +envoy_dynamic_module_type_udp_listener_filter_config_module_ptr +envoy_dynamic_module_on_udp_listener_filter_config_new( + envoy_dynamic_module_type_udp_listener_filter_config_envoy_ptr filter_config_envoy_ptr, + const char* name_ptr, size_t name_size, const char* config_ptr, size_t config_size) { + return &some_variable; +} + +envoy_dynamic_module_type_udp_listener_filter_module_ptr +envoy_dynamic_module_on_udp_listener_filter_new( + envoy_dynamic_module_type_udp_listener_filter_config_module_ptr filter_config_ptr, + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr) { + return &some_variable + 1; +} + +envoy_dynamic_module_type_on_udp_listener_filter_status +envoy_dynamic_module_on_udp_listener_filter_on_data( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_udp_listener_filter_module_ptr filter_module_ptr) { + return envoy_dynamic_module_type_on_udp_listener_filter_status_Continue; +} + +void envoy_dynamic_module_on_udp_listener_filter_destroy( + envoy_dynamic_module_type_udp_listener_filter_module_ptr filter_module_ptr) { + assert(filter_module_ptr == &some_variable + 1); +} diff --git a/test/extensions/dynamic_modules/test_data/c/udp_no_filter_destroy.c b/test/extensions/dynamic_modules/test_data/c/udp_no_filter_destroy.c new file mode 100644 index 0000000000000..81f0fd073aeeb --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/c/udp_no_filter_destroy.c @@ -0,0 +1,36 @@ +#include + +#include "source/extensions/dynamic_modules/abi.h" +#include "source/extensions/dynamic_modules/abi_version.h" + +static int some_variable = 0; + +envoy_dynamic_module_type_abi_version_module_ptr envoy_dynamic_module_on_program_init(void) { + return kAbiVersion; +} + +envoy_dynamic_module_type_udp_listener_filter_config_module_ptr +envoy_dynamic_module_on_udp_listener_filter_config_new( + envoy_dynamic_module_type_udp_listener_filter_config_envoy_ptr filter_config_envoy_ptr, + const char* name_ptr, size_t name_size, const char* config_ptr, size_t config_size) { + return &some_variable; +} + +void envoy_dynamic_module_on_udp_listener_filter_config_destroy( + envoy_dynamic_module_type_udp_listener_filter_config_module_ptr filter_config_ptr) { + assert(filter_config_ptr == &some_variable); +} + +envoy_dynamic_module_type_udp_listener_filter_module_ptr +envoy_dynamic_module_on_udp_listener_filter_new( + envoy_dynamic_module_type_udp_listener_filter_config_module_ptr filter_config_ptr, + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr) { + return &some_variable + 1; +} + +envoy_dynamic_module_type_on_udp_listener_filter_status +envoy_dynamic_module_on_udp_listener_filter_on_data( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_udp_listener_filter_module_ptr filter_module_ptr) { + return envoy_dynamic_module_type_on_udp_listener_filter_status_Continue; +} diff --git a/test/extensions/dynamic_modules/test_data/c/udp_no_filter_new.c b/test/extensions/dynamic_modules/test_data/c/udp_no_filter_new.c new file mode 100644 index 0000000000000..e68a6337afa1a --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/c/udp_no_filter_new.c @@ -0,0 +1,34 @@ +#include + +#include "source/extensions/dynamic_modules/abi.h" +#include "source/extensions/dynamic_modules/abi_version.h" + +static int some_variable = 0; + +envoy_dynamic_module_type_abi_version_module_ptr envoy_dynamic_module_on_program_init(void) { + return kAbiVersion; +} + +envoy_dynamic_module_type_udp_listener_filter_config_module_ptr +envoy_dynamic_module_on_udp_listener_filter_config_new( + envoy_dynamic_module_type_udp_listener_filter_config_envoy_ptr filter_config_envoy_ptr, + const char* name_ptr, size_t name_size, const char* config_ptr, size_t config_size) { + return &some_variable; +} + +void envoy_dynamic_module_on_udp_listener_filter_config_destroy( + envoy_dynamic_module_type_udp_listener_filter_config_module_ptr filter_config_ptr) { + assert(filter_config_ptr == &some_variable); +} + +envoy_dynamic_module_type_on_udp_listener_filter_status +envoy_dynamic_module_on_udp_listener_filter_on_data( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_udp_listener_filter_module_ptr filter_module_ptr) { + return envoy_dynamic_module_type_on_udp_listener_filter_status_Continue; +} + +void envoy_dynamic_module_on_udp_listener_filter_destroy( + envoy_dynamic_module_type_udp_listener_filter_module_ptr filter_module_ptr) { + assert(filter_module_ptr == &some_variable + 1); +} diff --git a/test/extensions/dynamic_modules/test_data/c/udp_no_on_data.c b/test/extensions/dynamic_modules/test_data/c/udp_no_on_data.c new file mode 100644 index 0000000000000..27808eef65a86 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/c/udp_no_on_data.c @@ -0,0 +1,34 @@ +#include + +#include "source/extensions/dynamic_modules/abi.h" +#include "source/extensions/dynamic_modules/abi_version.h" + +static int some_variable = 0; + +envoy_dynamic_module_type_abi_version_module_ptr envoy_dynamic_module_on_program_init(void) { + return kAbiVersion; +} + +envoy_dynamic_module_type_udp_listener_filter_config_module_ptr +envoy_dynamic_module_on_udp_listener_filter_config_new( + envoy_dynamic_module_type_udp_listener_filter_config_envoy_ptr filter_config_envoy_ptr, + const char* name_ptr, size_t name_size, const char* config_ptr, size_t config_size) { + return &some_variable; +} + +void envoy_dynamic_module_on_udp_listener_filter_config_destroy( + envoy_dynamic_module_type_udp_listener_filter_config_module_ptr filter_config_ptr) { + assert(filter_config_ptr == &some_variable); +} + +envoy_dynamic_module_type_udp_listener_filter_module_ptr +envoy_dynamic_module_on_udp_listener_filter_new( + envoy_dynamic_module_type_udp_listener_filter_config_module_ptr filter_config_ptr, + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr) { + return &some_variable + 1; +} + +void envoy_dynamic_module_on_udp_listener_filter_destroy( + envoy_dynamic_module_type_udp_listener_filter_module_ptr filter_module_ptr) { + assert(filter_module_ptr == &some_variable + 1); +} diff --git a/test/extensions/dynamic_modules/test_data/c/udp_no_op.c b/test/extensions/dynamic_modules/test_data/c/udp_no_op.c new file mode 100644 index 0000000000000..e3bd0eda12c57 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/c/udp_no_op.c @@ -0,0 +1,41 @@ +#include + +#include "source/extensions/dynamic_modules/abi.h" +#include "source/extensions/dynamic_modules/abi_version.h" + +static int some_variable = 0; + +envoy_dynamic_module_type_abi_version_module_ptr envoy_dynamic_module_on_program_init(void) { + return kAbiVersion; +} + +envoy_dynamic_module_type_udp_listener_filter_config_module_ptr +envoy_dynamic_module_on_udp_listener_filter_config_new( + envoy_dynamic_module_type_udp_listener_filter_config_envoy_ptr filter_config_envoy_ptr, + const char* name_ptr, size_t name_size, const char* config_ptr, size_t config_size) { + return &some_variable; +} + +void envoy_dynamic_module_on_udp_listener_filter_config_destroy( + envoy_dynamic_module_type_udp_listener_filter_config_module_ptr filter_config_ptr) { + assert(filter_config_ptr == &some_variable); +} + +envoy_dynamic_module_type_udp_listener_filter_module_ptr +envoy_dynamic_module_on_udp_listener_filter_new( + envoy_dynamic_module_type_udp_listener_filter_config_module_ptr filter_config_ptr, + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr) { + return &some_variable + 1; +} + +envoy_dynamic_module_type_on_udp_listener_filter_status +envoy_dynamic_module_on_udp_listener_filter_on_data( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_udp_listener_filter_module_ptr filter_module_ptr) { + return envoy_dynamic_module_type_on_udp_listener_filter_status_Continue; +} + +void envoy_dynamic_module_on_udp_listener_filter_destroy( + envoy_dynamic_module_type_udp_listener_filter_module_ptr filter_module_ptr) { + assert(filter_module_ptr == &some_variable + 1); +} diff --git a/test/extensions/dynamic_modules/test_data/c/udp_stop_iteration.c b/test/extensions/dynamic_modules/test_data/c/udp_stop_iteration.c new file mode 100644 index 0000000000000..cdeab1a80eb76 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/c/udp_stop_iteration.c @@ -0,0 +1,41 @@ +#include + +#include "source/extensions/dynamic_modules/abi.h" +#include "source/extensions/dynamic_modules/abi_version.h" + +static int some_variable = 0; + +envoy_dynamic_module_type_abi_version_module_ptr envoy_dynamic_module_on_program_init(void) { + return kAbiVersion; +} + +envoy_dynamic_module_type_udp_listener_filter_config_module_ptr +envoy_dynamic_module_on_udp_listener_filter_config_new( + envoy_dynamic_module_type_udp_listener_filter_config_envoy_ptr filter_config_envoy_ptr, + const char* name_ptr, size_t name_size, const char* config_ptr, size_t config_size) { + return &some_variable; +} + +void envoy_dynamic_module_on_udp_listener_filter_config_destroy( + envoy_dynamic_module_type_udp_listener_filter_config_module_ptr filter_config_ptr) { + assert(filter_config_ptr == &some_variable); +} + +envoy_dynamic_module_type_udp_listener_filter_module_ptr +envoy_dynamic_module_on_udp_listener_filter_new( + envoy_dynamic_module_type_udp_listener_filter_config_module_ptr filter_config_ptr, + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr) { + return &some_variable + 1; +} + +envoy_dynamic_module_type_on_udp_listener_filter_status +envoy_dynamic_module_on_udp_listener_filter_on_data( + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_udp_listener_filter_module_ptr filter_module_ptr) { + return envoy_dynamic_module_type_on_udp_listener_filter_status_StopIteration; +} + +void envoy_dynamic_module_on_udp_listener_filter_destroy( + envoy_dynamic_module_type_udp_listener_filter_module_ptr filter_module_ptr) { + assert(filter_module_ptr == &some_variable + 1); +} diff --git a/test/extensions/dynamic_modules/udp/BUILD b/test/extensions/dynamic_modules/udp/BUILD new file mode 100644 index 0000000000000..c948d23ff1f3a --- /dev/null +++ b/test/extensions/dynamic_modules/udp/BUILD @@ -0,0 +1,80 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_test( + name = "filter_test", + srcs = ["filter_test.cc"], + data = [ + "//test/extensions/dynamic_modules/test_data/c:no_op", + "//test/extensions/dynamic_modules/test_data/c:udp_no_config_destroy", + "//test/extensions/dynamic_modules/test_data/c:udp_no_filter_destroy", + "//test/extensions/dynamic_modules/test_data/c:udp_no_filter_new", + "//test/extensions/dynamic_modules/test_data/c:udp_no_on_data", + "//test/extensions/dynamic_modules/test_data/c:udp_no_op", + "//test/extensions/dynamic_modules/test_data/c:udp_stop_iteration", + ], + deps = [ + "//source/extensions/dynamic_modules:dynamic_modules_lib", + "//source/extensions/filters/udp/dynamic_modules:filter_lib", + "//test/extensions/dynamic_modules:util", + "//test/mocks/network:network_mocks", + "//test/test_common:utility_lib", + ], +) + +envoy_cc_test( + name = "abi_impl_test", + srcs = ["abi_impl_test.cc"], + data = [ + "//test/extensions/dynamic_modules/test_data/c:udp_no_op", + ], + deps = [ + "//source/extensions/filters/udp/dynamic_modules:filter_lib", + "//test/extensions/dynamic_modules:util", + "//test/mocks/network:network_mocks", + "//test/test_common:utility_lib", + ], +) + +envoy_cc_test( + name = "factory_test", + srcs = ["factory_test.cc"], + data = [ + "//test/extensions/dynamic_modules/test_data/c:no_op", + "//test/extensions/dynamic_modules/test_data/c:udp_no_op", + ], + deps = [ + "//source/extensions/filters/udp/dynamic_modules:config", + "//test/extensions/dynamic_modules:util", + "//test/mocks/network:network_mocks", + "//test/mocks/server:listener_factory_context_mocks", + "//test/test_common:utility_lib", + ], +) + +envoy_cc_test( + name = "udp_dynamic_modules_integration_test", + srcs = ["udp_dynamic_modules_integration_test.cc"], + data = [ + "//test/extensions/dynamic_modules/test_data/c:udp_no_op", + "//test/extensions/dynamic_modules/test_data/c:udp_stop_iteration", + ], + deps = [ + "//source/extensions/filters/udp/dynamic_modules:config", + "//source/extensions/filters/udp/udp_proxy:config", + "//test/extensions/dynamic_modules:util", + "//test/integration:integration_lib", + "//test/test_common:network_utility_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/udp/dynamic_modules/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/udp/udp_proxy/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/dynamic_modules/udp/abi_impl_test.cc b/test/extensions/dynamic_modules/udp/abi_impl_test.cc new file mode 100644 index 0000000000000..0cd814f5cc27f --- /dev/null +++ b/test/extensions/dynamic_modules/udp/abi_impl_test.cc @@ -0,0 +1,602 @@ +#include + +#include "source/common/network/address_impl.h" +#include "source/common/network/utility.h" +#include "source/extensions/dynamic_modules/abi.h" +#include "source/extensions/filters/udp/dynamic_modules/filter.h" + +#include "test/extensions/dynamic_modules/util.h" +#include "test/mocks/network/mocks.h" + +namespace Envoy { +namespace Extensions { +namespace UdpFilters { +namespace DynamicModules { + +class DynamicModuleUdpListenerFilterAbiCallbackTest : public testing::Test { +public: + void SetUp() override { + auto dynamic_module = Extensions::DynamicModules::newDynamicModule( + Extensions::DynamicModules::testSharedObjectPath("udp_no_op", "c"), false); + EXPECT_TRUE(dynamic_module.ok()) << dynamic_module.status().message(); + + envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter + proto_config; + proto_config.set_filter_name("test_filter"); + proto_config.mutable_filter_config()->set_value("some_config"); + + filter_config_ = std::make_shared( + proto_config, std::move(dynamic_module.value())); + + filter_ = std::make_shared(callbacks_, filter_config_); + } + + void TearDown() override { filter_.reset(); } + + void* filterPtr() { return static_cast(filter_.get()); } + + DynamicModuleUdpListenerFilterConfigSharedPtr filter_config_; + std::shared_ptr filter_; + NiceMock callbacks_; +}; + +// ============================================================================= +// Tests for get_datagram_data (size + chunks retrieval). +// ============================================================================= + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, GetDatagramDataWithSingleChunk) { + Network::UdpRecvData data; + data.buffer_ = std::make_unique("hello world"); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:1234"); + data.addresses_.local_ = Network::Utility::parseInternetAddressAndPortNoThrow("5.6.7.8:5678"); + + // Set current data to simulate being inside onData callback. + filter_->setCurrentDataForTest(&data); + + size_t chunks_size = 0; + bool ok = envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks_size( + filterPtr(), &chunks_size); + + EXPECT_TRUE(ok); + EXPECT_GE(chunks_size, 1); + + std::vector chunks(chunks_size); + ASSERT_TRUE(envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks( + filterPtr(), chunks.data())); + size_t returned_length = 0; + ASSERT_TRUE(envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_size( + filterPtr(), &returned_length)); + + // Verify the data. + size_t total_length = 0; + std::string reconstructed; + for (size_t i = 0; i < chunks_size; i++) { + total_length += chunks[i].length; + reconstructed.append(chunks[i].ptr, chunks[i].length); + } + EXPECT_EQ(returned_length, total_length); + EXPECT_EQ(11, total_length); + EXPECT_EQ("hello world", reconstructed); + + filter_->setCurrentDataForTest(nullptr); +} + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, GetDatagramDataNoCurrentData) { + // No current data set outside of onData callback. + size_t chunks_size = 0; + + bool ok = envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks_size( + filterPtr(), &chunks_size); + + EXPECT_FALSE(ok); + EXPECT_EQ(0, chunks_size); + + EXPECT_FALSE(envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks( + filterPtr(), nullptr)); + size_t returned_length = 0; + EXPECT_FALSE(envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_size( + filterPtr(), &returned_length)); + EXPECT_EQ(0, returned_length); +} + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, GetDatagramDataMultipleChunks) { + Network::UdpRecvData data; + // Create buffer with multiple chunks. + data.buffer_ = std::make_unique(); + data.buffer_->add("chunk1"); + data.buffer_->add("chunk2"); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:1234"); + + filter_->setCurrentDataForTest(&data); + + size_t chunks_size = 0; + bool ok = envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks_size( + filterPtr(), &chunks_size); + EXPECT_TRUE(ok); + EXPECT_GE(chunks_size, 1); + + std::vector chunks(chunks_size); + ASSERT_TRUE(envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks( + filterPtr(), chunks.data())); + size_t returned_length = 0; + ASSERT_TRUE(envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_size( + filterPtr(), &returned_length)); + + size_t total_length = 0; + std::string combined; + for (size_t i = 0; i < chunks_size; i++) { + total_length += chunks[i].length; + combined.append(chunks[i].ptr, chunks[i].length); + } + EXPECT_EQ(returned_length, total_length); + EXPECT_EQ(12, total_length); + EXPECT_EQ("chunk1chunk2", combined); + + filter_->setCurrentDataForTest(nullptr); +} + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, GetDatagramDataNullChunksSizeOut) { + Network::UdpRecvData data; + data.buffer_ = std::make_unique("test"); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:1234"); + + filter_->setCurrentDataForTest(&data); + + bool ok = envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks_size( + filterPtr(), nullptr); + + EXPECT_TRUE(ok); + + size_t chunks_size = 0; + envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks_size(filterPtr(), + &chunks_size); + std::vector chunks(chunks_size); + ASSERT_TRUE(envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks( + filterPtr(), chunks.data())); + + filter_->setCurrentDataForTest(nullptr); +} + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, GetDatagramDataEmptyBuffer) { + Network::UdpRecvData data; + data.buffer_ = std::make_unique(); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:1234"); + + filter_->setCurrentDataForTest(&data); + + size_t chunks_size = 0; + bool ok = envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks_size( + filterPtr(), &chunks_size); + + EXPECT_TRUE(ok); + EXPECT_EQ(0, chunks_size); + + EXPECT_FALSE(envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks( + filterPtr(), nullptr)); + size_t returned_length = 0; + EXPECT_TRUE(envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_size( + filterPtr(), &returned_length)); + EXPECT_EQ(0, returned_length); + + filter_->setCurrentDataForTest(nullptr); +} + +// ============================================================================= +// Tests for set_datagram_data. +// ============================================================================= + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, SetDatagramDataReplaceContent) { + Network::UdpRecvData data; + data.buffer_ = std::make_unique("original"); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:1234"); + + filter_->setCurrentDataForTest(&data); + + const char* new_data = "modified"; + envoy_dynamic_module_type_module_buffer new_buffer = {new_data, 8}; + + bool ok = + envoy_dynamic_module_callback_udp_listener_filter_set_datagram_data(filterPtr(), new_buffer); + + EXPECT_TRUE(ok); + EXPECT_EQ("modified", data.buffer_->toString()); + + filter_->setCurrentDataForTest(nullptr); +} + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, SetDatagramDataClearBuffer) { + Network::UdpRecvData data; + data.buffer_ = std::make_unique("original"); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:1234"); + + filter_->setCurrentDataForTest(&data); + + envoy_dynamic_module_type_module_buffer empty_buffer = {nullptr, 0}; + + bool ok = envoy_dynamic_module_callback_udp_listener_filter_set_datagram_data(filterPtr(), + empty_buffer); + + EXPECT_TRUE(ok); + EXPECT_EQ(0, data.buffer_->length()); + + filter_->setCurrentDataForTest(nullptr); +} + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, SetDatagramDataNoCurrentData) { + const char* new_data = "test"; + envoy_dynamic_module_type_module_buffer new_buffer = {new_data, 4}; + + bool ok = + envoy_dynamic_module_callback_udp_listener_filter_set_datagram_data(filterPtr(), new_buffer); + + EXPECT_FALSE(ok); +} + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, SetDatagramDataNullPointerWithLength) { + Network::UdpRecvData data; + data.buffer_ = std::make_unique("original"); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:1234"); + + filter_->setCurrentDataForTest(&data); + + envoy_dynamic_module_type_module_buffer invalid_buffer = {nullptr, 10}; + + bool ok = envoy_dynamic_module_callback_udp_listener_filter_set_datagram_data(filterPtr(), + invalid_buffer); + + EXPECT_TRUE(ok); + EXPECT_EQ(0, data.buffer_->length()); + + filter_->setCurrentDataForTest(nullptr); +} + +// ============================================================================= +// Tests for get_peer_address. +// ============================================================================= + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, GetPeerAddressIpv4) { + Network::UdpRecvData data; + data.buffer_ = std::make_unique("test"); + data.addresses_.peer_ = + Network::Utility::parseInternetAddressAndPortNoThrow("192.168.1.100:8080"); + data.addresses_.local_ = Network::Utility::parseInternetAddressAndPortNoThrow("10.0.0.1:9090"); + + filter_->setCurrentDataForTest(&data); + + envoy_dynamic_module_type_envoy_buffer address_buf = {nullptr, 0}; + uint32_t port = 0; + + bool ok = envoy_dynamic_module_callback_udp_listener_filter_get_peer_address(filterPtr(), + &address_buf, &port); + + EXPECT_TRUE(ok); + EXPECT_NE(nullptr, address_buf.ptr); + EXPECT_GT(address_buf.length, 0); + EXPECT_EQ("192.168.1.100", std::string(address_buf.ptr, address_buf.length)); + EXPECT_EQ(8080, port); + + filter_->setCurrentDataForTest(nullptr); +} + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, GetPeerAddressIpv6) { + Network::UdpRecvData data; + data.buffer_ = std::make_unique("test"); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("[::1]:12345"); + data.addresses_.local_ = Network::Utility::parseInternetAddressAndPortNoThrow("[::2]:54321"); + + filter_->setCurrentDataForTest(&data); + + envoy_dynamic_module_type_envoy_buffer address_buf = {nullptr, 0}; + uint32_t port = 0; + + bool ok = envoy_dynamic_module_callback_udp_listener_filter_get_peer_address(filterPtr(), + &address_buf, &port); + + EXPECT_TRUE(ok); + EXPECT_NE(nullptr, address_buf.ptr); + EXPECT_GT(address_buf.length, 0); + EXPECT_EQ("::1", std::string(address_buf.ptr, address_buf.length)); + EXPECT_EQ(12345, port); + + filter_->setCurrentDataForTest(nullptr); +} + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, GetPeerAddressNoCurrentData) { + envoy_dynamic_module_type_envoy_buffer address_buf = {nullptr, 0}; + uint32_t port = 0; + + bool ok = envoy_dynamic_module_callback_udp_listener_filter_get_peer_address(filterPtr(), + &address_buf, &port); + + EXPECT_FALSE(ok); +} + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, GetPeerAddressNullPeer) { + Network::UdpRecvData data; + data.buffer_ = std::make_unique("test"); + data.addresses_.peer_ = nullptr; + + filter_->setCurrentDataForTest(&data); + + envoy_dynamic_module_type_envoy_buffer address_buf = {nullptr, 0}; + uint32_t port = 0; + + bool ok = envoy_dynamic_module_callback_udp_listener_filter_get_peer_address(filterPtr(), + &address_buf, &port); + + EXPECT_FALSE(ok); + + filter_->setCurrentDataForTest(nullptr); +} + +// ============================================================================= +// Tests for get_local_address. +// ============================================================================= + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, GetLocalAddressFromRecvData) { + Network::UdpRecvData data; + data.buffer_ = std::make_unique("test"); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:1234"); + data.addresses_.local_ = Network::Utility::parseInternetAddressAndPortNoThrow("10.20.30.40:5555"); + + filter_->setCurrentDataForTest(&data); + + envoy_dynamic_module_type_envoy_buffer address_buf = {nullptr, 0}; + uint32_t port = 0; + + bool ok = envoy_dynamic_module_callback_udp_listener_filter_get_local_address( + filterPtr(), &address_buf, &port); + + EXPECT_TRUE(ok); + EXPECT_NE(nullptr, address_buf.ptr); + EXPECT_GT(address_buf.length, 0); + EXPECT_EQ("10.20.30.40", std::string(address_buf.ptr, address_buf.length)); + EXPECT_EQ(5555, port); + + filter_->setCurrentDataForTest(nullptr); +} + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, GetLocalAddressFromCallbacks) { + // No current data, should fall back to callbacks. + auto local_addr = Network::Utility::parseInternetAddressAndPortNoThrow("127.0.0.1:8888"); + auto mock_listener = std::make_shared>(); + EXPECT_CALL(*mock_listener, localAddress()).WillRepeatedly(testing::ReturnRef(local_addr)); + EXPECT_CALL(callbacks_, udpListener()).WillRepeatedly(testing::ReturnRef(*mock_listener)); + + envoy_dynamic_module_type_envoy_buffer address_buf = {nullptr, 0}; + uint32_t port = 0; + + bool ok = envoy_dynamic_module_callback_udp_listener_filter_get_local_address( + filterPtr(), &address_buf, &port); + + EXPECT_TRUE(ok); + EXPECT_NE(nullptr, address_buf.ptr); + EXPECT_EQ("127.0.0.1", std::string(address_buf.ptr, address_buf.length)); + EXPECT_EQ(8888, port); +} + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, GetLocalAddressIpv6) { + Network::UdpRecvData data; + data.buffer_ = std::make_unique("test"); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("[::1]:1234"); + data.addresses_.local_ = Network::Utility::parseInternetAddressAndPortNoThrow("[fe80::1]:9999"); + + filter_->setCurrentDataForTest(&data); + + envoy_dynamic_module_type_envoy_buffer address_buf = {nullptr, 0}; + uint32_t port = 0; + + bool ok = envoy_dynamic_module_callback_udp_listener_filter_get_local_address( + filterPtr(), &address_buf, &port); + + EXPECT_TRUE(ok); + EXPECT_NE(nullptr, address_buf.ptr); + EXPECT_EQ("fe80::1", std::string(address_buf.ptr, address_buf.length)); + EXPECT_EQ(9999, port); + + filter_->setCurrentDataForTest(nullptr); +} + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, GetLocalAddressNoData) { + // No current data, but callbacks will try to access udpListener. + // Mock a listener with a null/invalid address to test the error path. + auto local_addr = Network::Utility::parseInternetAddressAndPortNoThrow("pipe://test"); + auto mock_listener = std::make_shared>(); + EXPECT_CALL(*mock_listener, localAddress()).WillRepeatedly(testing::ReturnRef(local_addr)); + EXPECT_CALL(callbacks_, udpListener()).WillRepeatedly(testing::ReturnRef(*mock_listener)); + + envoy_dynamic_module_type_envoy_buffer address_buf = {nullptr, 0}; + uint32_t port = 0; + + bool ok = envoy_dynamic_module_callback_udp_listener_filter_get_local_address( + filterPtr(), &address_buf, &port); + + // Should return false since the address is not an IP address. + EXPECT_FALSE(ok); +} + +// ============================================================================= +// Tests for send_datagram. +// ============================================================================= + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, SendDatagramWithExplicitAddress) { + Network::UdpRecvData data; + data.buffer_ = std::make_unique("original"); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:1234"); + data.addresses_.local_ = Network::Utility::parseInternetAddressAndPortNoThrow("10.0.0.1:5000"); + + auto mock_listener = std::make_shared>(); + EXPECT_CALL(callbacks_, udpListener()).WillRepeatedly(testing::ReturnRef(*mock_listener)); + + // Expect send to be called with the right data. + EXPECT_CALL(*mock_listener, send(testing::_)) + .WillOnce(testing::Invoke([](const Network::UdpSendData& send_data) { + EXPECT_EQ("response data", send_data.buffer_.toString()); + EXPECT_EQ("192.168.1.1", send_data.peer_address_.ip()->addressAsString()); + EXPECT_EQ(7777, send_data.peer_address_.ip()->port()); + return Api::IoCallUint64Result(13, Api::IoError::none()); + })); + + filter_->setCurrentDataForTest(&data); + + const char* response = "response data"; + const char* peer_ip = "192.168.1.1"; + envoy_dynamic_module_type_module_buffer data_buf = {response, 13}; + envoy_dynamic_module_type_module_buffer peer_addr_buf = {peer_ip, 11}; + + EXPECT_TRUE(envoy_dynamic_module_callback_udp_listener_filter_send_datagram(filterPtr(), data_buf, + peer_addr_buf, 7777)); + + filter_->setCurrentDataForTest(nullptr); +} + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, SendDatagramToOriginalPeer) { + Network::UdpRecvData data; + data.buffer_ = std::make_unique("request"); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("9.8.7.6:4321"); + data.addresses_.local_ = Network::Utility::parseInternetAddressAndPortNoThrow("10.0.0.1:5000"); + + auto mock_listener = std::make_shared>(); + EXPECT_CALL(callbacks_, udpListener()).WillRepeatedly(testing::ReturnRef(*mock_listener)); + + EXPECT_CALL(*mock_listener, send(testing::_)) + .WillOnce(testing::Invoke([](const Network::UdpSendData& send_data) { + EXPECT_EQ("echo back", send_data.buffer_.toString()); + EXPECT_EQ("9.8.7.6", send_data.peer_address_.ip()->addressAsString()); + EXPECT_EQ(4321, send_data.peer_address_.ip()->port()); + return Api::IoCallUint64Result(9, Api::IoError::none()); + })); + + filter_->setCurrentDataForTest(&data); + + const char* response = "echo back"; + envoy_dynamic_module_type_module_buffer data_buf = {response, 9}; + envoy_dynamic_module_type_module_buffer peer_addr_buf = {nullptr, 0}; + + EXPECT_TRUE(envoy_dynamic_module_callback_udp_listener_filter_send_datagram(filterPtr(), data_buf, + peer_addr_buf, 0)); + + filter_->setCurrentDataForTest(nullptr); +} + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, SendDatagramEmptyData) { + Network::UdpRecvData data; + data.buffer_ = std::make_unique("test"); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:1234"); + data.addresses_.local_ = Network::Utility::parseInternetAddressAndPortNoThrow("10.0.0.1:5000"); + + auto mock_listener = std::make_shared>(); + EXPECT_CALL(callbacks_, udpListener()).WillRepeatedly(testing::ReturnRef(*mock_listener)); + + EXPECT_CALL(*mock_listener, send(testing::_)) + .WillOnce(testing::Invoke([](const Network::UdpSendData& send_data) { + EXPECT_EQ(0, send_data.buffer_.length()); + return Api::IoCallUint64Result(0, Api::IoError::none()); + })); + + filter_->setCurrentDataForTest(&data); + + envoy_dynamic_module_type_module_buffer data_buf = {nullptr, 0}; + envoy_dynamic_module_type_module_buffer peer_addr_buf = {nullptr, 0}; + + EXPECT_TRUE(envoy_dynamic_module_callback_udp_listener_filter_send_datagram(filterPtr(), data_buf, + peer_addr_buf, 0)); + + filter_->setCurrentDataForTest(nullptr); +} + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, SendDatagramInvalidPeerAddress) { + Network::UdpRecvData data; + data.buffer_ = std::make_unique("test"); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:1234"); + data.addresses_.local_ = Network::Utility::parseInternetAddressAndPortNoThrow("10.0.0.1:5000"); + + auto mock_listener = std::make_shared>(); + EXPECT_CALL(callbacks_, udpListener()).WillRepeatedly(testing::ReturnRef(*mock_listener)); + + // Should not call send since address is invalid. + EXPECT_CALL(*mock_listener, send(testing::_)).Times(0); + + filter_->setCurrentDataForTest(&data); + + const char* response = "data"; + const char* invalid_ip = "not.an.ip.address"; + envoy_dynamic_module_type_module_buffer data_buf = {response, 4}; + envoy_dynamic_module_type_module_buffer peer_addr_buf = {invalid_ip, 17}; + + EXPECT_FALSE(envoy_dynamic_module_callback_udp_listener_filter_send_datagram( + filterPtr(), data_buf, peer_addr_buf, 8888)); + + filter_->setCurrentDataForTest(nullptr); +} + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, SendDatagramNoPeerAddress) { + // No current data means no peer address to send to. + auto mock_listener = std::make_shared>(); + EXPECT_CALL(callbacks_, udpListener()).WillRepeatedly(testing::ReturnRef(*mock_listener)); + + EXPECT_CALL(*mock_listener, send(testing::_)).Times(0); + + const char* response = "data"; + envoy_dynamic_module_type_module_buffer data_buf = {response, 4}; + envoy_dynamic_module_type_module_buffer peer_addr_buf = {nullptr, 0}; + + EXPECT_FALSE(envoy_dynamic_module_callback_udp_listener_filter_send_datagram( + filterPtr(), data_buf, peer_addr_buf, 0)); +} + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, SendDatagramNoCallbacks) { + NiceMock local_callbacks; + auto filter_without_listener = + std::make_shared(local_callbacks, filter_config_); + + Network::UdpRecvData data; + data.buffer_ = std::make_unique("test"); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:1234"); + data.addresses_.local_ = Network::Utility::parseInternetAddressAndPortNoThrow("10.0.0.1:5000"); + + filter_without_listener->onData(data); + + const char* response = "data"; + envoy_dynamic_module_type_module_buffer data_buf = {response, 4}; + envoy_dynamic_module_type_module_buffer peer_addr_buf = {nullptr, 0}; + + // Should not crash. + EXPECT_FALSE(envoy_dynamic_module_callback_udp_listener_filter_send_datagram( + static_cast(filter_without_listener.get()), data_buf, peer_addr_buf, 0)); +} + +TEST_F(DynamicModuleUdpListenerFilterAbiCallbackTest, SendDatagramIpv6) { + Network::UdpRecvData data; + data.buffer_ = std::make_unique("test"); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("[::1]:1234"); + data.addresses_.local_ = Network::Utility::parseInternetAddressAndPortNoThrow("[::2]:5000"); + + auto mock_listener = std::make_shared>(); + EXPECT_CALL(callbacks_, udpListener()).WillRepeatedly(testing::ReturnRef(*mock_listener)); + + EXPECT_CALL(*mock_listener, send(testing::_)) + .WillOnce(testing::Invoke([](const Network::UdpSendData& send_data) { + EXPECT_EQ("ipv6 data", send_data.buffer_.toString()); + EXPECT_EQ("2001:db8::1", send_data.peer_address_.ip()->addressAsString()); + EXPECT_EQ(9999, send_data.peer_address_.ip()->port()); + return Api::IoCallUint64Result(9, Api::IoError::none()); + })); + + filter_->setCurrentDataForTest(&data); + + const char* response = "ipv6 data"; + const char* peer_ip = "2001:db8::1"; + envoy_dynamic_module_type_module_buffer data_buf = {response, 9}; + envoy_dynamic_module_type_module_buffer peer_addr_buf = {peer_ip, 11}; + + EXPECT_TRUE(envoy_dynamic_module_callback_udp_listener_filter_send_datagram(filterPtr(), data_buf, + peer_addr_buf, 9999)); + + filter_->setCurrentDataForTest(nullptr); +} + +} // namespace DynamicModules +} // namespace UdpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/dynamic_modules/udp/factory_test.cc b/test/extensions/dynamic_modules/udp/factory_test.cc new file mode 100644 index 0000000000000..9453d1973c1b1 --- /dev/null +++ b/test/extensions/dynamic_modules/udp/factory_test.cc @@ -0,0 +1,174 @@ +#include "source/extensions/filters/udp/dynamic_modules/factory.h" + +#include "test/extensions/dynamic_modules/util.h" +#include "test/mocks/server/listener_factory_context.h" +#include "test/test_common/utility.h" + +namespace Envoy { +namespace Extensions { +namespace UdpFilters { +namespace DynamicModules { + +class DynamicModuleUdpListenerFilterFactoryTest : public testing::Test { +public: + DynamicModuleUdpListenerFilterFactoryTest() { + std::string shared_object_path = + Extensions::DynamicModules::testSharedObjectPath("udp_no_op", "c"); + std::string shared_object_dir = + std::filesystem::path(shared_object_path).parent_path().string(); + TestEnvironment::setEnvVar("ENVOY_DYNAMIC_MODULES_SEARCH_PATH", shared_object_dir, 1); + } + + DynamicModuleUdpListenerFilterConfigFactory factory_; +}; + +TEST_F(DynamicModuleUdpListenerFilterFactoryTest, ValidConfig) { + NiceMock context; + const std::string yaml = R"EOF( +dynamic_module_config: + name: udp_no_op + do_not_close: true +filter_name: test_filter +filter_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: test_config +)EOF"; + + envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + auto callback = factory_.createFilterFactoryFromProto(proto_config, context); + + NiceMock filter_manager; + NiceMock read_callbacks; + + EXPECT_CALL(filter_manager, addReadFilter_(testing::_)); + callback(filter_manager, read_callbacks); +} + +TEST_F(DynamicModuleUdpListenerFilterFactoryTest, EmptyProto) { + auto proto = factory_.createEmptyConfigProto(); + EXPECT_NE(nullptr, proto); +} + +TEST_F(DynamicModuleUdpListenerFilterFactoryTest, FactoryName) { + EXPECT_EQ("envoy.filters.udp_listener.dynamic_modules", factory_.name()); +} + +TEST_F(DynamicModuleUdpListenerFilterFactoryTest, InvalidModulePath) { + NiceMock context; + + const std::string yaml = R"EOF( +dynamic_module_config: + name: nonexistent_module +filter_name: test_filter +)EOF"; + + envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + EXPECT_THROW_WITH_REGEX(factory_.createFilterFactoryFromProto(proto_config, context), + EnvoyException, "Failed to load.*"); +} + +TEST_F(DynamicModuleUdpListenerFilterFactoryTest, ModuleWithoutUdpSupport) { + NiceMock context; + + const std::string yaml = R"EOF( +dynamic_module_config: + name: no_op +filter_name: test_filter +)EOF"; + + envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + EXPECT_THROW_WITH_MESSAGE( + factory_.createFilterFactoryFromProto(proto_config, context), EnvoyException, + "Dynamic module does not support UDP listener filters: Failed to " + "resolve symbol envoy_dynamic_module_on_udp_listener_filter_config_new"); +} + +TEST_F(DynamicModuleUdpListenerFilterFactoryTest, MultipleFactoryCallsSameModule) { + NiceMock context; + + const std::string yaml = R"EOF( +dynamic_module_config: + name: udp_no_op + do_not_close: true +filter_name: test_filter +)EOF"; + + envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + auto callback1 = factory_.createFilterFactoryFromProto(proto_config, context); + auto callback2 = factory_.createFilterFactoryFromProto(proto_config, context); + + NiceMock filter_manager1; + NiceMock read_callbacks1; + EXPECT_CALL(filter_manager1, addReadFilter_(testing::_)); + callback1(filter_manager1, read_callbacks1); + + NiceMock filter_manager2; + NiceMock read_callbacks2; + EXPECT_CALL(filter_manager2, addReadFilter_(testing::_)); + callback2(filter_manager2, read_callbacks2); +} + +TEST_F(DynamicModuleUdpListenerFilterFactoryTest, ConfigWithBytesValue) { + NiceMock context; + + const std::string yaml = R"EOF( +dynamic_module_config: + name: udp_no_op + do_not_close: true +filter_name: test_filter +filter_config: + "@type": type.googleapis.com/google.protobuf.BytesValue + value: "aGVsbG8=" +)EOF"; + + envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + auto callback = factory_.createFilterFactoryFromProto(proto_config, context); + + NiceMock filter_manager; + NiceMock read_callbacks; + + EXPECT_CALL(filter_manager, addReadFilter_(testing::_)); + callback(filter_manager, read_callbacks); +} + +TEST_F(DynamicModuleUdpListenerFilterFactoryTest, ConfigWithStruct) { + NiceMock context; + + const std::string yaml = R"EOF( +dynamic_module_config: + name: udp_no_op + do_not_close: true +filter_name: test_filter +filter_config: + "@type": type.googleapis.com/google.protobuf.Struct + value: + key1: value1 + key2: 123 +)EOF"; + + envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + + auto callback = factory_.createFilterFactoryFromProto(proto_config, context); + + NiceMock filter_manager; + NiceMock read_callbacks; + + EXPECT_CALL(filter_manager, addReadFilter_(testing::_)); + callback(filter_manager, read_callbacks); +} + +} // namespace DynamicModules +} // namespace UdpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/dynamic_modules/udp/filter_test.cc b/test/extensions/dynamic_modules/udp/filter_test.cc new file mode 100644 index 0000000000000..f763c25c622cb --- /dev/null +++ b/test/extensions/dynamic_modules/udp/filter_test.cc @@ -0,0 +1,312 @@ +#include "source/extensions/dynamic_modules/abi.h" +#include "source/extensions/filters/udp/dynamic_modules/filter.h" + +#include "test/extensions/dynamic_modules/util.h" +#include "test/mocks/network/mocks.h" +#include "test/test_common/utility.h" + +namespace Envoy { +namespace Extensions { +namespace UdpFilters { +namespace DynamicModules { + +class DynamicModuleUdpListenerFilterTest : public testing::Test { +public: + void SetUp() override { + auto dynamic_module = Extensions::DynamicModules::newDynamicModule( + Extensions::DynamicModules::testSharedObjectPath("udp_no_op", "c"), false); + EXPECT_TRUE(dynamic_module.ok()) << dynamic_module.status().message(); + + envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter + proto_config; + proto_config.set_filter_name("test_filter"); + proto_config.mutable_filter_config()->set_value("some_config"); + + filter_config_ = std::make_shared( + proto_config, std::move(dynamic_module.value())); + } + + DynamicModuleUdpListenerFilterConfigSharedPtr filter_config_; +}; + +TEST_F(DynamicModuleUdpListenerFilterTest, BasicDataFlow) { + NiceMock callbacks; + auto filter = std::make_unique(callbacks, filter_config_); + + Network::UdpRecvData data; + data.buffer_ = std::make_unique("hello"); + // Set addresses to avoid null dereferences if ABI accesses them + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:1234"); + data.addresses_.local_ = Network::Utility::parseInternetAddressAndPortNoThrow("5.6.7.8:5678"); + + EXPECT_EQ(Network::FilterStatus::Continue, filter->onData(data)); + + // Verify buffer is cleared after callbacks. + EXPECT_EQ(nullptr, filter->currentData()); +} + +TEST_F(DynamicModuleUdpListenerFilterTest, ReceiveError) { + NiceMock callbacks; + auto filter = std::make_unique(callbacks, filter_config_); + + // Just check it doesn't crash + EXPECT_EQ(Network::FilterStatus::Continue, + filter->onReceiveError(Api::IoError::IoErrorCode::UnknownError)); +} + +TEST_F(DynamicModuleUdpListenerFilterTest, ConfigMissingSymbols) { + // Use the no_op module which lacks UDP symbols. + auto dynamic_module = Extensions::DynamicModules::newDynamicModule( + Extensions::DynamicModules::testSharedObjectPath("no_op", "c"), false); + EXPECT_TRUE(dynamic_module.ok()) << dynamic_module.status().message(); + + envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter proto_config; + proto_config.set_filter_name("test_filter"); + + EXPECT_THROW_WITH_MESSAGE( + std::make_shared(proto_config, + std::move(dynamic_module.value())), + EnvoyException, + "Dynamic module does not support UDP listener filters: Failed to resolve symbol " + "envoy_dynamic_module_on_udp_listener_filter_config_new"); +} + +TEST_F(DynamicModuleUdpListenerFilterTest, NullInModuleFilter) { + NiceMock callbacks; + + // Create a separate config that returns null from on_filter_new. + auto dynamic_module = Extensions::DynamicModules::newDynamicModule( + Extensions::DynamicModules::testSharedObjectPath("udp_no_op", "c"), false); + EXPECT_TRUE(dynamic_module.ok()) << dynamic_module.status().message(); + + envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter proto_config; + proto_config.set_filter_name("test_filter"); + proto_config.mutable_filter_config()->set_value("config"); + + auto bad_filter_config = std::make_shared( + proto_config, std::move(dynamic_module.value())); + + // Replace the on_filter_new function to return null. + auto null_returner = +[](envoy_dynamic_module_type_udp_listener_filter_config_module_ptr, + envoy_dynamic_module_type_udp_listener_filter_envoy_ptr) + -> envoy_dynamic_module_type_udp_listener_filter_module_ptr { return nullptr; }; + bad_filter_config->on_filter_new_ = null_returner; + + auto filter = std::make_unique(callbacks, bad_filter_config); + + Network::UdpRecvData data; + data.buffer_ = std::make_unique("test"); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:1234"); + + // Should return Continue when in_module_filter is null. + EXPECT_EQ(Network::FilterStatus::Continue, filter->onData(data)); +} + +TEST_F(DynamicModuleUdpListenerFilterTest, EmptyBuffer) { + NiceMock callbacks; + auto filter = std::make_unique(callbacks, filter_config_); + + Network::UdpRecvData data; + data.buffer_ = std::make_unique(); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:1234"); + + EXPECT_EQ(Network::FilterStatus::Continue, filter->onData(data)); + EXPECT_EQ(nullptr, filter->currentData()); +} + +TEST_F(DynamicModuleUdpListenerFilterTest, LargeDataPayload) { + NiceMock callbacks; + auto filter = std::make_unique(callbacks, filter_config_); + + std::string large_data(65000, 'x'); + Network::UdpRecvData data; + data.buffer_ = std::make_unique(large_data); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:1234"); + + EXPECT_EQ(Network::FilterStatus::Continue, filter->onData(data)); + EXPECT_EQ(nullptr, filter->currentData()); +} + +TEST_F(DynamicModuleUdpListenerFilterTest, MultipleReceiveErrors) { + NiceMock callbacks; + auto filter = std::make_unique(callbacks, filter_config_); + + EXPECT_EQ(Network::FilterStatus::Continue, + filter->onReceiveError(Api::IoError::IoErrorCode::NoSupport)); + EXPECT_EQ(Network::FilterStatus::Continue, + filter->onReceiveError(Api::IoError::IoErrorCode::Again)); + EXPECT_EQ(Network::FilterStatus::Continue, + filter->onReceiveError(Api::IoError::IoErrorCode::Permission)); +} + +TEST_F(DynamicModuleUdpListenerFilterTest, FilterConfigWithEmptyName) { + auto dynamic_module = Extensions::DynamicModules::newDynamicModule( + Extensions::DynamicModules::testSharedObjectPath("udp_no_op", "c"), false); + EXPECT_TRUE(dynamic_module.ok()) << dynamic_module.status().message(); + + envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter proto_config; + proto_config.set_filter_name(""); + proto_config.mutable_filter_config()->set_value("config"); + + auto config = std::make_shared( + proto_config, std::move(dynamic_module.value())); + EXPECT_EQ("", config->filter_name_); +} + +TEST_F(DynamicModuleUdpListenerFilterTest, FilterConfigWithNoConfig) { + auto dynamic_module = Extensions::DynamicModules::newDynamicModule( + Extensions::DynamicModules::testSharedObjectPath("udp_no_op", "c"), false); + EXPECT_TRUE(dynamic_module.ok()) << dynamic_module.status().message(); + + envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter proto_config; + proto_config.set_filter_name("test"); + // No filter_config set. + + auto config = std::make_shared( + proto_config, std::move(dynamic_module.value())); + EXPECT_FALSE(config->filter_config_.empty()); +} + +TEST_F(DynamicModuleUdpListenerFilterTest, MultipleFiltersShareConfig) { + NiceMock callbacks1; + NiceMock callbacks2; + + auto filter1 = std::make_unique(callbacks1, filter_config_); + auto filter2 = std::make_unique(callbacks2, filter_config_); + + Network::UdpRecvData data1; + data1.buffer_ = std::make_unique("data1"); + data1.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:1234"); + + Network::UdpRecvData data2; + data2.buffer_ = std::make_unique("data2"); + data2.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("5.6.7.8:5678"); + + EXPECT_EQ(Network::FilterStatus::Continue, filter1->onData(data1)); + EXPECT_EQ(Network::FilterStatus::Continue, filter2->onData(data2)); +} + +TEST_F(DynamicModuleUdpListenerFilterTest, CallbacksAccessor) { + NiceMock callbacks; + auto filter = std::make_unique(callbacks, filter_config_); + + EXPECT_EQ(&callbacks, filter->callbacks()); +} + +TEST_F(DynamicModuleUdpListenerFilterTest, CurrentDataAccessor) { + NiceMock callbacks; + auto filter = std::make_unique(callbacks, filter_config_); + + EXPECT_EQ(nullptr, filter->currentData()); + + Network::UdpRecvData data; + data.buffer_ = std::make_unique("test"); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:1234"); + + filter->onData(data); + EXPECT_EQ(nullptr, filter->currentData()); +} + +class DynamicModuleUdpListenerFilterStopIterationTest : public testing::Test { +public: + void SetUp() override { + auto dynamic_module = Extensions::DynamicModules::newDynamicModule( + Extensions::DynamicModules::testSharedObjectPath("udp_stop_iteration", "c"), false); + EXPECT_TRUE(dynamic_module.ok()) << dynamic_module.status().message(); + + envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter + proto_config; + proto_config.set_filter_name("stop_filter"); + proto_config.mutable_filter_config()->set_value("config"); + + filter_config_ = std::make_shared( + proto_config, std::move(dynamic_module.value())); + } + + DynamicModuleUdpListenerFilterConfigSharedPtr filter_config_; +}; + +TEST_F(DynamicModuleUdpListenerFilterStopIterationTest, ReturnsStopIteration) { + NiceMock callbacks; + auto filter = std::make_unique(callbacks, filter_config_); + + Network::UdpRecvData data; + data.buffer_ = std::make_unique("test"); + data.addresses_.peer_ = Network::Utility::parseInternetAddressAndPortNoThrow("1.2.3.4:1234"); + + EXPECT_EQ(Network::FilterStatus::StopIteration, filter->onData(data)); +} + +// Test for missing config_destroy symbol. +TEST(DynamicModuleUdpListenerFilterConfigErrorTest, MissingConfigDestroy) { + auto dynamic_module = Extensions::DynamicModules::newDynamicModule( + Extensions::DynamicModules::testSharedObjectPath("udp_no_config_destroy", "c"), false); + EXPECT_TRUE(dynamic_module.ok()) << dynamic_module.status().message(); + + envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter proto_config; + proto_config.set_filter_name("test"); + proto_config.mutable_filter_config()->set_value("config"); + + EXPECT_THROW_WITH_MESSAGE( + std::make_shared(proto_config, + std::move(dynamic_module.value())), + EnvoyException, + "Dynamic module does not support UDP listener filters: Failed to resolve symbol " + "envoy_dynamic_module_on_udp_listener_filter_config_destroy"); +} + +// Test for missing filter_new symbol. +TEST(DynamicModuleUdpListenerFilterConfigErrorTest, MissingFilterNew) { + auto dynamic_module = Extensions::DynamicModules::newDynamicModule( + Extensions::DynamicModules::testSharedObjectPath("udp_no_filter_new", "c"), false); + EXPECT_TRUE(dynamic_module.ok()) << dynamic_module.status().message(); + + envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter proto_config; + proto_config.set_filter_name("test"); + + EXPECT_THROW_WITH_MESSAGE( + std::make_shared(proto_config, + std::move(dynamic_module.value())), + EnvoyException, + "Dynamic module does not support UDP listener filters: Failed to resolve symbol " + "envoy_dynamic_module_on_udp_listener_filter_new"); +} + +// Test for missing on_data symbol. +TEST(DynamicModuleUdpListenerFilterConfigErrorTest, MissingOnData) { + auto dynamic_module = Extensions::DynamicModules::newDynamicModule( + Extensions::DynamicModules::testSharedObjectPath("udp_no_on_data", "c"), false); + EXPECT_TRUE(dynamic_module.ok()) << dynamic_module.status().message(); + + envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter proto_config; + proto_config.set_filter_name("test"); + + EXPECT_THROW_WITH_MESSAGE( + std::make_shared(proto_config, + std::move(dynamic_module.value())), + EnvoyException, + "Dynamic module does not support UDP listener filters: Failed to resolve symbol " + "envoy_dynamic_module_on_udp_listener_filter_on_data"); +} + +// Test for missing filter_destroy symbol. +TEST(DynamicModuleUdpListenerFilterConfigErrorTest, MissingFilterDestroy) { + auto dynamic_module = Extensions::DynamicModules::newDynamicModule( + Extensions::DynamicModules::testSharedObjectPath("udp_no_filter_destroy", "c"), false); + EXPECT_TRUE(dynamic_module.ok()) << dynamic_module.status().message(); + + envoy::extensions::filters::udp::dynamic_modules::v3::DynamicModuleUdpListenerFilter proto_config; + proto_config.set_filter_name("test"); + + EXPECT_THROW_WITH_MESSAGE( + std::make_shared(proto_config, + std::move(dynamic_module.value())), + EnvoyException, + "Dynamic module does not support UDP listener filters: Failed to resolve symbol " + "envoy_dynamic_module_on_udp_listener_filter_destroy"); +} + +} // namespace DynamicModules +} // namespace UdpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/dynamic_modules/udp/udp_dynamic_modules_integration_test.cc b/test/extensions/dynamic_modules/udp/udp_dynamic_modules_integration_test.cc new file mode 100644 index 0000000000000..c34e91a477f99 --- /dev/null +++ b/test/extensions/dynamic_modules/udp/udp_dynamic_modules_integration_test.cc @@ -0,0 +1,152 @@ +#include "envoy/config/bootstrap/v3/bootstrap.pb.h" +#include "envoy/extensions/filters/udp/dynamic_modules/v3/dynamic_modules.pb.h" +#include "envoy/extensions/filters/udp/udp_proxy/v3/udp_proxy.pb.h" + +#include "test/extensions/dynamic_modules/util.h" +#include "test/integration/integration.h" +#include "test/test_common/network_utility.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace UdpFilters { +namespace DynamicModules { +namespace { + +class UdpDynamicModulesIntegrationTest : public testing::TestWithParam, + public BaseIntegrationTest { +public: + UdpDynamicModulesIntegrationTest() + : BaseIntegrationTest(GetParam(), ConfigHelper::baseUdpListenerConfig()) {} + + void SetUp() override { + // The shared object is created by the build system. + // We need to set the DYNAMIC_MODULES_SEARCH_PATH to the location of the shared object. + std::string shared_object_path = + Extensions::DynamicModules::testSharedObjectPath("udp_no_op", "c"); + std::string shared_object_dir = + std::filesystem::path(shared_object_path).parent_path().string(); + TestEnvironment::setEnvVar("ENVOY_DYNAMIC_MODULES_SEARCH_PATH", shared_object_dir, 1); + } + + void setup(const std::string& module_name = "udp_no_op") { + FakeUpstreamConfig::UdpConfig config; + setUdpFakeUpstream(config); + + const std::string filter_config = fmt::format(R"EOF( +name: envoy.filters.udp_listener.dynamic_modules +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.udp.dynamic_modules.v3.DynamicModuleUdpListenerFilter + dynamic_module_config: + name: "{}" + do_not_close: true + filter_name: "test_filter" + filter_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: "some_config" +)EOF", + module_name); + + config_helper_.addListenerFilter(filter_config); + + config_helper_.addListenerFilter(R"EOF( +name: envoy.filters.udp_listener.udp_proxy +typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.UdpProxyConfig + stat_prefix: service + matcher: + on_no_match: + action: + name: route + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.Route + cluster: cluster_0 +)EOF"); + + BaseIntegrationTest::initialize(); + } +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, UdpDynamicModulesIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +TEST_P(UdpDynamicModulesIntegrationTest, BasicDataFlow) { + setup(); + + const uint32_t port = lookupPort("listener_0"); + const auto listener_address = *Network::Utility::resolveUrl( + fmt::format("tcp://{}:{}", Network::Test::getLoopbackAddressUrlString(GetParam()), port)); + + std::string request = "hello"; + Network::Test::UdpSyncPeer client(GetParam()); + client.write(request, *listener_address); + + Network::UdpRecvData request_datagram; + ASSERT_TRUE(fake_upstreams_[0]->waitForUdpDatagram(request_datagram)); + EXPECT_EQ(request, request_datagram.buffer_->toString()); +} + +TEST_P(UdpDynamicModulesIntegrationTest, StopIteration) { + setup("udp_stop_iteration"); + + const uint32_t port = lookupPort("listener_0"); + const auto listener_address = *Network::Utility::resolveUrl( + fmt::format("tcp://{}:{}", Network::Test::getLoopbackAddressUrlString(GetParam()), port)); + + std::string request = "should be blocked"; + Network::Test::UdpSyncPeer client(GetParam()); + client.write(request, *listener_address); + + Network::UdpRecvData request_datagram; + // UDP listener filter StopIteration is currently not enforced in the fake upstream path. + // We verify that the datagram still arrives. When StopIteration is enforced in the future, + // this expectation can be flipped. + EXPECT_TRUE(fake_upstreams_[0]->waitForUdpDatagram(request_datagram, std::chrono::seconds(1))); + EXPECT_EQ(request, request_datagram.buffer_->toString()); +} + +TEST_P(UdpDynamicModulesIntegrationTest, LargePayload) { + setup(); + + const uint32_t port = lookupPort("listener_0"); + const auto listener_address = *Network::Utility::resolveUrl( + fmt::format("tcp://{}:{}", Network::Test::getLoopbackAddressUrlString(GetParam()), port)); + + // Use a conservative payload size to avoid platform-specific UDP limits. + std::string large_request(512, 'x'); + Network::Test::UdpSyncPeer client(GetParam()); + client.write(large_request, *listener_address); + + Network::UdpRecvData request_datagram; + ASSERT_TRUE(fake_upstreams_[0]->waitForUdpDatagram(request_datagram)); + EXPECT_EQ(large_request, request_datagram.buffer_->toString()); +} + +TEST_P(UdpDynamicModulesIntegrationTest, MultipleDatagrams) { + setup(); + + const uint32_t port = lookupPort("listener_0"); + const auto listener_address = *Network::Utility::resolveUrl( + fmt::format("tcp://{}:{}", Network::Test::getLoopbackAddressUrlString(GetParam()), port)); + + Network::Test::UdpSyncPeer client(GetParam()); + + // Send multiple datagrams. + for (int i = 0; i < 5; i++) { + std::string request = fmt::format("datagram_{}", i); + client.write(request, *listener_address); + + Network::UdpRecvData request_datagram; + ASSERT_TRUE(fake_upstreams_[0]->waitForUdpDatagram(request_datagram)); + EXPECT_EQ(request, request_datagram.buffer_->toString()); + } +} + +} // namespace +} // namespace DynamicModules +} // namespace UdpFilters +} // namespace Extensions +} // namespace Envoy