diff --git a/source/extensions/dynamic_modules/abi.h b/source/extensions/dynamic_modules/abi.h index e918767ef4411..35f5e01572fa2 100644 --- a/source/extensions/dynamic_modules/abi.h +++ b/source/extensions/dynamic_modules/abi.h @@ -1198,6 +1198,156 @@ void envoy_dynamic_module_on_network_filter_event( void envoy_dynamic_module_on_network_filter_destroy( envoy_dynamic_module_type_network_filter_module_ptr filter_module_ptr); +/** + * envoy_dynamic_module_on_network_filter_http_callout_done is called when the HTTP callout + * response is received initiated by a network filter. + * + * @param filter_envoy_ptr is the pointer to the DynamicModuleNetworkFilter object of the + * corresponding network filter. + * @param filter_module_ptr is the pointer to the in-module network filter created by + * envoy_dynamic_module_on_network_filter_new. + * @param callout_id is the ID of the callout. This is used to differentiate between multiple + * calls. + * @param result is the result of the callout. + * @param headers is the headers of the response. + * @param headers_size is the size of the headers. + * @param body_chunks is the body of the response. + * @param body_chunks_size is the size of the body. + * + * headers and body_chunks are owned by Envoy, and they are guaranteed to be valid until the end of + * this event hook. They may be null if the callout fails or the response is empty. + */ +void envoy_dynamic_module_on_network_filter_http_callout_done( + envoy_dynamic_module_type_network_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_network_filter_module_ptr filter_module_ptr, uint64_t callout_id, + envoy_dynamic_module_type_http_callout_result result, + envoy_dynamic_module_type_envoy_http_header* headers, size_t headers_size, + envoy_dynamic_module_type_envoy_buffer* body_chunks, size_t body_chunks_size); + +// ----------------------------------------------------------------------------- +// Socket Options +// ----------------------------------------------------------------------------- + +/** + * envoy_dynamic_module_type_socket_option_state represents the socket state at which an option + * should be applied. + */ +typedef enum envoy_dynamic_module_type_socket_option_state { + envoy_dynamic_module_type_socket_option_state_Prebind = 0, + envoy_dynamic_module_type_socket_option_state_Bound = 1, + envoy_dynamic_module_type_socket_option_state_Listening = 2, +} envoy_dynamic_module_type_socket_option_state; + +/** + * envoy_dynamic_module_type_socket_option_value_type represents the type of value stored in a + * socket option. + */ +typedef enum envoy_dynamic_module_type_socket_option_value_type { + envoy_dynamic_module_type_socket_option_value_type_Int = 0, + envoy_dynamic_module_type_socket_option_value_type_Bytes = 1, +} envoy_dynamic_module_type_socket_option_value_type; + +/** + * envoy_dynamic_module_type_socket_option represents a socket option with its level, name, state, + * and value. The value can be either an integer or bytes depending on value_type. + */ +typedef struct envoy_dynamic_module_type_socket_option { + int64_t level; + int64_t name; + envoy_dynamic_module_type_socket_option_state state; + envoy_dynamic_module_type_socket_option_value_type value_type; + int64_t int_value; + envoy_dynamic_module_type_envoy_buffer byte_value; +} envoy_dynamic_module_type_socket_option; + +/** + * envoy_dynamic_module_callback_network_set_socket_option_int sets an integer socket option with + * the given level, name, and state. + * + * @param filter_envoy_ptr is the pointer to the DynamicModuleNetworkFilter object. + * @param level is the socket option level (e.g., SOL_SOCKET). + * @param name is the socket option name (e.g., SO_KEEPALIVE). + * @param state is the socket state at which this option should be applied. + * @param value is the integer value for the socket option. + * @return true if the operation is successful, false otherwise. + */ +bool envoy_dynamic_module_callback_network_set_socket_option_int( + envoy_dynamic_module_type_network_filter_envoy_ptr filter_envoy_ptr, int64_t level, + int64_t name, envoy_dynamic_module_type_socket_option_state state, int64_t value); + +/** + * envoy_dynamic_module_callback_network_set_socket_option_bytes sets a bytes socket option with + * the given level, name, and state. + * + * @param filter_envoy_ptr is the pointer to the DynamicModuleNetworkFilter object. + * @param level is the socket option level. + * @param name is the socket option name. + * @param state is the socket state at which this option should be applied. + * @param value is the byte buffer value for the socket option. + * @return true if the operation is successful, false otherwise. + */ +bool envoy_dynamic_module_callback_network_set_socket_option_bytes( + envoy_dynamic_module_type_network_filter_envoy_ptr filter_envoy_ptr, int64_t level, + int64_t name, envoy_dynamic_module_type_socket_option_state state, + envoy_dynamic_module_type_module_buffer value); + +/** + * envoy_dynamic_module_callback_network_get_socket_option_int retrieves an integer socket option + * value. + * + * @param filter_envoy_ptr is the pointer to the DynamicModuleNetworkFilter object. + * @param level is the socket option level. + * @param name is the socket option name. + * @param state is the socket state. + * @param value_out is the pointer to store the retrieved integer value. + * @return true if the option is found, false otherwise. + */ +bool envoy_dynamic_module_callback_network_get_socket_option_int( + envoy_dynamic_module_type_network_filter_envoy_ptr filter_envoy_ptr, int64_t level, + int64_t name, envoy_dynamic_module_type_socket_option_state state, int64_t* value_out); + +/** + * envoy_dynamic_module_callback_network_get_socket_option_bytes retrieves a bytes socket option + * value. + * + * @param filter_envoy_ptr is the pointer to the DynamicModuleNetworkFilter object. + * @param level is the socket option level. + * @param name is the socket option name. + * @param state is the socket state. + * @param value_out is the pointer to store the retrieved buffer. The buffer is owned by Envoy and + * valid until the filter is destroyed. + * @return true if the option is found, false otherwise. + */ +bool envoy_dynamic_module_callback_network_get_socket_option_bytes( + envoy_dynamic_module_type_network_filter_envoy_ptr filter_envoy_ptr, int64_t level, + int64_t name, envoy_dynamic_module_type_socket_option_state state, + envoy_dynamic_module_type_envoy_buffer* value_out); + +/** + * envoy_dynamic_module_callback_network_get_socket_options_size returns the number of socket + * options stored on the connection. + * + * @param filter_envoy_ptr is the pointer to the DynamicModuleNetworkFilter object. + * @return the number of socket options. + */ +size_t envoy_dynamic_module_callback_network_get_socket_options_size( + envoy_dynamic_module_type_network_filter_envoy_ptr filter_envoy_ptr); + +/** + * envoy_dynamic_module_callback_network_get_socket_options gets all socket options stored on the + * connection. The caller should first call + * envoy_dynamic_module_callback_network_get_socket_options_size to get the size, allocate an array + * of that size, and pass the pointer to this function. + * + * @param filter_envoy_ptr is the pointer to the DynamicModuleNetworkFilter object. + * @param options_out is the pointer to an array of socket options that will be filled. The array + * must be pre-allocated by the caller with size equal to the value returned by + * envoy_dynamic_module_callback_network_get_socket_options_size. + */ +void envoy_dynamic_module_callback_network_get_socket_options( + envoy_dynamic_module_type_network_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_socket_option* options_out); + // ============================================================================= // ------------------------------ Listener Filter Event Hooks ------------------ // ============================================================================= @@ -2741,6 +2891,34 @@ bool envoy_dynamic_module_callback_network_get_dynamic_metadata_number( envoy_dynamic_module_type_module_buffer filter_namespace, envoy_dynamic_module_type_module_buffer key, double* result); +// ----------------------------------------------------------------------------- +// HTTP Callouts +// ----------------------------------------------------------------------------- + +/** + * envoy_dynamic_module_callback_network_filter_http_callout is called by the module to initiate an + * HTTP callout. The callout is initiated by the network filter and the response is received in + * envoy_dynamic_module_on_network_filter_http_callout_done. + * + * @param filter_envoy_ptr is the pointer to the DynamicModuleNetworkFilter object of the + * corresponding network filter. + * @param callout_id_out is a pointer to a variable where the callout ID will be stored. This can be + * arbitrary and is used to differentiate between multiple calls from the same filter. + * @param cluster_name is the name of the cluster to which the callout is sent. + * @param headers is the headers of the request. It must contain :method, :path and host headers. + * @param headers_size is the size of the headers. + * @param body is the body of the request. + * @param timeout_milliseconds is the timeout for the callout in milliseconds. + * @return envoy_dynamic_module_type_http_callout_init_result is the result of the callout + * initialization. + */ +envoy_dynamic_module_type_http_callout_init_result +envoy_dynamic_module_callback_network_filter_http_callout( + envoy_dynamic_module_type_network_filter_envoy_ptr filter_envoy_ptr, uint64_t* callout_id_out, + envoy_dynamic_module_type_module_buffer cluster_name, + envoy_dynamic_module_type_module_http_header* headers, size_t headers_size, + envoy_dynamic_module_type_module_buffer body, uint64_t timeout_milliseconds); + // ============================================================================= // ----------------------------- Listener Filter Callbacks --------------------- // ============================================================================= diff --git a/source/extensions/dynamic_modules/sdk/rust/src/lib.rs b/source/extensions/dynamic_modules/sdk/rust/src/lib.rs index c3d5ad0455316..62c428f3928b9 100644 --- a/source/extensions/dynamic_modules/sdk/rust/src/lib.rs +++ b/source/extensions/dynamic_modules/sdk/rust/src/lib.rs @@ -2,6 +2,7 @@ #![allow(non_camel_case_types)] #![allow(non_snake_case)] #![allow(dead_code)] +#![allow(clippy::unnecessary_cast)] pub mod buffer; pub use buffer::{EnvoyBuffer, EnvoyMutBuffer}; @@ -3167,6 +3168,22 @@ pub trait NetworkFilter { ) { } + /// This is called when an HTTP callout response is received. + /// + /// @param _callout_id is the ID of the callout returned by send_http_callout. + /// @param _result is the result of the callout. + /// @param _headers is the headers of the response. Empty if the callout failed. + /// @param _body_chunks is the body chunks of the response. Empty if the callout failed. + fn on_http_callout_done( + &mut self, + _envoy_filter: &mut ENF, + _callout_id: u64, + _result: abi::envoy_dynamic_module_type_http_callout_result, + _headers: Vec<(EnvoyBuffer, EnvoyBuffer)>, + _body_chunks: Vec, + ) { + } + /// This is called when the network filter is destroyed for each TCP connection. fn on_destroy(&mut self, _envoy_filter: &mut ENF) {} } @@ -3237,23 +3254,23 @@ pub trait EnvoyNetworkFilter { /// Get the requested server name (SNI). /// Returns None if SNI is not available. - fn get_requested_server_name(&self) -> Option; + fn get_requested_server_name(&self) -> Option>; /// Get the direct remote (client) address and port without considering proxy protocol. /// Returns None if the address is not available or not an IP address. - fn get_direct_remote_address(&self) -> Option<(String, u32)>; + fn get_direct_remote_address(&self) -> Option<(EnvoyBuffer<'_>, u32)>; /// Get the SSL URI SANs from the peer certificate. /// Returns an empty vector if the connection is not SSL or no URI SANs are present. - fn get_ssl_uri_sans(&self) -> Vec; + fn get_ssl_uri_sans(&self) -> Vec>; /// Get the SSL DNS SANs from the peer certificate. /// Returns an empty vector if the connection is not SSL or no DNS SANs are present. - fn get_ssl_dns_sans(&self) -> Vec; + fn get_ssl_dns_sans(&self) -> Vec>; /// Get the SSL subject from the peer certificate. /// Returns None if the connection is not SSL or subject is not available. - fn get_ssl_subject(&self) -> Option; + fn get_ssl_subject(&self) -> Option>; /// Set the filter state with the given key and byte value. /// Returns true if the operation is successful. @@ -3278,6 +3295,85 @@ pub trait EnvoyNetworkFilter { /// Get the number-typed dynamic metadata value with the given namespace and key value. /// Returns None if the metadata is not found or is the wrong type. fn get_dynamic_metadata_number(&self, namespace: &str, key: &str) -> Option; + + /// Set an integer socket option with the given level, name, and state. + fn set_socket_option_int( + &mut self, + level: i64, + name: i64, + state: abi::envoy_dynamic_module_type_socket_option_state, + value: i64, + ) -> bool; + + /// Set a bytes socket option with the given level, name, and state. + fn set_socket_option_bytes( + &mut self, + level: i64, + name: i64, + state: abi::envoy_dynamic_module_type_socket_option_state, + value: &[u8], + ) -> bool; + + /// Get an integer socket option value. + fn get_socket_option_int( + &self, + level: i64, + name: i64, + state: abi::envoy_dynamic_module_type_socket_option_state, + ) -> Option; + + /// Get a bytes socket option value. + fn get_socket_option_bytes( + &self, + level: i64, + name: i64, + state: abi::envoy_dynamic_module_type_socket_option_state, + ) -> Option>; + + /// List all socket options stored on the connection. + fn get_socket_options(&self) -> Vec; + + /// Send an HTTP callout to the given cluster with the given headers and body. + /// Multiple callouts can be made from the same filter. Different callouts can be + /// distinguished by the returned callout id. + /// + /// Headers must contain the `:method`, ":path", and `host` headers. + /// + /// This returns the status and callout id of the callout. The id is used to + /// distinguish different callouts made from the same filter and is generated by Envoy. + /// The meaning of the status is: + /// + /// * Success: The callout was sent successfully. + /// * MissingRequiredHeaders: One of the required headers is missing: `:method`, `:path`, or + /// `host`. + /// * ClusterNotFound: The cluster with the given name was not found. + /// * DuplicateCalloutId: The callout ID is already in use. + /// * CannotCreateRequest: The request could not be created. This happens when, for example, + /// there's no healthy upstream host in the cluster. + /// + /// The callout result will be delivered to the [`NetworkFilter::on_http_callout_done`] method. + fn send_http_callout<'a>( + &mut self, + _cluster_name: &'a str, + _headers: Vec<(&'a str, &'a [u8])>, + _body: Option<&'a [u8]>, + _timeout_milliseconds: u64, + ) -> ( + abi::envoy_dynamic_module_type_http_callout_init_result, + u64, // callout handle + ); +} + +pub enum SocketOptionValue { + Int(i64), + Bytes(Vec), +} + +pub struct SocketOption { + pub level: i64, + pub name: i64, + pub state: abi::envoy_dynamic_module_type_socket_option_state, + pub value: SocketOptionValue, } /// The implementation of [`EnvoyNetworkFilterConfig`] for the Envoy network filter configuration. @@ -3543,7 +3639,7 @@ impl EnvoyNetworkFilter for EnvoyNetworkFilterImpl { } } - fn get_requested_server_name(&self) -> Option { + fn get_requested_server_name(&self) -> Option { let mut result = abi::envoy_dynamic_module_type_envoy_buffer { ptr: std::ptr::null(), length: 0, @@ -3555,19 +3651,13 @@ impl EnvoyNetworkFilter for EnvoyNetworkFilterImpl { ) }; if success && !result.ptr.is_null() && result.length > 0 { - let sni_str = unsafe { - std::str::from_utf8_unchecked(std::slice::from_raw_parts( - result.ptr as *const _, - result.length, - )) - }; - Some(sni_str.to_string()) + Some(unsafe { EnvoyBuffer::new_from_raw(result.ptr as *const _, result.length) }) } else { None } } - fn get_direct_remote_address(&self) -> Option<(String, u32)> { + fn get_direct_remote_address(&self) -> Option<(EnvoyBuffer, u32)> { let mut address = abi::envoy_dynamic_module_type_envoy_buffer { ptr: std::ptr::null(), length: 0, @@ -3583,16 +3673,13 @@ impl EnvoyNetworkFilter for EnvoyNetworkFilterImpl { if !result || address.length == 0 || address.ptr.is_null() { return None; } - let address_str = unsafe { - std::str::from_utf8_unchecked(std::slice::from_raw_parts( - address.ptr as *const _, - address.length, - )) - }; - Some((address_str.to_string(), port)) + Some(( + unsafe { EnvoyBuffer::new_from_raw(address.ptr as *const _, address.length) }, + port, + )) } - fn get_ssl_uri_sans(&self) -> Vec { + fn get_ssl_uri_sans(&self) -> Vec { let mut size: usize = 0; let success = unsafe { abi::envoy_dynamic_module_callback_network_filter_get_ssl_uri_sans_size(self.raw, &mut size) @@ -3623,21 +3710,15 @@ impl EnvoyNetworkFilter for EnvoyNetworkFilterImpl { .take(count) .map(|buf| { if !buf.ptr.is_null() && buf.length > 0 { - unsafe { - std::str::from_utf8_unchecked(std::slice::from_raw_parts( - buf.ptr as *const _, - buf.length, - )) - .to_string() - } + unsafe { EnvoyBuffer::new_from_raw(buf.ptr as *const _, buf.length) } } else { - String::new() + EnvoyBuffer::default() } }) .collect() } - fn get_ssl_dns_sans(&self) -> Vec { + fn get_ssl_dns_sans(&self) -> Vec { let mut size: usize = 0; let success = unsafe { abi::envoy_dynamic_module_callback_network_filter_get_ssl_dns_sans_size(self.raw, &mut size) @@ -3668,21 +3749,15 @@ impl EnvoyNetworkFilter for EnvoyNetworkFilterImpl { .take(count) .map(|buf| { if !buf.ptr.is_null() && buf.length > 0 { - unsafe { - std::str::from_utf8_unchecked(std::slice::from_raw_parts( - buf.ptr as *const _, - buf.length, - )) - .to_string() - } + unsafe { EnvoyBuffer::new_from_raw(buf.ptr as *const _, buf.length) } } else { - String::new() + EnvoyBuffer::default() } }) .collect() } - fn get_ssl_subject(&self) -> Option { + fn get_ssl_subject(&self) -> Option { let mut result = abi::envoy_dynamic_module_type_envoy_buffer { ptr: std::ptr::null(), length: 0, @@ -3694,13 +3769,7 @@ impl EnvoyNetworkFilter for EnvoyNetworkFilterImpl { ) }; if success && !result.ptr.is_null() && result.length > 0 { - let subject_str = unsafe { - std::str::from_utf8_unchecked(std::slice::from_raw_parts( - result.ptr as *const _, - result.length, - )) - }; - Some(subject_str.to_string()) + Some(unsafe { EnvoyBuffer::new_from_raw(result.ptr as *const _, result.length) }) } else { None } @@ -3802,6 +3871,186 @@ impl EnvoyNetworkFilter for EnvoyNetworkFilterImpl { None } } + + fn set_socket_option_int( + &mut self, + level: i64, + name: i64, + state: abi::envoy_dynamic_module_type_socket_option_state, + value: i64, + ) -> bool { + unsafe { + abi::envoy_dynamic_module_callback_network_set_socket_option_int( + self.raw, level, name, state, value, + ) + } + } + + fn set_socket_option_bytes( + &mut self, + level: i64, + name: i64, + state: abi::envoy_dynamic_module_type_socket_option_state, + value: &[u8], + ) -> bool { + unsafe { + abi::envoy_dynamic_module_callback_network_set_socket_option_bytes( + self.raw, + level, + name, + state, + abi::envoy_dynamic_module_type_module_buffer { + ptr: value.as_ptr() as *const _, + length: value.len(), + }, + ) + } + } + + fn get_socket_option_int( + &self, + level: i64, + name: i64, + state: abi::envoy_dynamic_module_type_socket_option_state, + ) -> Option { + let mut value: i64 = 0; + let success = unsafe { + abi::envoy_dynamic_module_callback_network_get_socket_option_int( + self.raw, level, name, state, &mut value, + ) + }; + if success { + Some(value) + } else { + None + } + } + + fn get_socket_option_bytes( + &self, + level: i64, + name: i64, + state: abi::envoy_dynamic_module_type_socket_option_state, + ) -> Option> { + let mut result = abi::envoy_dynamic_module_type_envoy_buffer { + ptr: std::ptr::null(), + length: 0, + }; + let success = unsafe { + abi::envoy_dynamic_module_callback_network_get_socket_option_bytes( + self.raw, + level, + name, + state, + &mut result as *mut _ as *mut _, + ) + }; + if success && !result.ptr.is_null() && result.length > 0 { + let slice = unsafe { std::slice::from_raw_parts(result.ptr as *const u8, result.length) }; + Some(slice.to_vec()) + } else { + None + } + } + + fn get_socket_options(&self) -> Vec { + let size = + unsafe { abi::envoy_dynamic_module_callback_network_get_socket_options_size(self.raw) }; + if size == 0 { + return Vec::new(); + } + let mut options: Vec = vec![ + abi::envoy_dynamic_module_type_socket_option { + level: 0, + name: 0, + state: abi::envoy_dynamic_module_type_socket_option_state::Prebind, + value_type: abi::envoy_dynamic_module_type_socket_option_value_type::Int, + int_value: 0, + byte_value: abi::envoy_dynamic_module_type_envoy_buffer { + ptr: std::ptr::null(), + length: 0, + }, + }; + size + ]; + unsafe { + abi::envoy_dynamic_module_callback_network_get_socket_options(self.raw, options.as_mut_ptr()) + }; + + options + .into_iter() + .map(|opt| { + let value = match opt.value_type { + abi::envoy_dynamic_module_type_socket_option_value_type::Int => { + SocketOptionValue::Int(opt.int_value) + }, + abi::envoy_dynamic_module_type_socket_option_value_type::Bytes => { + if !opt.byte_value.ptr.is_null() && opt.byte_value.length > 0 { + let bytes = unsafe { + std::slice::from_raw_parts(opt.byte_value.ptr as *const u8, opt.byte_value.length) + .to_vec() + }; + SocketOptionValue::Bytes(bytes) + } else { + SocketOptionValue::Bytes(Vec::new()) + } + }, + }; + SocketOption { + level: opt.level, + name: opt.name, + state: opt.state, + value, + } + }) + .collect() + } + + fn send_http_callout<'a>( + &mut self, + cluster_name: &'a str, + headers: Vec<(&'a str, &'a [u8])>, + body: Option<&'a [u8]>, + timeout_milliseconds: u64, + ) -> (abi::envoy_dynamic_module_type_http_callout_init_result, u64) { + let mut callout_id: u64 = 0; + + // Convert headers to module HTTP headers. + let module_headers: Vec = headers + .iter() + .map(|(k, v)| abi::envoy_dynamic_module_type_module_http_header { + key_ptr: k.as_ptr() as *const _, + key_length: k.len(), + value_ptr: v.as_ptr() as *const _, + value_length: v.len(), + }) + .collect(); + + let body_buffer = match body { + Some(b) => abi::envoy_dynamic_module_type_module_buffer { + ptr: b.as_ptr() as *const _, + length: b.len(), + }, + None => abi::envoy_dynamic_module_type_module_buffer { + ptr: std::ptr::null(), + length: 0, + }, + }; + + let result = unsafe { + abi::envoy_dynamic_module_callback_network_filter_http_callout( + self.raw, + &mut callout_id, + str_to_module_buffer(cluster_name), + module_headers.as_ptr() as *mut _, + module_headers.len(), + body_buffer, + timeout_milliseconds, + ) + }; + + (result, callout_id) + } } // Network Filter Event Hook Implementations @@ -3934,6 +4183,59 @@ pub extern "C" fn envoy_dynamic_module_on_network_filter_destroy( unsafe { Box::from_raw(filter_ptr as *mut Box>) }; } +#[no_mangle] +/// # Safety +/// Caller must ensure `filter_ptr`, `headers`, and `body_chunks` point to valid memory for the +/// provided sizes, and that the pointed-to data lives for the duration of this call. +pub unsafe extern "C" fn envoy_dynamic_module_on_network_filter_http_callout_done( + envoy_ptr: abi::envoy_dynamic_module_type_network_filter_envoy_ptr, + filter_ptr: abi::envoy_dynamic_module_type_network_filter_module_ptr, + callout_id: u64, + result: abi::envoy_dynamic_module_type_http_callout_result, + headers: *const abi::envoy_dynamic_module_type_envoy_http_header, + headers_size: usize, + body_chunks: *const abi::envoy_dynamic_module_type_envoy_buffer, + body_chunks_size: usize, +) { + let filter = filter_ptr as *mut Box>; + let filter = unsafe { &mut *filter }; + + // Convert headers to Vec<(EnvoyBuffer, EnvoyBuffer)>. + let header_vec = if headers.is_null() || headers_size == 0 { + Vec::new() + } else { + let headers_slice = unsafe { std::slice::from_raw_parts(headers, headers_size) }; + headers_slice + .iter() + .map(|h| { + ( + unsafe { EnvoyBuffer::new_from_raw(h.key_ptr as *const _, h.key_length) }, + unsafe { EnvoyBuffer::new_from_raw(h.value_ptr as *const _, h.value_length) }, + ) + }) + .collect() + }; + + // Convert body chunks to Vec. + let body_vec = if body_chunks.is_null() || body_chunks_size == 0 { + Vec::new() + } else { + let chunks_slice = unsafe { std::slice::from_raw_parts(body_chunks, body_chunks_size) }; + chunks_slice + .iter() + .map(|c| unsafe { EnvoyBuffer::new_from_raw(c.ptr as *const _, c.length) }) + .collect() + }; + + filter.on_http_callout_done( + &mut EnvoyNetworkFilterImpl::new(envoy_ptr), + callout_id, + result, + header_vec, + body_vec, + ); +} + // ============================================================================= // Listener Filter Support // ============================================================================= diff --git a/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs b/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs index 27e4afdedb857..4af374c6c2534 100644 --- a/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs +++ b/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs @@ -1,3 +1,4 @@ +#![allow(clippy::unnecessary_cast)] use crate::*; #[cfg(test)] use std::sync::atomic::AtomicBool; // This is used for testing the drop, not for the actual concurrency. @@ -612,3 +613,226 @@ fn test_envoy_dynamic_module_on_network_filter_callbacks() { assert!(ON_WRITE_CALLED.load(std::sync::atomic::Ordering::SeqCst)); assert!(ON_EVENT_CALLED.load(std::sync::atomic::Ordering::SeqCst)); } + +// ============================================================================= +// Socket option FFI stubs for testing. +// ============================================================================= + +#[derive(Clone)] +struct StoredOption { + level: i64, + name: i64, + state: abi::envoy_dynamic_module_type_socket_option_state, + value: Option>, + int_value: Option, +} + +static STORED_OPTIONS: std::sync::Mutex> = std::sync::Mutex::new(Vec::new()); + +fn reset_socket_options() { + STORED_OPTIONS.lock().unwrap().clear(); +} + +#[no_mangle] +pub extern "C" fn envoy_dynamic_module_callback_network_set_socket_option_int( + _filter_envoy_ptr: abi::envoy_dynamic_module_type_network_filter_envoy_ptr, + level: i64, + name: i64, + state: abi::envoy_dynamic_module_type_socket_option_state, + value: i64, +) -> bool { + STORED_OPTIONS.lock().unwrap().push(StoredOption { + level, + name, + state, + value: None, + int_value: Some(value), + }); + true +} + +#[no_mangle] +pub extern "C" fn envoy_dynamic_module_callback_network_set_socket_option_bytes( + _filter_envoy_ptr: abi::envoy_dynamic_module_type_network_filter_envoy_ptr, + level: i64, + name: i64, + state: abi::envoy_dynamic_module_type_socket_option_state, + value: abi::envoy_dynamic_module_type_module_buffer, +) -> bool { + let slice = unsafe { std::slice::from_raw_parts(value.ptr as *const u8, value.length) }; + STORED_OPTIONS.lock().unwrap().push(StoredOption { + level, + name, + state, + value: Some(slice.to_vec()), + int_value: None, + }); + true +} + +#[no_mangle] +pub extern "C" fn envoy_dynamic_module_callback_network_get_socket_option_int( + _filter_envoy_ptr: abi::envoy_dynamic_module_type_network_filter_envoy_ptr, + level: i64, + name: i64, + state: abi::envoy_dynamic_module_type_socket_option_state, + value_out: *mut i64, +) -> bool { + let options = STORED_OPTIONS.lock().unwrap(); + options.iter().any(|opt| { + if opt.level == level && opt.name == name && opt.state == state { + if let Some(v) = opt.int_value { + if !value_out.is_null() { + unsafe { + *value_out = v; + } + } + return true; + } + } + false + }) +} + +#[no_mangle] +pub extern "C" fn envoy_dynamic_module_callback_network_get_socket_option_bytes( + _filter_envoy_ptr: abi::envoy_dynamic_module_type_network_filter_envoy_ptr, + level: i64, + name: i64, + state: abi::envoy_dynamic_module_type_socket_option_state, + value_out: *mut abi::envoy_dynamic_module_type_envoy_buffer, +) -> bool { + let options = STORED_OPTIONS.lock().unwrap(); + options.iter().any(|opt| { + if opt.level == level && opt.name == name && opt.state == state { + if let Some(ref bytes) = opt.value { + if !value_out.is_null() { + unsafe { + (*value_out).ptr = bytes.as_ptr() as *const _; + (*value_out).length = bytes.len(); + } + } + return true; + } + } + false + }) +} + +#[no_mangle] +pub extern "C" fn envoy_dynamic_module_callback_network_get_socket_options_size( + _filter_envoy_ptr: abi::envoy_dynamic_module_type_network_filter_envoy_ptr, +) -> usize { + STORED_OPTIONS.lock().unwrap().len() +} + +#[no_mangle] +pub extern "C" fn envoy_dynamic_module_callback_network_get_socket_options( + _filter_envoy_ptr: abi::envoy_dynamic_module_type_network_filter_envoy_ptr, + options_out: *mut abi::envoy_dynamic_module_type_socket_option, +) { + if options_out.is_null() { + return; + } + let options = STORED_OPTIONS.lock().unwrap(); + let mut written = 0usize; + for opt in options.iter() { + unsafe { + let out = options_out.add(written); + (*out).level = opt.level; + (*out).name = opt.name; + (*out).state = opt.state; + match opt.int_value { + Some(v) => { + (*out).value_type = abi::envoy_dynamic_module_type_socket_option_value_type::Int; + (*out).int_value = v; + (*out).byte_value.ptr = std::ptr::null(); + (*out).byte_value.length = 0; + }, + None => { + (*out).value_type = abi::envoy_dynamic_module_type_socket_option_value_type::Bytes; + if let Some(ref bytes) = opt.value { + (*out).byte_value.ptr = bytes.as_ptr() as *const _; + (*out).byte_value.length = bytes.len(); + } else { + (*out).byte_value.ptr = std::ptr::null(); + (*out).byte_value.length = 0; + } + (*out).int_value = 0; + }, + } + } + written += 1; + } +} + +#[test] +fn test_socket_option_int_round_trip() { + reset_socket_options(); + let mut filter = EnvoyNetworkFilterImpl { + raw: std::ptr::null_mut(), + }; + assert!(filter.set_socket_option_int( + 1, + 2, + abi::envoy_dynamic_module_type_socket_option_state::Prebind, + 42 + )); + let value = filter.get_socket_option_int( + 1, + 2, + abi::envoy_dynamic_module_type_socket_option_state::Prebind, + ); + assert_eq!(Some(42), value); +} + +#[test] +fn test_socket_option_bytes_round_trip() { + reset_socket_options(); + let mut filter = EnvoyNetworkFilterImpl { + raw: std::ptr::null_mut(), + }; + assert!(filter.set_socket_option_bytes( + 3, + 4, + abi::envoy_dynamic_module_type_socket_option_state::Bound, + b"bytes-val", + )); + let value = filter.get_socket_option_bytes( + 3, + 4, + abi::envoy_dynamic_module_type_socket_option_state::Bound, + ); + assert_eq!(Some(b"bytes-val".to_vec()), value); +} + +#[test] +fn test_socket_option_list() { + reset_socket_options(); + let mut filter = EnvoyNetworkFilterImpl { + raw: std::ptr::null_mut(), + }; + assert!(filter.set_socket_option_int( + 5, + 6, + abi::envoy_dynamic_module_type_socket_option_state::Prebind, + 11 + )); + assert!(filter.set_socket_option_bytes( + 7, + 8, + abi::envoy_dynamic_module_type_socket_option_state::Listening, + b"data", + )); + + let options = filter.get_socket_options(); + assert_eq!(2, options.len()); + match &options[0].value { + SocketOptionValue::Int(v) => assert_eq!(&11, v), + _ => panic!("expected int"), + } + match &options[1].value { + SocketOptionValue::Bytes(bytes) => assert_eq!(b"data".to_vec(), *bytes), + _ => panic!("expected bytes"), + } +} diff --git a/source/extensions/filters/network/dynamic_modules/BUILD b/source/extensions/filters/network/dynamic_modules/BUILD index d9f4179d98d57..4704ea4ff1a61 100644 --- a/source/extensions/filters/network/dynamic_modules/BUILD +++ b/source/extensions/filters/network/dynamic_modules/BUILD @@ -14,6 +14,7 @@ envoy_cc_library( srcs = ["filter_config.cc"], hdrs = ["filter_config.h"], deps = [ + "//envoy/upstream:cluster_manager_interface", "//source/common/config:utility_lib", "//source/extensions/dynamic_modules:dynamic_modules_lib", ], @@ -28,14 +29,20 @@ envoy_cc_library( hdrs = ["filter.h"], deps = [ ":filter_config_lib", + "//envoy/http:async_client_interface", + "//envoy/http:message_interface", "//envoy/network:connection_interface", "//envoy/network:filter_interface", "//envoy/router:string_accessor_interface", "//source/common/buffer:buffer_lib", "//source/common/common:logger_lib", + "//source/common/http:message_lib", + "//source/common/network:socket_option_lib", + "//source/common/network:upstream_socket_options_filter_state_lib", "//source/common/protobuf", "//source/common/router:string_accessor_lib", "//source/extensions/dynamic_modules:dynamic_modules_lib", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], ) diff --git a/source/extensions/filters/network/dynamic_modules/abi_impl.cc b/source/extensions/filters/network/dynamic_modules/abi_impl.cc index 26842c553fb05..cd17c0d1a2fd6 100644 --- a/source/extensions/filters/network/dynamic_modules/abi_impl.cc +++ b/source/extensions/filters/network/dynamic_modules/abi_impl.cc @@ -1,7 +1,12 @@ #include +#include "envoy/config/core/v3/socket_option.pb.h" +#include "envoy/http/message.h" #include "envoy/router/string_accessor.h" +#include "source/common/http/message_impl.h" +#include "source/common/network/socket_option_impl.h" +#include "source/common/network/upstream_socket_options_filter_state.h" #include "source/common/protobuf/protobuf.h" #include "source/common/router/string_accessor_impl.h" #include "source/extensions/dynamic_modules/abi.h" @@ -545,6 +550,164 @@ bool envoy_dynamic_module_callback_network_get_dynamic_metadata_number( return true; } +envoy_dynamic_module_type_http_callout_init_result +envoy_dynamic_module_callback_network_filter_http_callout( + envoy_dynamic_module_type_network_filter_envoy_ptr filter_envoy_ptr, uint64_t* callout_id_out, + envoy_dynamic_module_type_module_buffer cluster_name, + envoy_dynamic_module_type_module_http_header* headers, size_t headers_size, + envoy_dynamic_module_type_module_buffer body, uint64_t timeout_milliseconds) { + auto* filter = static_cast(filter_envoy_ptr); + + // Build the request message. + Http::RequestMessagePtr message = std::make_unique(); + + // Add headers. + for (size_t i = 0; i < headers_size; i++) { + const auto& header = headers[i]; + message->headers().addCopy( + Http::LowerCaseString(std::string(header.key_ptr, header.key_length)), + std::string(header.value_ptr, header.value_length)); + } + + // Add body if present. + if (body.length > 0 && body.ptr != nullptr) { + message->body().add(body.ptr, body.length); + } + + // Validate required headers. + if (message->headers().Method() == nullptr || message->headers().Path() == nullptr || + message->headers().Host() == nullptr) { + return envoy_dynamic_module_type_http_callout_init_result_MissingRequiredHeaders; + } + + // Send the callout. + return filter->sendHttpCallout(callout_id_out, std::string(cluster_name.ptr, cluster_name.length), + std::move(message), timeout_milliseconds); +} + +namespace { + +Network::UpstreamSocketOptionsFilterState* +ensureUpstreamSocketOptionsFilterState(DynamicModuleNetworkFilter& filter) { + auto filter_state_shared = filter.connection().streamInfo().filterState(); + StreamInfo::FilterState& filter_state = *filter_state_shared; + const bool has_options = filter_state.hasData( + Network::UpstreamSocketOptionsFilterState::key()); + if (!has_options) { + filter_state.setData(Network::UpstreamSocketOptionsFilterState::key(), + std::make_unique(), + StreamInfo::FilterState::StateType::Mutable, + StreamInfo::FilterState::LifeSpan::Connection); + } + return filter_state.getDataMutable( + Network::UpstreamSocketOptionsFilterState::key()); +} + +envoy::config::core::v3::SocketOption::SocketState +mapSocketState(envoy_dynamic_module_type_socket_option_state state) { + switch (state) { + case envoy_dynamic_module_type_socket_option_state_Prebind: + return envoy::config::core::v3::SocketOption::STATE_PREBIND; + case envoy_dynamic_module_type_socket_option_state_Bound: + return envoy::config::core::v3::SocketOption::STATE_BOUND; + case envoy_dynamic_module_type_socket_option_state_Listening: + return envoy::config::core::v3::SocketOption::STATE_LISTENING; + } + return envoy::config::core::v3::SocketOption::STATE_PREBIND; +} + +bool validateSocketState(envoy_dynamic_module_type_socket_option_state state) { + return state == envoy_dynamic_module_type_socket_option_state_Prebind || + state == envoy_dynamic_module_type_socket_option_state_Bound || + state == envoy_dynamic_module_type_socket_option_state_Listening; +} + +} // namespace + +bool envoy_dynamic_module_callback_network_set_socket_option_int( + envoy_dynamic_module_type_network_filter_envoy_ptr filter_envoy_ptr, int64_t level, + int64_t name, envoy_dynamic_module_type_socket_option_state state, int64_t value) { + if (!validateSocketState(state)) { + return false; + } + auto* filter = static_cast(filter_envoy_ptr); + auto* upstream_options = ensureUpstreamSocketOptionsFilterState(*filter); + + auto option = std::make_shared( + mapSocketState(state), + Network::SocketOptionName(static_cast(level), static_cast(name), ""), + static_cast(value)); + Network::Socket::OptionsSharedPtr option_list = std::make_shared(); + option_list->push_back(option); + upstream_options->addOption(option_list); + + filter->storeSocketOptionInt(level, name, state, value); + return true; +} + +bool envoy_dynamic_module_callback_network_set_socket_option_bytes( + envoy_dynamic_module_type_network_filter_envoy_ptr filter_envoy_ptr, int64_t level, + int64_t name, envoy_dynamic_module_type_socket_option_state state, + envoy_dynamic_module_type_module_buffer value) { + if (!validateSocketState(state) || value.ptr == nullptr) { + return false; + } + auto* filter = static_cast(filter_envoy_ptr); + auto* upstream_options = ensureUpstreamSocketOptionsFilterState(*filter); + + absl::string_view value_view(value.ptr, value.length); + auto option = std::make_shared( + mapSocketState(state), + Network::SocketOptionName(static_cast(level), static_cast(name), ""), value_view); + Network::Socket::OptionsSharedPtr option_list = std::make_shared(); + option_list->push_back(option); + upstream_options->addOption(option_list); + + filter->storeSocketOptionBytes(level, name, state, value_view); + return true; +} + +bool envoy_dynamic_module_callback_network_get_socket_option_int( + envoy_dynamic_module_type_network_filter_envoy_ptr filter_envoy_ptr, int64_t level, + int64_t name, envoy_dynamic_module_type_socket_option_state state, int64_t* value_out) { + if (value_out == nullptr || !validateSocketState(state)) { + return false; + } + auto* filter = static_cast(filter_envoy_ptr); + return filter->tryGetSocketOptionInt(level, name, state, *value_out); +} + +bool envoy_dynamic_module_callback_network_get_socket_option_bytes( + envoy_dynamic_module_type_network_filter_envoy_ptr filter_envoy_ptr, int64_t level, + int64_t name, envoy_dynamic_module_type_socket_option_state state, + envoy_dynamic_module_type_envoy_buffer* value_out) { + if (value_out == nullptr || !validateSocketState(state)) { + return false; + } + auto* filter = static_cast(filter_envoy_ptr); + absl::string_view value_view; + if (!filter->tryGetSocketOptionBytes(level, name, state, value_view)) { + return false; + } + value_out->ptr = value_view.data(); + value_out->length = value_view.size(); + return true; +} + +size_t envoy_dynamic_module_callback_network_get_socket_options_size( + envoy_dynamic_module_type_network_filter_envoy_ptr filter_envoy_ptr) { + auto* filter = static_cast(filter_envoy_ptr); + return filter->socketOptionCount(); +} + +void envoy_dynamic_module_callback_network_get_socket_options( + envoy_dynamic_module_type_network_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_socket_option* options_out) { + auto* filter = static_cast(filter_envoy_ptr); + size_t options_written = 0; + filter->copySocketOptions(options_out, filter->socketOptionCount(), options_written); +} + } // extern "C" } // namespace NetworkFilters diff --git a/source/extensions/filters/network/dynamic_modules/factory.cc b/source/extensions/filters/network/dynamic_modules/factory.cc index a5c8f033ca56e..52faac0c79a27 100644 --- a/source/extensions/filters/network/dynamic_modules/factory.cc +++ b/source/extensions/filters/network/dynamic_modules/factory.cc @@ -12,7 +12,7 @@ namespace Configuration { absl::StatusOr DynamicModuleNetworkFilterConfigFactory::createFilterFactoryFromProtoTyped( - const FilterConfig& proto_config, FactoryContext& /*context*/) { + const FilterConfig& proto_config, FactoryContext& context) { const auto& module_config = proto_config.dynamic_module_config(); auto dynamic_module = Extensions::DynamicModules::newDynamicModuleByName( @@ -33,7 +33,8 @@ DynamicModuleNetworkFilterConfigFactory::createFilterFactoryFromProtoTyped( Envoy::Extensions::DynamicModules::NetworkFilters::DynamicModuleNetworkFilterConfigSharedPtr> filter_config = Envoy::Extensions::DynamicModules::NetworkFilters::newDynamicModuleNetworkFilterConfig( - proto_config.filter_name(), config, std::move(dynamic_module.value())); + proto_config.filter_name(), config, std::move(dynamic_module.value()), + context.serverFactoryContext().clusterManager()); if (!filter_config.ok()) { return absl::InvalidArgumentError("Failed to create filter config: " + diff --git a/source/extensions/filters/network/dynamic_modules/filter.cc b/source/extensions/filters/network/dynamic_modules/filter.cc index 00524db0fafcd..8015fcd03c9e2 100644 --- a/source/extensions/filters/network/dynamic_modules/filter.cc +++ b/source/extensions/filters/network/dynamic_modules/filter.cc @@ -46,6 +46,14 @@ void DynamicModuleNetworkFilter::initializeInModuleFilter() { } void DynamicModuleNetworkFilter::destroy() { + // Cancel all pending HTTP callouts before destroying the filter. + for (auto& callout : http_callouts_) { + if (callout.second->request_ != nullptr) { + callout.second->request_->cancel(); + } + } + http_callouts_.clear(); + if (in_module_filter_ != nullptr) { config_->on_network_filter_destroy_(in_module_filter_); in_module_filter_ = nullptr; @@ -134,6 +142,174 @@ void DynamicModuleNetworkFilter::write(Buffer::Instance& data, bool end_stream) } } +void DynamicModuleNetworkFilter::storeSocketOptionInt( + int64_t level, int64_t name, envoy_dynamic_module_type_socket_option_state state, + int64_t value) { + socket_options_.push_back( + StoredSocketOption{level, name, state, /*is_int=*/true, value, std::string()}); +} + +void DynamicModuleNetworkFilter::storeSocketOptionBytes( + int64_t level, int64_t name, envoy_dynamic_module_type_socket_option_state state, + absl::string_view value) { + socket_options_.push_back(StoredSocketOption{level, name, state, /*is_int=*/false, + /*int_value=*/0, + std::string(value.data(), value.size())}); +} + +bool DynamicModuleNetworkFilter::tryGetSocketOptionInt( + int64_t level, int64_t name, envoy_dynamic_module_type_socket_option_state state, + int64_t& value_out) const { + for (const auto& opt : socket_options_) { + if (opt.is_int && opt.level == level && opt.name == name && opt.state == state) { + value_out = opt.int_value; + return true; + } + } + return false; +} + +bool DynamicModuleNetworkFilter::tryGetSocketOptionBytes( + int64_t level, int64_t name, envoy_dynamic_module_type_socket_option_state state, + absl::string_view& value_out) const { + for (const auto& opt : socket_options_) { + if (!opt.is_int && opt.level == level && opt.name == name && opt.state == state) { + value_out = opt.byte_value; + return true; + } + } + return false; +} + +void DynamicModuleNetworkFilter::copySocketOptions( + envoy_dynamic_module_type_socket_option* options_out, size_t options_size, + size_t& options_written) const { + options_written = 0; + for (const auto& opt : socket_options_) { + if (options_written >= options_size) { + break; + } + auto& out = options_out[options_written]; + out.level = opt.level; + out.name = opt.name; + out.state = opt.state; + if (opt.is_int) { + out.value_type = envoy_dynamic_module_type_socket_option_value_type_Int; + out.int_value = opt.int_value; + out.byte_value.ptr = nullptr; + out.byte_value.length = 0; + } else { + out.value_type = envoy_dynamic_module_type_socket_option_value_type_Bytes; + out.int_value = 0; + out.byte_value.ptr = opt.byte_value.data(); + out.byte_value.length = opt.byte_value.size(); + } + ++options_written; + } +} + +envoy_dynamic_module_type_http_callout_init_result DynamicModuleNetworkFilter::sendHttpCallout( + uint64_t* callout_id_out, absl::string_view cluster_name, Http::RequestMessagePtr&& message, + uint64_t timeout_milliseconds) { + Upstream::ThreadLocalCluster* cluster = + config_->cluster_manager_.getThreadLocalCluster(cluster_name); + if (!cluster) { + return envoy_dynamic_module_type_http_callout_init_result_ClusterNotFound; + } + Http::AsyncClient::RequestOptions options; + options.setTimeout(std::chrono::milliseconds(timeout_milliseconds)); + + // Prepare the callback and the ID. + const uint64_t callout_id = getNextCalloutId(); + auto http_callout_callback = std::make_unique( + shared_from_this(), callout_id); + DynamicModuleNetworkFilter::HttpCalloutCallback& callback = *http_callout_callback; + + auto request = cluster->httpAsyncClient().send(std::move(message), callback, options); + if (!request) { + return envoy_dynamic_module_type_http_callout_init_result_CannotCreateRequest; + } + + // Register the callout. + callback.request_ = request; + http_callouts_.emplace(callout_id, std::move(http_callout_callback)); + *callout_id_out = callout_id; + + return envoy_dynamic_module_type_http_callout_init_result_Success; +} + +void DynamicModuleNetworkFilter::HttpCalloutCallback::onSuccess( + const Http::AsyncClient::Request&, Http::ResponseMessagePtr&& response) { + // Copy the filter shared_ptr and callout id to the local scope since + // on_network_filter_http_callout_done_ might cause destruction of the filter. That eventually + // ends up deallocating this callback itself. + DynamicModuleNetworkFilterSharedPtr filter = filter_.lock(); + uint64_t callout_id = callout_id_; + // Check if the filter is destroyed before the callout completed. + if (!filter || !filter->in_module_filter_ || + !filter->config_->on_network_filter_http_callout_done_) { + return; + } + + absl::InlinedVector headers_vector; + headers_vector.reserve(response->headers().size()); + response->headers().iterate( + [&headers_vector](const Http::HeaderEntry& header) -> Http::HeaderMap::Iterate { + headers_vector.emplace_back(envoy_dynamic_module_type_envoy_http_header{ + .key_ptr = const_cast(header.key().getStringView().data()), + .key_length = header.key().getStringView().size(), + .value_ptr = const_cast(header.value().getStringView().data()), + .value_length = header.value().getStringView().size()}); + return Http::HeaderMap::Iterate::Continue; + }); + + absl::InlinedVector body_chunks_vector; + const Buffer::Instance& body = response->body(); + for (const Buffer::RawSlice& slice : body.getRawSlices()) { + body_chunks_vector.emplace_back( + envoy_dynamic_module_type_envoy_buffer{static_cast(slice.mem_), slice.len_}); + } + + filter->config_->on_network_filter_http_callout_done_( + filter->thisAsVoidPtr(), filter->in_module_filter_, callout_id, + envoy_dynamic_module_type_http_callout_result_Success, headers_vector.data(), + headers_vector.size(), body_chunks_vector.data(), body_chunks_vector.size()); + + // Remove the callout from the map. + filter->http_callouts_.erase(callout_id); +} + +void DynamicModuleNetworkFilter::HttpCalloutCallback::onFailure( + const Http::AsyncClient::Request&, Http::AsyncClient::FailureReason reason) { + // Copy the filter shared_ptr and callout id to the local scope since + // on_network_filter_http_callout_done_ might cause destruction of the filter. That eventually + // ends up deallocating this callback itself. + DynamicModuleNetworkFilterSharedPtr filter = filter_.lock(); + uint64_t callout_id = callout_id_; + if (!filter || !filter->in_module_filter_ || + !filter->config_->on_network_filter_http_callout_done_) { + return; + } + + envoy_dynamic_module_type_http_callout_result result = + envoy_dynamic_module_type_http_callout_result_Reset; + switch (reason) { + case Http::AsyncClient::FailureReason::Reset: + result = envoy_dynamic_module_type_http_callout_result_Reset; + break; + case Http::AsyncClient::FailureReason::ExceedResponseBufferLimit: + result = envoy_dynamic_module_type_http_callout_result_ExceedResponseBufferLimit; + break; + } + + filter->config_->on_network_filter_http_callout_done_(filter->thisAsVoidPtr(), + filter->in_module_filter_, callout_id, + result, nullptr, 0, nullptr, 0); + + // Remove the callout from the map. + filter->http_callouts_.erase(callout_id); +} + } // namespace NetworkFilters } // namespace DynamicModules } // namespace Extensions diff --git a/source/extensions/filters/network/dynamic_modules/filter.h b/source/extensions/filters/network/dynamic_modules/filter.h index e37844172ff25..43670f8fbe0bd 100644 --- a/source/extensions/filters/network/dynamic_modules/filter.h +++ b/source/extensions/filters/network/dynamic_modules/filter.h @@ -1,5 +1,9 @@ #pragma once +#include +#include + +#include "envoy/http/async_client.h" #include "envoy/network/connection.h" #include "envoy/network/filter.h" @@ -17,6 +21,7 @@ namespace NetworkFilters { */ class DynamicModuleNetworkFilter : public Network::Filter, public Network::ConnectionCallbacks, + public std::enable_shared_from_this, public Logger::Loggable { public: DynamicModuleNetworkFilter(DynamicModuleNetworkFilterConfigSharedPtr config); @@ -81,6 +86,51 @@ class DynamicModuleNetworkFilter : public Network::Filter, */ Network::Connection& connection() { return read_callbacks_->connection(); } + /** + * Sends an HTTP callout to the specified cluster with the given message. + */ + envoy_dynamic_module_type_http_callout_init_result + sendHttpCallout(uint64_t* callout_id_out, absl::string_view cluster_name, + Http::RequestMessagePtr&& message, uint64_t timeout_milliseconds); + + /** + * Store an integer socket option for the current connection and Surface it back to modules. + */ + void storeSocketOptionInt(int64_t level, int64_t name, + envoy_dynamic_module_type_socket_option_state state, int64_t value); + + /** + * Store a bytes socket option for the current connection and Surface it back to modules. + */ + void storeSocketOptionBytes(int64_t level, int64_t name, + envoy_dynamic_module_type_socket_option_state state, + absl::string_view value); + + /** + * Retrieve an integer socket option by level/name/state. + */ + bool tryGetSocketOptionInt(int64_t level, int64_t name, + envoy_dynamic_module_type_socket_option_state state, + int64_t& value_out) const; + + /** + * Retrieve a bytes socket option by level/name/state. + */ + bool tryGetSocketOptionBytes(int64_t level, int64_t name, + envoy_dynamic_module_type_socket_option_state state, + absl::string_view& value_out) const; + + /** + * Number of socket options stored for this connection. + */ + size_t socketOptionCount() const { return socket_options_.size(); } + + /** + * Fill provided buffer with stored socket options up to options_size. + */ + void copySocketOptions(envoy_dynamic_module_type_socket_option* options_out, size_t options_size, + size_t& options_written) const; + private: /** * Helper to get the `this` pointer as a void pointer. @@ -104,6 +154,49 @@ class DynamicModuleNetworkFilter : public Network::Filter, Buffer::Instance* current_write_buffer_ = nullptr; bool destroyed_ = false; + + /** + * This implementation of the AsyncClient::Callbacks is used to handle the response from the HTTP + * callout from the parent network filter. + */ + class HttpCalloutCallback : public Http::AsyncClient::Callbacks { + public: + HttpCalloutCallback(std::shared_ptr filter, uint64_t id) + : filter_(std::move(filter)), callout_id_(id) {} + ~HttpCalloutCallback() override = default; + + void onSuccess(const Http::AsyncClient::Request& request, + Http::ResponseMessagePtr&& response) override; + void onFailure(const Http::AsyncClient::Request& request, + Http::AsyncClient::FailureReason reason) override; + void onBeforeFinalizeUpstreamSpan(Envoy::Tracing::Span&, + const Http::ResponseHeaderMap*) override {}; + // This is the request object that is used to send the HTTP callout. It is used to cancel the + // callout if the filter is destroyed before the callout is completed. + Http::AsyncClient::Request* request_ = nullptr; + + private: + const std::weak_ptr filter_; + const uint64_t callout_id_{}; + }; + + uint64_t getNextCalloutId() { return next_callout_id_++; } + + uint64_t next_callout_id_ = 1; // 0 is reserved as an invalid id. + + absl::flat_hash_map> + http_callouts_; + + struct StoredSocketOption { + int64_t level; + int64_t name; + envoy_dynamic_module_type_socket_option_state state; + bool is_int; + int64_t int_value; + std::string byte_value; + }; + + std::vector socket_options_; }; using DynamicModuleNetworkFilterSharedPtr = std::shared_ptr; diff --git a/source/extensions/filters/network/dynamic_modules/filter_config.cc b/source/extensions/filters/network/dynamic_modules/filter_config.cc index 3f22f528b7051..beb60aa65ed4a 100644 --- a/source/extensions/filters/network/dynamic_modules/filter_config.cc +++ b/source/extensions/filters/network/dynamic_modules/filter_config.cc @@ -11,8 +11,8 @@ namespace NetworkFilters { DynamicModuleNetworkFilterConfig::DynamicModuleNetworkFilterConfig( const absl::string_view filter_name, const absl::string_view filter_config, - DynamicModulePtr dynamic_module) - : filter_name_(filter_name), filter_config_(filter_config), + DynamicModulePtr dynamic_module, Envoy::Upstream::ClusterManager& cluster_manager) + : cluster_manager_(cluster_manager), filter_name_(filter_name), filter_config_(filter_config), dynamic_module_(std::move(dynamic_module)) {} DynamicModuleNetworkFilterConfig::~DynamicModuleNetworkFilterConfig() { @@ -21,10 +21,9 @@ DynamicModuleNetworkFilterConfig::~DynamicModuleNetworkFilterConfig() { } } -absl::StatusOr -newDynamicModuleNetworkFilterConfig(const absl::string_view filter_name, - const absl::string_view filter_config, - DynamicModulePtr dynamic_module) { +absl::StatusOr newDynamicModuleNetworkFilterConfig( + const absl::string_view filter_name, const absl::string_view filter_config, + DynamicModulePtr dynamic_module, Envoy::Upstream::ClusterManager& cluster_manager) { // Resolve the symbols for the network filter using graceful error handling. auto on_config_new = @@ -61,8 +60,13 @@ newDynamicModuleNetworkFilterConfig(const absl::string_view filter_name, "envoy_dynamic_module_on_network_filter_destroy"); RETURN_IF_NOT_OK_REF(on_destroy.status()); - auto config = std::make_shared(filter_name, filter_config, - std::move(dynamic_module)); + // HTTP callout done is optional - module may not implement async calls. + auto on_http_callout_done = + dynamic_module->getFunctionPointer( + "envoy_dynamic_module_on_network_filter_http_callout_done"); + + auto config = std::make_shared( + filter_name, filter_config, std::move(dynamic_module), cluster_manager); // Store the resolved function pointers. config->on_network_filter_config_destroy_ = on_config_destroy.value(); @@ -72,6 +76,8 @@ newDynamicModuleNetworkFilterConfig(const absl::string_view filter_name, config->on_network_filter_write_ = on_write.value(); config->on_network_filter_event_ = on_event.value(); config->on_network_filter_destroy_ = on_destroy.value(); + config->on_network_filter_http_callout_done_ = + on_http_callout_done.ok() ? on_http_callout_done.value() : nullptr; // Create the in-module configuration. envoy_dynamic_module_type_envoy_buffer name_buffer = {const_cast(filter_name.data()), diff --git a/source/extensions/filters/network/dynamic_modules/filter_config.h b/source/extensions/filters/network/dynamic_modules/filter_config.h index b0d6b2c8bc60d..fd43ffe81efb1 100644 --- a/source/extensions/filters/network/dynamic_modules/filter_config.h +++ b/source/extensions/filters/network/dynamic_modules/filter_config.h @@ -1,5 +1,7 @@ #pragma once +#include "envoy/upstream/cluster_manager.h" + #include "source/common/common/statusor.h" #include "source/extensions/dynamic_modules/abi.h" #include "source/extensions/dynamic_modules/dynamic_modules.h" @@ -17,6 +19,8 @@ using OnNetworkFilterReadType = decltype(&envoy_dynamic_module_on_network_filter using OnNetworkFilterWriteType = decltype(&envoy_dynamic_module_on_network_filter_write); using OnNetworkFilterEventType = decltype(&envoy_dynamic_module_on_network_filter_event); using OnNetworkFilterDestroyType = decltype(&envoy_dynamic_module_on_network_filter_destroy); +using OnNetworkFilterHttpCalloutDoneType = + decltype(&envoy_dynamic_module_on_network_filter_http_callout_done); /** * A config to create network filters based on a dynamic module. This will be owned by multiple @@ -34,11 +38,12 @@ class DynamicModuleNetworkFilterConfig { * @param filter_name the name of the filter. * @param filter_config the configuration for the module. * @param dynamic_module the dynamic module to use. - * @param context the factory context. + * @param cluster_manager the cluster manager for async HTTP callouts. */ DynamicModuleNetworkFilterConfig(const absl::string_view filter_name, const absl::string_view filter_config, - DynamicModulePtr dynamic_module); + DynamicModulePtr dynamic_module, + Envoy::Upstream::ClusterManager& cluster_manager); ~DynamicModuleNetworkFilterConfig(); @@ -55,13 +60,17 @@ class DynamicModuleNetworkFilterConfig { OnNetworkFilterWriteType on_network_filter_write_ = nullptr; OnNetworkFilterEventType on_network_filter_event_ = nullptr; OnNetworkFilterDestroyType on_network_filter_destroy_ = nullptr; + OnNetworkFilterHttpCalloutDoneType on_network_filter_http_callout_done_ = nullptr; + + Envoy::Upstream::ClusterManager& cluster_manager_; private: // Allow the factory function to access private members for initialization. friend absl::StatusOr> newDynamicModuleNetworkFilterConfig(const absl::string_view filter_name, const absl::string_view filter_config, - DynamicModulePtr dynamic_module); + DynamicModulePtr dynamic_module, + Envoy::Upstream::ClusterManager& cluster_manager); // The name of the filter passed in the constructor. const std::string filter_name_; @@ -80,13 +89,14 @@ using DynamicModuleNetworkFilterConfigSharedPtr = std::shared_ptr newDynamicModuleNetworkFilterConfig(const absl::string_view filter_name, const absl::string_view filter_config, - Extensions::DynamicModules::DynamicModulePtr dynamic_module); + Extensions::DynamicModules::DynamicModulePtr dynamic_module, + Envoy::Upstream::ClusterManager& cluster_manager); } // namespace NetworkFilters } // namespace DynamicModules diff --git a/test/extensions/dynamic_modules/network/BUILD b/test/extensions/dynamic_modules/network/BUILD index 62c4b559aab81..14739f5637839 100644 --- a/test/extensions/dynamic_modules/network/BUILD +++ b/test/extensions/dynamic_modules/network/BUILD @@ -23,6 +23,7 @@ envoy_cc_test( "//source/extensions/filters/network/dynamic_modules:filter_lib", "//test/extensions/dynamic_modules:util", "//test/mocks/network:network_mocks", + "//test/mocks/upstream:cluster_manager_mocks", ], ) @@ -56,8 +57,10 @@ envoy_cc_test( "//source/extensions/filters/network/dynamic_modules:config", "//source/extensions/filters/network/dynamic_modules:filter_lib", "//test/extensions/dynamic_modules:util", + "//test/mocks/http:http_mocks", "//test/mocks/network:network_mocks", "//test/mocks/ssl:ssl_mocks", + "//test/mocks/upstream:cluster_manager_mocks", ], ) diff --git a/test/extensions/dynamic_modules/network/abi_impl_test.cc b/test/extensions/dynamic_modules/network/abi_impl_test.cc index f5153bc3538ea..49932a5e23c66 100644 --- a/test/extensions/dynamic_modules/network/abi_impl_test.cc +++ b/test/extensions/dynamic_modules/network/abi_impl_test.cc @@ -1,14 +1,17 @@ #include +#include "source/common/http/message_impl.h" #include "source/common/network/address_impl.h" #include "source/common/router/string_accessor_impl.h" #include "source/extensions/dynamic_modules/abi.h" #include "source/extensions/filters/network/dynamic_modules/filter.h" #include "test/extensions/dynamic_modules/util.h" +#include "test/mocks/http/mocks.h" #include "test/mocks/network/mocks.h" #include "test/mocks/ssl/mocks.h" #include "test/mocks/stream_info/mocks.h" +#include "test/mocks/upstream/cluster_manager.h" namespace Envoy { namespace Extensions { @@ -21,8 +24,8 @@ class DynamicModuleNetworkFilterAbiCallbackTest : public testing::Test { auto dynamic_module = newDynamicModule(testSharedObjectPath("network_no_op", "c"), false); EXPECT_TRUE(dynamic_module.ok()) << dynamic_module.status().message(); - auto filter_config_or_status = - newDynamicModuleNetworkFilterConfig("test_filter", "", std::move(dynamic_module.value())); + auto filter_config_or_status = newDynamicModuleNetworkFilterConfig( + "test_filter", "", std::move(dynamic_module.value()), cluster_manager_); EXPECT_TRUE(filter_config_or_status.ok()) << filter_config_or_status.status().message(); filter_config_ = filter_config_or_status.value(); @@ -34,10 +37,17 @@ class DynamicModuleNetworkFilterAbiCallbackTest : public testing::Test { filter_->initializeWriteFilterCallbacks(write_callbacks_); } - void TearDown() override { filter_.reset(); } + void TearDown() override { + if (filter_) { + filter_->onEvent(Network::ConnectionEvent::LocalClose); + } + filter_.reset(); + filter_config_.reset(); + } void* filterPtr() { return static_cast(filter_.get()); } + NiceMock cluster_manager_; DynamicModuleNetworkFilterConfigSharedPtr filter_config_; std::shared_ptr filter_; NiceMock read_callbacks_; @@ -1167,6 +1177,432 @@ TEST_F(DynamicModuleNetworkFilterAbiCallbackTest, SetDynamicMetadataNumberNegati EXPECT_DOUBLE_EQ(negative_value, result); } +// ============================================================================= +// Tests for socket options. +// ============================================================================= + +TEST_F(DynamicModuleNetworkFilterAbiCallbackTest, SetAndGetSocketOptionInt) { + const int64_t level = 1; + const int64_t name = 2; + const int64_t value = 12345; + EXPECT_TRUE(envoy_dynamic_module_callback_network_set_socket_option_int( + filterPtr(), level, name, envoy_dynamic_module_type_socket_option_state_Prebind, value)); + + int64_t result = 0; + EXPECT_TRUE(envoy_dynamic_module_callback_network_get_socket_option_int( + filterPtr(), level, name, envoy_dynamic_module_type_socket_option_state_Prebind, &result)); + EXPECT_EQ(value, result); +} + +TEST_F(DynamicModuleNetworkFilterAbiCallbackTest, SetAndGetSocketOptionBytes) { + const int64_t level = 3; + const int64_t name = 4; + const std::string value = "socket-bytes"; + EXPECT_TRUE(envoy_dynamic_module_callback_network_set_socket_option_bytes( + filterPtr(), level, name, envoy_dynamic_module_type_socket_option_state_Bound, + {value.data(), value.size()})); + + envoy_dynamic_module_type_envoy_buffer result; + EXPECT_TRUE(envoy_dynamic_module_callback_network_get_socket_option_bytes( + filterPtr(), level, name, envoy_dynamic_module_type_socket_option_state_Bound, &result)); + EXPECT_EQ(value, std::string(result.ptr, result.length)); +} + +TEST_F(DynamicModuleNetworkFilterAbiCallbackTest, GetSocketOptionIntMissing) { + int64_t value = 0; + EXPECT_FALSE(envoy_dynamic_module_callback_network_get_socket_option_int( + filterPtr(), 99, 100, envoy_dynamic_module_type_socket_option_state_Prebind, &value)); +} + +TEST_F(DynamicModuleNetworkFilterAbiCallbackTest, GetSocketOptionBytesMissing) { + envoy_dynamic_module_type_envoy_buffer value_out; + EXPECT_FALSE(envoy_dynamic_module_callback_network_get_socket_option_bytes( + filterPtr(), 99, 100, envoy_dynamic_module_type_socket_option_state_Bound, &value_out)); +} + +TEST_F(DynamicModuleNetworkFilterAbiCallbackTest, ListSocketOptions) { + // Add two options. + EXPECT_TRUE(envoy_dynamic_module_callback_network_set_socket_option_int( + filterPtr(), 10, 11, envoy_dynamic_module_type_socket_option_state_Prebind, 7)); + const std::string bytes_val = "opt-bytes"; + EXPECT_TRUE(envoy_dynamic_module_callback_network_set_socket_option_bytes( + filterPtr(), 12, 13, envoy_dynamic_module_type_socket_option_state_Listening, + {bytes_val.data(), bytes_val.size()})); + + const size_t size = envoy_dynamic_module_callback_network_get_socket_options_size(filterPtr()); + EXPECT_EQ(2, size); + + std::vector options(size); + envoy_dynamic_module_callback_network_get_socket_options(filterPtr(), options.data()); + + // Verify first option (int). + EXPECT_EQ(10, options[0].level); + EXPECT_EQ(11, options[0].name); + EXPECT_EQ(envoy_dynamic_module_type_socket_option_value_type_Int, options[0].value_type); + EXPECT_EQ(7, options[0].int_value); + + // Verify second option (bytes). + EXPECT_EQ(12, options[1].level); + EXPECT_EQ(13, options[1].name); + EXPECT_EQ(envoy_dynamic_module_type_socket_option_value_type_Bytes, options[1].value_type); + EXPECT_EQ(bytes_val, std::string(options[1].byte_value.ptr, options[1].byte_value.length)); +} + +// ============================================================================= +// Tests for send_http_callout. +// ============================================================================= + +class DynamicModuleNetworkFilterHttpCalloutTest : public testing::Test { +public: + void SetUp() override { + auto dynamic_module = newDynamicModule(testSharedObjectPath("network_no_op", "c"), false); + EXPECT_TRUE(dynamic_module.ok()) << dynamic_module.status().message(); + + auto filter_config_or_status = newDynamicModuleNetworkFilterConfig( + "test_filter", "", std::move(dynamic_module.value()), cluster_manager_); + EXPECT_TRUE(filter_config_or_status.ok()) << filter_config_or_status.status().message(); + filter_config_ = filter_config_or_status.value(); + + filter_ = std::make_shared(filter_config_); + filter_->initializeInModuleFilter(); + + ON_CALL(read_callbacks_, connection()).WillByDefault(testing::ReturnRef(connection_)); + filter_->initializeReadFilterCallbacks(read_callbacks_); + filter_->initializeWriteFilterCallbacks(write_callbacks_); + } + + void TearDown() override { + if (filter_) { + filter_->onEvent(Network::ConnectionEvent::LocalClose); + } + filter_.reset(); + } + + void* filterPtr() { return static_cast(filter_.get()); } + + NiceMock cluster_manager_; + DynamicModuleNetworkFilterConfigSharedPtr filter_config_; + std::shared_ptr filter_; + NiceMock read_callbacks_; + NiceMock write_callbacks_; + NiceMock connection_; +}; + +TEST_F(DynamicModuleNetworkFilterHttpCalloutTest, SendHttpCalloutClusterNotFound) { + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("nonexistent_cluster")) + .WillOnce(testing::Return(nullptr)); + + uint64_t callout_id = 0; + std::vector headers = { + {.key_ptr = ":method", .key_length = 7, .value_ptr = "GET", .value_length = 3}, + {.key_ptr = ":path", .key_length = 5, .value_ptr = "/test", .value_length = 5}, + {.key_ptr = "host", .key_length = 4, .value_ptr = "example.com", .value_length = 11}, + }; + + auto result = envoy_dynamic_module_callback_network_filter_http_callout( + filterPtr(), &callout_id, {"nonexistent_cluster", 19}, headers.data(), headers.size(), + {nullptr, 0}, 5000); + + EXPECT_EQ(envoy_dynamic_module_type_http_callout_init_result_ClusterNotFound, result); + EXPECT_EQ(0, callout_id); +} + +TEST_F(DynamicModuleNetworkFilterHttpCalloutTest, SendHttpCalloutMissingRequiredHeaders) { + uint64_t callout_id = 0; + // Missing :method header. + std::vector headers = { + {.key_ptr = ":path", .key_length = 5, .value_ptr = "/test", .value_length = 5}, + {.key_ptr = "host", .key_length = 4, .value_ptr = "example.com", .value_length = 11}, + }; + + auto result = envoy_dynamic_module_callback_network_filter_http_callout( + filterPtr(), &callout_id, {"test_cluster", 12}, headers.data(), headers.size(), {nullptr, 0}, + 5000); + + EXPECT_EQ(envoy_dynamic_module_type_http_callout_init_result_MissingRequiredHeaders, result); +} + +TEST_F(DynamicModuleNetworkFilterHttpCalloutTest, SendHttpCalloutCannotCreateRequest) { + NiceMock cluster; + NiceMock async_client; + + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test_cluster")) + .WillOnce(testing::Return(&cluster)); + EXPECT_CALL(cluster, httpAsyncClient()).WillOnce(testing::ReturnRef(async_client)); + EXPECT_CALL(async_client, send_(testing::_, testing::_, testing::_)) + .WillOnce(testing::Return(nullptr)); + + uint64_t callout_id = 0; + std::vector headers = { + {.key_ptr = ":method", .key_length = 7, .value_ptr = "GET", .value_length = 3}, + {.key_ptr = ":path", .key_length = 5, .value_ptr = "/test", .value_length = 5}, + {.key_ptr = "host", .key_length = 4, .value_ptr = "example.com", .value_length = 11}, + }; + + auto result = envoy_dynamic_module_callback_network_filter_http_callout( + filterPtr(), &callout_id, {"test_cluster", 12}, headers.data(), headers.size(), {nullptr, 0}, + 5000); + + EXPECT_EQ(envoy_dynamic_module_type_http_callout_init_result_CannotCreateRequest, result); +} + +TEST_F(DynamicModuleNetworkFilterHttpCalloutTest, SendHttpCalloutSuccess) { + NiceMock cluster; + NiceMock async_client; + Http::MockAsyncClientRequest request(&async_client); + + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test_cluster")) + .WillOnce(testing::Return(&cluster)); + EXPECT_CALL(cluster, httpAsyncClient()).WillOnce(testing::ReturnRef(async_client)); + EXPECT_CALL(async_client, send_(testing::_, testing::_, testing::_)) + .WillOnce(testing::Return(&request)); + + uint64_t callout_id = 0; + std::vector headers = { + {.key_ptr = ":method", .key_length = 7, .value_ptr = "POST", .value_length = 4}, + {.key_ptr = ":path", .key_length = 5, .value_ptr = "/api/v1/data", .value_length = 12}, + {.key_ptr = "host", .key_length = 4, .value_ptr = "api.example.com", .value_length = 15}, + {.key_ptr = "content-type", + .key_length = 12, + .value_ptr = "application/json", + .value_length = 16}, + }; + + const char* body_data = R"({"key": "value"})"; + envoy_dynamic_module_type_module_buffer body = {body_data, strlen(body_data)}; + + auto result = envoy_dynamic_module_callback_network_filter_http_callout( + filterPtr(), &callout_id, {"test_cluster", 12}, headers.data(), headers.size(), body, 5000); + + EXPECT_EQ(envoy_dynamic_module_type_http_callout_init_result_Success, result); + EXPECT_GT(callout_id, 0); + + EXPECT_CALL(request, cancel()); + filter_.reset(); +} + +TEST_F(DynamicModuleNetworkFilterHttpCalloutTest, SendHttpCalloutSuccessWithCallback) { + NiceMock cluster; + NiceMock async_client; + Http::MockAsyncClientRequest request(&async_client); + const Http::AsyncClient::Callbacks* captured_callback = nullptr; + + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test_cluster")) + .WillOnce(testing::Return(&cluster)); + EXPECT_CALL(cluster, httpAsyncClient()).WillOnce(testing::ReturnRef(async_client)); + EXPECT_CALL(async_client, send_(testing::_, testing::_, testing::_)) + .WillOnce(testing::DoAll(testing::WithArg<1>([&](const Http::AsyncClient::Callbacks& cb) { + captured_callback = &cb; + }), + testing::Return(&request))); + + uint64_t callout_id = 0; + std::vector headers = { + {.key_ptr = ":method", .key_length = 7, .value_ptr = "GET", .value_length = 3}, + {.key_ptr = ":path", .key_length = 5, .value_ptr = "/test", .value_length = 5}, + {.key_ptr = "host", .key_length = 4, .value_ptr = "example.com", .value_length = 11}, + }; + + auto result = envoy_dynamic_module_callback_network_filter_http_callout( + filterPtr(), &callout_id, {"test_cluster", 12}, headers.data(), headers.size(), {nullptr, 0}, + 5000); + + EXPECT_EQ(envoy_dynamic_module_type_http_callout_init_result_Success, result); + EXPECT_GT(callout_id, 0); + EXPECT_NE(nullptr, captured_callback); + + // Simulate a successful response. Note: on_network_filter_http_callout_done_ is nullptr + // for network_no_op module, so the callback will silently return without calling the module. + Http::ResponseMessagePtr response = + std::make_unique(Http::ResponseHeaderMapImpl::create()); + response->headers().setStatus(200); + response->body().add("response body"); + const_cast(captured_callback) + ->onSuccess(request, std::move(response)); + + filter_.reset(); +} + +TEST_F(DynamicModuleNetworkFilterHttpCalloutTest, SendHttpCalloutFailureReset) { + NiceMock cluster; + NiceMock async_client; + Http::MockAsyncClientRequest request(&async_client); + const Http::AsyncClient::Callbacks* captured_callback = nullptr; + + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test_cluster")) + .WillOnce(testing::Return(&cluster)); + EXPECT_CALL(cluster, httpAsyncClient()).WillOnce(testing::ReturnRef(async_client)); + EXPECT_CALL(async_client, send_(testing::_, testing::_, testing::_)) + .WillOnce(testing::DoAll(testing::WithArg<1>([&](const Http::AsyncClient::Callbacks& cb) { + captured_callback = &cb; + }), + testing::Return(&request))); + + uint64_t callout_id = 0; + std::vector headers = { + {.key_ptr = ":method", .key_length = 7, .value_ptr = "GET", .value_length = 3}, + {.key_ptr = ":path", .key_length = 5, .value_ptr = "/test", .value_length = 5}, + {.key_ptr = "host", .key_length = 4, .value_ptr = "example.com", .value_length = 11}, + }; + + auto result = envoy_dynamic_module_callback_network_filter_http_callout( + filterPtr(), &callout_id, {"test_cluster", 12}, headers.data(), headers.size(), {nullptr, 0}, + 5000); + + EXPECT_EQ(envoy_dynamic_module_type_http_callout_init_result_Success, result); + EXPECT_NE(nullptr, captured_callback); + + // Simulate a failure with Reset reason. + const_cast(captured_callback) + ->onFailure(request, Http::AsyncClient::FailureReason::Reset); + + filter_.reset(); +} + +TEST_F(DynamicModuleNetworkFilterHttpCalloutTest, SendHttpCalloutFailureExceedResponseBufferLimit) { + NiceMock cluster; + NiceMock async_client; + Http::MockAsyncClientRequest request(&async_client); + const Http::AsyncClient::Callbacks* captured_callback = nullptr; + + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test_cluster")) + .WillOnce(testing::Return(&cluster)); + EXPECT_CALL(cluster, httpAsyncClient()).WillOnce(testing::ReturnRef(async_client)); + EXPECT_CALL(async_client, send_(testing::_, testing::_, testing::_)) + .WillOnce(testing::DoAll(testing::WithArg<1>([&](const Http::AsyncClient::Callbacks& cb) { + captured_callback = &cb; + }), + testing::Return(&request))); + + uint64_t callout_id = 0; + std::vector headers = { + {.key_ptr = ":method", .key_length = 7, .value_ptr = "GET", .value_length = 3}, + {.key_ptr = ":path", .key_length = 5, .value_ptr = "/test", .value_length = 5}, + {.key_ptr = "host", .key_length = 4, .value_ptr = "example.com", .value_length = 11}, + }; + + auto result = envoy_dynamic_module_callback_network_filter_http_callout( + filterPtr(), &callout_id, {"test_cluster", 12}, headers.data(), headers.size(), {nullptr, 0}, + 5000); + + EXPECT_EQ(envoy_dynamic_module_type_http_callout_init_result_Success, result); + EXPECT_NE(nullptr, captured_callback); + + // Simulate a failure with ExceedResponseBufferLimit reason. + const_cast(captured_callback) + ->onFailure(request, Http::AsyncClient::FailureReason::ExceedResponseBufferLimit); + + filter_.reset(); +} + +TEST_F(DynamicModuleNetworkFilterHttpCalloutTest, OnBeforeFinalizeUpstreamSpanNoop) { + NiceMock cluster; + NiceMock async_client; + Http::MockAsyncClientRequest request(&async_client); + const Http::AsyncClient::Callbacks* captured_callback = nullptr; + + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test_cluster")) + .WillOnce(testing::Return(&cluster)); + EXPECT_CALL(cluster, httpAsyncClient()).WillOnce(testing::ReturnRef(async_client)); + EXPECT_CALL(async_client, send_(testing::_, testing::_, testing::_)) + .WillOnce(testing::DoAll(testing::WithArg<1>([&](const Http::AsyncClient::Callbacks& cb) { + captured_callback = &cb; + }), + testing::Return(&request))); + + uint64_t callout_id = 0; + std::vector headers = { + {.key_ptr = ":method", .key_length = 7, .value_ptr = "GET", .value_length = 3}, + {.key_ptr = ":path", .key_length = 5, .value_ptr = "/test", .value_length = 5}, + {.key_ptr = "host", .key_length = 4, .value_ptr = "example.com", .value_length = 11}, + }; + + auto result = envoy_dynamic_module_callback_network_filter_http_callout( + filterPtr(), &callout_id, {"test_cluster", 12}, headers.data(), headers.size(), {nullptr, 0}, + 5000); + + EXPECT_EQ(envoy_dynamic_module_type_http_callout_init_result_Success, result); + ASSERT_NE(nullptr, captured_callback); + + // No-op path: should be safe to call and not crash. + Envoy::Tracing::MockSpan span; + const_cast(captured_callback) + ->onBeforeFinalizeUpstreamSpan(span, nullptr); + + EXPECT_CALL(request, cancel()); + filter_.reset(); +} + +TEST_F(DynamicModuleNetworkFilterAbiCallbackTest, GetRemoteAddressNullProvider) { + // Return a provider whose remote address is null to hit address == nullptr. + NiceMock cip; + Network::Address::InstanceConstSharedPtr null_addr; + EXPECT_CALL(cip, remoteAddress()).WillOnce(testing::ReturnRef(null_addr)); + EXPECT_CALL(connection_, connectionInfoProvider()).WillOnce(testing::ReturnRef(cip)); + + envoy_dynamic_module_type_envoy_buffer address_out = {nullptr, 0}; + uint32_t port_out = 0; + bool result = envoy_dynamic_module_callback_network_filter_get_remote_address( + filterPtr(), &address_out, &port_out); + + EXPECT_FALSE(result); + EXPECT_EQ(nullptr, address_out.ptr); + EXPECT_EQ(0, address_out.length); + EXPECT_EQ(0, port_out); +} + +TEST_F(DynamicModuleNetworkFilterAbiCallbackTest, SetSocketOptionBytesNullPtr) { + auto state = envoy_dynamic_module_type_socket_option_state_Prebind; + envoy_dynamic_module_type_module_buffer value = {nullptr, 3}; + bool ok = envoy_dynamic_module_callback_network_set_socket_option_bytes(filterPtr(), 1, 2, state, + value); + EXPECT_FALSE(ok); +} + +TEST_F(DynamicModuleNetworkFilterAbiCallbackTest, GetSocketOptionIntNullOut) { + auto state = envoy_dynamic_module_type_socket_option_state_Prebind; + // null output pointer should return false + bool ok = envoy_dynamic_module_callback_network_get_socket_option_int(filterPtr(), 1, 2, state, + nullptr); + EXPECT_FALSE(ok); +} + +TEST_F(DynamicModuleNetworkFilterAbiCallbackTest, GetSocketOptionBytesNullOut) { + auto state = envoy_dynamic_module_type_socket_option_state_Prebind; + bool ok = envoy_dynamic_module_callback_network_get_socket_option_bytes(filterPtr(), 1, 2, state, + nullptr); + EXPECT_FALSE(ok); +} + +TEST_F(DynamicModuleNetworkFilterHttpCalloutTest, FilterDestructionCancelsPendingCallouts) { + NiceMock cluster; + NiceMock async_client; + Http::MockAsyncClientRequest request(&async_client); + + EXPECT_CALL(cluster_manager_, getThreadLocalCluster("test_cluster")) + .WillOnce(testing::Return(&cluster)); + EXPECT_CALL(cluster, httpAsyncClient()).WillOnce(testing::ReturnRef(async_client)); + EXPECT_CALL(async_client, send_(testing::_, testing::_, testing::_)) + .WillOnce(testing::Return(&request)); + + uint64_t callout_id = 0; + std::vector headers = { + {.key_ptr = ":method", .key_length = 7, .value_ptr = "GET", .value_length = 3}, + {.key_ptr = ":path", .key_length = 5, .value_ptr = "/test", .value_length = 5}, + {.key_ptr = "host", .key_length = 4, .value_ptr = "example.com", .value_length = 11}, + }; + + auto result = envoy_dynamic_module_callback_network_filter_http_callout( + filterPtr(), &callout_id, {"test_cluster", 12}, headers.data(), headers.size(), {nullptr, 0}, + 5000); + + EXPECT_EQ(envoy_dynamic_module_type_http_callout_init_result_Success, result); + + EXPECT_CALL(request, cancel()); + // Destroy the filter. This should cancel all pending callouts. + filter_.reset(); +} + } // namespace NetworkFilters } // namespace DynamicModules } // namespace Extensions diff --git a/test/extensions/dynamic_modules/network/filter_test.cc b/test/extensions/dynamic_modules/network/filter_test.cc index 153c1ccffde6d..4866204f316e4 100644 --- a/test/extensions/dynamic_modules/network/filter_test.cc +++ b/test/extensions/dynamic_modules/network/filter_test.cc @@ -3,6 +3,7 @@ #include "test/extensions/dynamic_modules/util.h" #include "test/mocks/network/mocks.h" +#include "test/mocks/upstream/cluster_manager.h" #include "test/test_common/utility.h" namespace Envoy { @@ -16,13 +17,14 @@ class DynamicModuleNetworkFilterTest : public testing::Test { auto dynamic_module = newDynamicModule(testSharedObjectPath("network_no_op", "c"), false); EXPECT_TRUE(dynamic_module.ok()) << dynamic_module.status().message(); - auto filter_config_or_status = - newDynamicModuleNetworkFilterConfig("test_filter", "", std::move(dynamic_module.value())); + auto filter_config_or_status = newDynamicModuleNetworkFilterConfig( + "test_filter", "", std::move(dynamic_module.value()), cluster_manager_); EXPECT_TRUE(filter_config_or_status.ok()) << filter_config_or_status.status().message(); filter_config_ = filter_config_or_status.value(); } DynamicModuleNetworkFilterConfigSharedPtr filter_config_; + NiceMock cluster_manager_; }; TEST_F(DynamicModuleNetworkFilterTest, BasicDataFlow) { @@ -238,8 +240,9 @@ TEST(DynamicModuleNetworkFilterConfigTest, ConfigInitialization) { auto dynamic_module = newDynamicModule(testSharedObjectPath("network_no_op", "c"), false); EXPECT_TRUE(dynamic_module.ok()) << dynamic_module.status().message(); + NiceMock cluster_manager; auto filter_config_or_status = newDynamicModuleNetworkFilterConfig( - "test_filter", "some_config", std::move(dynamic_module.value())); + "test_filter", "some_config", std::move(dynamic_module.value()), cluster_manager); EXPECT_TRUE(filter_config_or_status.ok()); auto config = filter_config_or_status.value(); @@ -258,8 +261,9 @@ TEST(DynamicModuleNetworkFilterConfigTest, MissingSymbols) { auto dynamic_module = newDynamicModule(testSharedObjectPath("no_op", "c"), false); EXPECT_TRUE(dynamic_module.ok()) << dynamic_module.status().message(); - auto filter_config_or_status = - newDynamicModuleNetworkFilterConfig("test_filter", "", std::move(dynamic_module.value())); + NiceMock cluster_manager; + auto filter_config_or_status = newDynamicModuleNetworkFilterConfig( + "test_filter", "", std::move(dynamic_module.value()), cluster_manager); EXPECT_FALSE(filter_config_or_status.ok()); } @@ -269,8 +273,9 @@ TEST(DynamicModuleNetworkFilterConfigTest, ConfigInitializationFailure) { newDynamicModule(testSharedObjectPath("network_config_new_fail", "c"), false); EXPECT_TRUE(dynamic_module.ok()) << dynamic_module.status().message(); - auto filter_config_or_status = - newDynamicModuleNetworkFilterConfig("test_filter", "", std::move(dynamic_module.value())); + NiceMock cluster_manager; + auto filter_config_or_status = newDynamicModuleNetworkFilterConfig( + "test_filter", "", std::move(dynamic_module.value()), cluster_manager); EXPECT_FALSE(filter_config_or_status.ok()); EXPECT_THAT(filter_config_or_status.status().message(), testing::HasSubstr("Failed to initialize")); @@ -281,8 +286,9 @@ TEST(DynamicModuleNetworkFilterConfigTest, StopIterationStatus) { newDynamicModule(testSharedObjectPath("network_stop_iteration", "c"), false); EXPECT_TRUE(dynamic_module.ok()) << dynamic_module.status().message(); - auto filter_config_or_status = - newDynamicModuleNetworkFilterConfig("test_filter", "", std::move(dynamic_module.value())); + NiceMock cluster_manager; + auto filter_config_or_status = newDynamicModuleNetworkFilterConfig( + "test_filter", "", std::move(dynamic_module.value()), cluster_manager); EXPECT_TRUE(filter_config_or_status.ok()); auto config = filter_config_or_status.value(); diff --git a/test/extensions/dynamic_modules/test_data/c/network_no_op.c b/test/extensions/dynamic_modules/test_data/c/network_no_op.c index 87483cd53a55a..c1b519f01043f 100644 --- a/test/extensions/dynamic_modules/test_data/c/network_no_op.c +++ b/test/extensions/dynamic_modules/test_data/c/network_no_op.c @@ -64,3 +64,19 @@ void envoy_dynamic_module_on_network_filter_destroy( envoy_dynamic_module_type_network_filter_module_ptr filter_module_ptr) { assert(filter_module_ptr == &some_variable + 1); } + +void envoy_dynamic_module_on_network_filter_http_callout_done( + envoy_dynamic_module_type_network_filter_envoy_ptr filter_envoy_ptr, + envoy_dynamic_module_type_network_filter_module_ptr filter_module_ptr, uint64_t callout_id, + envoy_dynamic_module_type_http_callout_result result, + envoy_dynamic_module_type_envoy_http_header* headers, size_t headers_count, + envoy_dynamic_module_type_envoy_buffer* body_chunks, size_t body_chunks_count) { + (void)filter_envoy_ptr; + (void)filter_module_ptr; + (void)callout_id; + (void)result; + (void)headers; + (void)headers_count; + (void)body_chunks; + (void)body_chunks_count; +}