diff --git a/source/extensions/dynamic_modules/BUILD b/source/extensions/dynamic_modules/BUILD index 7c5922745b797..fc39afb094fdb 100644 --- a/source/extensions/dynamic_modules/BUILD +++ b/source/extensions/dynamic_modules/BUILD @@ -49,23 +49,71 @@ envoy_cc_library( alwayslink = True, ) +# A meta-target that aggregates every per-surface abi_impl-style C++ library that +# implements the `envoy_dynamic_module_callback_*` host callbacks. +# +# The Go SDK's `abi/*.go` package is monolithic — importing it pulls in references to +# every surface's callbacks. So a Go test_data .so always references all of them. Tests +# that load such .so files must provide all of those symbols in the test binary's +# symbol table; otherwise dlopen's RTLD_LAZY can't resolve them and load fails. This +# meta-target lets a test depend on a single line and get every callback the Go SDK +# may have referenced. +envoy_cc_library( + name = "all_abi_impls", + deps = [ + ":abi_impl", + "//source/extensions/access_loggers/dynamic_modules:access_log_lib", + "//source/extensions/bootstrap/dynamic_modules:abi_impl", + "//source/extensions/clusters/dynamic_modules:cluster_lib", + "//source/extensions/filters/http/dynamic_modules:abi_impl", + "//source/extensions/filters/http/dynamic_modules:filter_lib", + "//source/extensions/filters/listener/dynamic_modules:filter_lib", + "//source/extensions/filters/network/dynamic_modules:filter_lib", + "//source/extensions/filters/udp/dynamic_modules:filter_lib", + "//source/extensions/load_balancing_policies/dynamic_modules:abi_impl", + "//source/extensions/matching/input_matchers/dynamic_modules:matcher_lib", + "//source/extensions/tracers/dynamic_modules:abi_impl", + "//source/extensions/transport_sockets/tls/cert_validator/dynamic_modules:config", + "//source/extensions/upstreams/http/dynamic_modules:abi_impl", + ], + alwayslink = True, +) + +# The shared package is pure Go (no cgo) — interfaces, types, and EmptyXxx no-ops shared +# across the SDK and module code. Modules depend on this transitively via go_sdk and +# go_sdk_abi. go_library( name = "go_sdk_shared", srcs = [ - "sdk/go/shared/api.go", - "sdk/go/shared/base.go", + "sdk/go/shared/access_log.go", + "sdk/go/shared/bootstrap.go", + "sdk/go/shared/cert_validator.go", + "sdk/go/shared/cluster.go", + "sdk/go/shared/dns_resolver.go", + "sdk/go/shared/http.go", + "sdk/go/shared/listener_api.go", + "sdk/go/shared/listener_base.go", + "sdk/go/shared/load_balancer.go", + "sdk/go/shared/matcher.go", "sdk/go/shared/network_api.go", "sdk/go/shared/network_base.go", + "sdk/go/shared/program.go", + "sdk/go/shared/tracer.go", + "sdk/go/shared/transport_socket.go", + "sdk/go/shared/types.go", + "sdk/go/shared/udp_listener_api.go", + "sdk/go/shared/udp_listener_base.go", + "sdk/go/shared/upstream_http_tcp_bridge.go", ], - cgo = True, importpath = "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared", visibility = ["//visibility:public"], ) +# The top-level sdk package — pure Go. Holds the factory registries and program-handle +# indirection. Imports shared. go_library( name = "go_sdk", srcs = ["sdk/go/sdk.go"], - cgo = True, importpath = "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go", visibility = ["//visibility:public"], deps = [ @@ -73,11 +121,30 @@ go_library( ], ) +# The abi package — cgo-heavy. Implements the C-callable trampolines that Envoy invokes +# and the Go-callable wrappers that bridge into Envoy's host callbacks. go_library( name = "go_sdk_abi", srcs = [ - "sdk/go/abi/internal.go", + "sdk/go/abi/access_log.go", + "sdk/go/abi/bootstrap.go", + # Stand-alone .c file that defines C trampolines passed by address from + # Go to Envoy. Cannot live in the cgo preamble; see file header for why. + "sdk/go/abi/bootstrap_trampolines.c", + "sdk/go/abi/cert_validator.go", + "sdk/go/abi/cluster.go", + "sdk/go/abi/dns_resolver.go", + "sdk/go/abi/http.go", + "sdk/go/abi/listener.go", + "sdk/go/abi/load_balancer.go", + "sdk/go/abi/matcher.go", "sdk/go/abi/network.go", + "sdk/go/abi/program.go", + "sdk/go/abi/scheduler.go", + "sdk/go/abi/tracer.go", + "sdk/go/abi/transport_socket.go", + "sdk/go/abi/udp_listener.go", + "sdk/go/abi/upstream_http_tcp_bridge.go", "//source/extensions/dynamic_modules/abi:abi.h", ], cgo = True, diff --git a/source/extensions/dynamic_modules/builtin_extensions/hickory_dns.rs b/source/extensions/dynamic_modules/builtin_extensions/hickory_dns.rs index 77bfd3c40d00a..f639dc722b1ef 100644 --- a/source/extensions/dynamic_modules/builtin_extensions/hickory_dns.rs +++ b/source/extensions/dynamic_modules/builtin_extensions/hickory_dns.rs @@ -116,7 +116,7 @@ fn parse_proto_duration(s: &str) -> Option { if let Some(stripped) = s.strip_suffix('s') { if let Some((whole, frac)) = stripped.split_once('.') { let secs: u64 = whole.parse().ok()?; - let nanos: u32 = format!("{:0<9}", frac)[..9].parse().ok()?; + let nanos: u32 = format!("{frac:0<9}")[..9].parse().ok()?; Some(std::time::Duration::new(secs, nanos)) } else { let secs: u64 = stripped.parse().ok()?; diff --git a/source/extensions/dynamic_modules/sdk/go/abi/access_log.go b/source/extensions/dynamic_modules/sdk/go/abi/access_log.go new file mode 100644 index 0000000000000..5352944681d23 --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/abi/access_log.go @@ -0,0 +1,652 @@ +package abi + +/* +#cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup +#cgo linux LDFLAGS: -Wl,--unresolved-symbols=ignore-all +#include +#include +#include +#include +#include "../../../abi/abi.h" +*/ +import "C" +import ( + "runtime" + "unsafe" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +type accessLoggerConfigWrapper struct { + factory shared.AccessLoggerFactory + configHandle *dymAccessLoggerConfigHandle + + // destroyed is set during config_destroy so any late logger creation becomes a + // no-op instead of re-entering user code. + destroyed bool +} + +type accessLoggerWrapper struct { + logger shared.AccessLogger + + // destroyed is set during logger_destroy so any late log / flush callbacks become + // no-ops. + destroyed bool +} + +var accessLoggerConfigManager = newManager[accessLoggerConfigWrapper]() +var accessLoggerManager = newManager[accessLoggerWrapper]() + +// dymAccessLoggerConfigHandle implements shared.AccessLoggerConfigHandle. +type dymAccessLoggerConfigHandle struct { + hostConfigPtr C.envoy_dynamic_module_type_access_logger_config_envoy_ptr +} + +func (h *dymAccessLoggerConfigHandle) DefineCounter(name string) (shared.MetricID, shared.MetricsResult) { + var id C.size_t + ret := C.envoy_dynamic_module_callback_access_logger_config_define_counter( + h.hostConfigPtr, stringToModuleBuffer(name), &id) + runtime.KeepAlive(name) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymAccessLoggerConfigHandle) DefineGauge(name string) (shared.MetricID, shared.MetricsResult) { + var id C.size_t + ret := C.envoy_dynamic_module_callback_access_logger_config_define_gauge( + h.hostConfigPtr, stringToModuleBuffer(name), &id) + runtime.KeepAlive(name) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymAccessLoggerConfigHandle) DefineHistogram(name string) (shared.MetricID, shared.MetricsResult) { + var id C.size_t + ret := C.envoy_dynamic_module_callback_access_logger_config_define_histogram( + h.hostConfigPtr, stringToModuleBuffer(name), &id) + runtime.KeepAlive(name) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymAccessLoggerConfigHandle) IncrementCounter(id shared.MetricID, value uint64) shared.MetricsResult { + return shared.MetricsResult(C.envoy_dynamic_module_callback_access_logger_increment_counter( + h.hostConfigPtr, C.size_t(uint64(id)), C.uint64_t(value))) +} + +func (h *dymAccessLoggerConfigHandle) SetGauge(id shared.MetricID, value uint64) shared.MetricsResult { + return shared.MetricsResult(C.envoy_dynamic_module_callback_access_logger_set_gauge( + h.hostConfigPtr, C.size_t(uint64(id)), C.uint64_t(value))) +} + +func (h *dymAccessLoggerConfigHandle) IncrementGauge(id shared.MetricID, value uint64) shared.MetricsResult { + return shared.MetricsResult(C.envoy_dynamic_module_callback_access_logger_increment_gauge( + h.hostConfigPtr, C.size_t(uint64(id)), C.uint64_t(value))) +} + +func (h *dymAccessLoggerConfigHandle) DecrementGauge(id shared.MetricID, value uint64) shared.MetricsResult { + return shared.MetricsResult(C.envoy_dynamic_module_callback_access_logger_decrement_gauge( + h.hostConfigPtr, C.size_t(uint64(id)), C.uint64_t(value))) +} + +func (h *dymAccessLoggerConfigHandle) RecordHistogramValue(id shared.MetricID, value uint64) shared.MetricsResult { + return shared.MetricsResult(C.envoy_dynamic_module_callback_access_logger_record_histogram_value( + h.hostConfigPtr, C.size_t(uint64(id)), C.uint64_t(value))) +} + +// dymAccessLogContext implements shared.AccessLogContext. It is bound to a single Log call — +// the underlying envoy ptr is only valid for that callback. +type dymAccessLogContext struct { + hostLoggerPtr C.envoy_dynamic_module_type_access_logger_envoy_ptr +} + +// helper: returns (UnsafeEnvoyBuffer, true) when the callback returned true and produced a +// non-empty buffer. +func accessLogStrResult(buf C.envoy_dynamic_module_type_envoy_buffer, ok C.bool) (shared.UnsafeEnvoyBuffer, bool) { + if !bool(ok) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ok) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func accessLogAddrResult(buf C.envoy_dynamic_module_type_envoy_buffer, port C.uint32_t, ok C.bool) (shared.UnsafeEnvoyBuffer, uint32, bool) { + if !bool(ok) { + return shared.UnsafeEnvoyBuffer{}, 0, false + } + return envoyBufferToUnsafeEnvoyBuffer(buf), uint32(port), true +} + +// ---- headers ---- + +func (c *dymAccessLogContext) GetHeadersSize(headerType shared.HttpHeaderType) uint64 { + return uint64(C.envoy_dynamic_module_callback_access_logger_get_headers_size( + c.hostLoggerPtr, C.envoy_dynamic_module_type_http_header_type(headerType))) +} + +func (c *dymAccessLogContext) GetHeaders(headerType shared.HttpHeaderType) [][2]shared.UnsafeEnvoyBuffer { + size := C.envoy_dynamic_module_callback_access_logger_get_headers_size( + c.hostLoggerPtr, C.envoy_dynamic_module_type_http_header_type(headerType)) + if size == 0 { + return nil + } + hdrs := make([]C.envoy_dynamic_module_type_envoy_http_header, int(size)) + if !bool(C.envoy_dynamic_module_callback_access_logger_get_headers( + c.hostLoggerPtr, C.envoy_dynamic_module_type_http_header_type(headerType), + unsafe.SliceData(hdrs))) { + return nil + } + out := envoyHttpHeaderSliceToUnsafeHeaderSlice(hdrs) + runtime.KeepAlive(hdrs) + return out +} + +func (c *dymAccessLogContext) GetHeaderValue(headerType shared.HttpHeaderType, key string, index uint64) (shared.UnsafeEnvoyBuffer, uint64, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var total C.size_t + ret := C.envoy_dynamic_module_callback_access_logger_get_header_value( + c.hostLoggerPtr, + C.envoy_dynamic_module_type_http_header_type(headerType), + stringToModuleBuffer(key), + &buf, + C.size_t(index), + &total, + ) + runtime.KeepAlive(key) + if !bool(ret) { + return shared.UnsafeEnvoyBuffer{}, uint64(total), false + } + if buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, uint64(total), true + } + return envoyBufferToUnsafeEnvoyBuffer(buf), uint64(total), true +} + +// ---- attribute accessors ---- + +func (c *dymAccessLogContext) GetAttributeString(id shared.AttributeID) (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_access_logger_get_attribute_string( + c.hostLoggerPtr, C.envoy_dynamic_module_type_attribute_id(id), &buf) + return accessLogStrResult(buf, ok) +} + +func (c *dymAccessLogContext) GetAttributeNumber(id shared.AttributeID) (uint64, bool) { + var v C.uint64_t + ok := C.envoy_dynamic_module_callback_access_logger_get_attribute_int( + c.hostLoggerPtr, C.envoy_dynamic_module_type_attribute_id(id), &v) + if !bool(ok) { + return 0, false + } + return uint64(v), true +} + +func (c *dymAccessLogContext) GetAttributeBool(id shared.AttributeID) (bool, bool) { + var v C.bool + ok := C.envoy_dynamic_module_callback_access_logger_get_attribute_bool( + c.hostLoggerPtr, C.envoy_dynamic_module_type_attribute_id(id), &v) + if !bool(ok) { + return false, false + } + return bool(v), true +} + +// ---- response flags / timing / bytes ---- + +func (c *dymAccessLogContext) HasResponseFlag(flag shared.ResponseFlag) bool { + return bool(C.envoy_dynamic_module_callback_access_logger_has_response_flag( + c.hostLoggerPtr, C.envoy_dynamic_module_type_response_flag(flag))) +} + +func (c *dymAccessLogContext) GetResponseFlags() uint64 { + return uint64(C.envoy_dynamic_module_callback_access_logger_get_response_flags(c.hostLoggerPtr)) +} + +func (c *dymAccessLogContext) GetTimingInfo() shared.AccessLogTimingInfo { + var t C.envoy_dynamic_module_type_timing_info + C.envoy_dynamic_module_callback_access_logger_get_timing_info(c.hostLoggerPtr, &t) + return shared.AccessLogTimingInfo{ + StartTimeUnixNs: int64(t.start_time_unix_ns), + RequestCompleteDurationNs: int64(t.request_complete_duration_ns), + FirstUpstreamTxByteSentNs: int64(t.first_upstream_tx_byte_sent_ns), + LastUpstreamTxByteSentNs: int64(t.last_upstream_tx_byte_sent_ns), + FirstUpstreamRxByteReceivedNs: int64(t.first_upstream_rx_byte_received_ns), + LastUpstreamRxByteReceivedNs: int64(t.last_upstream_rx_byte_received_ns), + FirstDownstreamTxByteSentNs: int64(t.first_downstream_tx_byte_sent_ns), + LastDownstreamTxByteSentNs: int64(t.last_downstream_tx_byte_sent_ns), + } +} + +func (c *dymAccessLogContext) GetBytesInfo() shared.AccessLogBytesInfo { + var b C.envoy_dynamic_module_type_bytes_info + C.envoy_dynamic_module_callback_access_logger_get_bytes_info(c.hostLoggerPtr, &b) + return shared.AccessLogBytesInfo{ + BytesReceived: uint64(b.bytes_received), + BytesSent: uint64(b.bytes_sent), + WireBytesReceived: uint64(b.wire_bytes_received), + WireBytesSent: uint64(b.wire_bytes_sent), + } +} + +// ---- addresses ---- + +func (c *dymAccessLogContext) GetDownstreamRemoteAddress() (shared.UnsafeEnvoyBuffer, uint32, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var port C.uint32_t + ok := C.envoy_dynamic_module_callback_access_logger_get_downstream_remote_address(c.hostLoggerPtr, &buf, &port) + return accessLogAddrResult(buf, port, ok) +} + +func (c *dymAccessLogContext) GetDownstreamLocalAddress() (shared.UnsafeEnvoyBuffer, uint32, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var port C.uint32_t + ok := C.envoy_dynamic_module_callback_access_logger_get_downstream_local_address(c.hostLoggerPtr, &buf, &port) + return accessLogAddrResult(buf, port, ok) +} + +func (c *dymAccessLogContext) GetDownstreamDirectRemoteAddress() (shared.UnsafeEnvoyBuffer, uint32, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var port C.uint32_t + ok := C.envoy_dynamic_module_callback_access_logger_get_downstream_direct_remote_address(c.hostLoggerPtr, &buf, &port) + return accessLogAddrResult(buf, port, ok) +} + +func (c *dymAccessLogContext) GetDownstreamDirectLocalAddress() (shared.UnsafeEnvoyBuffer, uint32, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var port C.uint32_t + ok := C.envoy_dynamic_module_callback_access_logger_get_downstream_direct_local_address(c.hostLoggerPtr, &buf, &port) + return accessLogAddrResult(buf, port, ok) +} + +func (c *dymAccessLogContext) GetUpstreamRemoteAddress() (shared.UnsafeEnvoyBuffer, uint32, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var port C.uint32_t + ok := C.envoy_dynamic_module_callback_access_logger_get_upstream_remote_address(c.hostLoggerPtr, &buf, &port) + return accessLogAddrResult(buf, port, ok) +} + +func (c *dymAccessLogContext) GetUpstreamLocalAddress() (shared.UnsafeEnvoyBuffer, uint32, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var port C.uint32_t + ok := C.envoy_dynamic_module_callback_access_logger_get_upstream_local_address(c.hostLoggerPtr, &buf, &port) + return accessLogAddrResult(buf, port, ok) +} + +// ---- upstream info ---- + +func (c *dymAccessLogContext) GetUpstreamCluster() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_access_logger_get_upstream_cluster(c.hostLoggerPtr, &buf) + return accessLogStrResult(buf, ok) +} + +func (c *dymAccessLogContext) GetUpstreamHost() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_access_logger_get_upstream_host(c.hostLoggerPtr, &buf) + return accessLogStrResult(buf, ok) +} + +func (c *dymAccessLogContext) GetUpstreamConnectionID() uint64 { + return uint64(C.envoy_dynamic_module_callback_access_logger_get_upstream_connection_id(c.hostLoggerPtr)) +} + +func (c *dymAccessLogContext) GetUpstreamTLSCipher() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_access_logger_get_upstream_tls_cipher(c.hostLoggerPtr, &buf) + return accessLogStrResult(buf, ok) +} + +func (c *dymAccessLogContext) GetUpstreamTLSSessionID() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_access_logger_get_upstream_tls_session_id(c.hostLoggerPtr, &buf) + return accessLogStrResult(buf, ok) +} + +func (c *dymAccessLogContext) GetUpstreamPeerIssuer() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_access_logger_get_upstream_peer_issuer(c.hostLoggerPtr, &buf) + return accessLogStrResult(buf, ok) +} + +func (c *dymAccessLogContext) GetUpstreamPeerCertValidityStart() int64 { + return int64(C.envoy_dynamic_module_callback_access_logger_get_upstream_peer_cert_v_start(c.hostLoggerPtr)) +} + +func (c *dymAccessLogContext) GetUpstreamPeerCertValidityEnd() int64 { + return int64(C.envoy_dynamic_module_callback_access_logger_get_upstream_peer_cert_v_end(c.hostLoggerPtr)) +} + +func (c *dymAccessLogContext) sansViaSize( + sizeFn func(C.envoy_dynamic_module_type_access_logger_envoy_ptr) C.size_t, + getFn func(C.envoy_dynamic_module_type_access_logger_envoy_ptr, *C.envoy_dynamic_module_type_envoy_buffer) C.bool, +) []shared.UnsafeEnvoyBuffer { + size := sizeFn(c.hostLoggerPtr) + if size == 0 { + return nil + } + bufs := make([]C.envoy_dynamic_module_type_envoy_buffer, int(size)) + if !bool(getFn(c.hostLoggerPtr, unsafe.SliceData(bufs))) { + return nil + } + out := envoyBufferSliceToUnsafeEnvoyBufferSlice(bufs) + runtime.KeepAlive(bufs) + return out +} + +func (c *dymAccessLogContext) GetUpstreamPeerURISans() []shared.UnsafeEnvoyBuffer { + return c.sansViaSize( + func(p C.envoy_dynamic_module_type_access_logger_envoy_ptr) C.size_t { + return C.envoy_dynamic_module_callback_access_logger_get_upstream_peer_uri_san_size(p) + }, + func(p C.envoy_dynamic_module_type_access_logger_envoy_ptr, b *C.envoy_dynamic_module_type_envoy_buffer) C.bool { + return C.envoy_dynamic_module_callback_access_logger_get_upstream_peer_uri_san(p, b) + }, + ) +} + +func (c *dymAccessLogContext) GetUpstreamLocalURISans() []shared.UnsafeEnvoyBuffer { + return c.sansViaSize( + func(p C.envoy_dynamic_module_type_access_logger_envoy_ptr) C.size_t { + return C.envoy_dynamic_module_callback_access_logger_get_upstream_local_uri_san_size(p) + }, + func(p C.envoy_dynamic_module_type_access_logger_envoy_ptr, b *C.envoy_dynamic_module_type_envoy_buffer) C.bool { + return C.envoy_dynamic_module_callback_access_logger_get_upstream_local_uri_san(p, b) + }, + ) +} + +func (c *dymAccessLogContext) GetUpstreamPeerDNSSans() []shared.UnsafeEnvoyBuffer { + return c.sansViaSize( + func(p C.envoy_dynamic_module_type_access_logger_envoy_ptr) C.size_t { + return C.envoy_dynamic_module_callback_access_logger_get_upstream_peer_dns_san_size(p) + }, + func(p C.envoy_dynamic_module_type_access_logger_envoy_ptr, b *C.envoy_dynamic_module_type_envoy_buffer) C.bool { + return C.envoy_dynamic_module_callback_access_logger_get_upstream_peer_dns_san(p, b) + }, + ) +} + +func (c *dymAccessLogContext) GetUpstreamLocalDNSSans() []shared.UnsafeEnvoyBuffer { + return c.sansViaSize( + func(p C.envoy_dynamic_module_type_access_logger_envoy_ptr) C.size_t { + return C.envoy_dynamic_module_callback_access_logger_get_upstream_local_dns_san_size(p) + }, + func(p C.envoy_dynamic_module_type_access_logger_envoy_ptr, b *C.envoy_dynamic_module_type_envoy_buffer) C.bool { + return C.envoy_dynamic_module_callback_access_logger_get_upstream_local_dns_san(p, b) + }, + ) +} + +// ---- downstream connection / TLS info ---- + +func (c *dymAccessLogContext) GetDownstreamTLSCipher() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_access_logger_get_downstream_tls_cipher(c.hostLoggerPtr, &buf) + return accessLogStrResult(buf, ok) +} + +func (c *dymAccessLogContext) GetDownstreamTLSSessionID() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_access_logger_get_downstream_tls_session_id(c.hostLoggerPtr, &buf) + return accessLogStrResult(buf, ok) +} + +func (c *dymAccessLogContext) GetDownstreamPeerIssuer() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_access_logger_get_downstream_peer_issuer(c.hostLoggerPtr, &buf) + return accessLogStrResult(buf, ok) +} + +func (c *dymAccessLogContext) GetDownstreamPeerSerial() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_access_logger_get_downstream_peer_serial(c.hostLoggerPtr, &buf) + return accessLogStrResult(buf, ok) +} + +func (c *dymAccessLogContext) GetDownstreamPeerFingerprint1() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_access_logger_get_downstream_peer_fingerprint_1(c.hostLoggerPtr, &buf) + return accessLogStrResult(buf, ok) +} + +func (c *dymAccessLogContext) GetDownstreamPeerCertPresented() bool { + return bool(C.envoy_dynamic_module_callback_access_logger_get_downstream_peer_cert_presented(c.hostLoggerPtr)) +} + +func (c *dymAccessLogContext) GetDownstreamPeerCertValidated() bool { + return bool(C.envoy_dynamic_module_callback_access_logger_get_downstream_peer_cert_validated(c.hostLoggerPtr)) +} + +func (c *dymAccessLogContext) GetDownstreamPeerCertValidityStart() int64 { + return int64(C.envoy_dynamic_module_callback_access_logger_get_downstream_peer_cert_v_start(c.hostLoggerPtr)) +} + +func (c *dymAccessLogContext) GetDownstreamPeerCertValidityEnd() int64 { + return int64(C.envoy_dynamic_module_callback_access_logger_get_downstream_peer_cert_v_end(c.hostLoggerPtr)) +} + +func (c *dymAccessLogContext) GetDownstreamPeerURISans() []shared.UnsafeEnvoyBuffer { + return c.sansViaSize( + func(p C.envoy_dynamic_module_type_access_logger_envoy_ptr) C.size_t { + return C.envoy_dynamic_module_callback_access_logger_get_downstream_peer_uri_san_size(p) + }, + func(p C.envoy_dynamic_module_type_access_logger_envoy_ptr, b *C.envoy_dynamic_module_type_envoy_buffer) C.bool { + return C.envoy_dynamic_module_callback_access_logger_get_downstream_peer_uri_san(p, b) + }, + ) +} + +func (c *dymAccessLogContext) GetDownstreamLocalURISans() []shared.UnsafeEnvoyBuffer { + return c.sansViaSize( + func(p C.envoy_dynamic_module_type_access_logger_envoy_ptr) C.size_t { + return C.envoy_dynamic_module_callback_access_logger_get_downstream_local_uri_san_size(p) + }, + func(p C.envoy_dynamic_module_type_access_logger_envoy_ptr, b *C.envoy_dynamic_module_type_envoy_buffer) C.bool { + return C.envoy_dynamic_module_callback_access_logger_get_downstream_local_uri_san(p, b) + }, + ) +} + +func (c *dymAccessLogContext) GetDownstreamPeerDNSSans() []shared.UnsafeEnvoyBuffer { + return c.sansViaSize( + func(p C.envoy_dynamic_module_type_access_logger_envoy_ptr) C.size_t { + return C.envoy_dynamic_module_callback_access_logger_get_downstream_peer_dns_san_size(p) + }, + func(p C.envoy_dynamic_module_type_access_logger_envoy_ptr, b *C.envoy_dynamic_module_type_envoy_buffer) C.bool { + return C.envoy_dynamic_module_callback_access_logger_get_downstream_peer_dns_san(p, b) + }, + ) +} + +func (c *dymAccessLogContext) GetDownstreamLocalDNSSans() []shared.UnsafeEnvoyBuffer { + return c.sansViaSize( + func(p C.envoy_dynamic_module_type_access_logger_envoy_ptr) C.size_t { + return C.envoy_dynamic_module_callback_access_logger_get_downstream_local_dns_san_size(p) + }, + func(p C.envoy_dynamic_module_type_access_logger_envoy_ptr, b *C.envoy_dynamic_module_type_envoy_buffer) C.bool { + return C.envoy_dynamic_module_callback_access_logger_get_downstream_local_dns_san(p, b) + }, + ) +} + +// ---- metadata / filter state / tracing ---- + +func (c *dymAccessLogContext) GetDynamicMetadata(filterName, path string) (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_access_logger_get_dynamic_metadata( + c.hostLoggerPtr, stringToModuleBuffer(filterName), stringToModuleBuffer(path), &buf) + runtime.KeepAlive(filterName) + runtime.KeepAlive(path) + return accessLogStrResult(buf, ok) +} + +func (c *dymAccessLogContext) GetFilterState(key string) (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_access_logger_get_filter_state( + c.hostLoggerPtr, stringToModuleBuffer(key), &buf) + runtime.KeepAlive(key) + return accessLogStrResult(buf, ok) +} + +func (c *dymAccessLogContext) GetLocalReplyBody() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_access_logger_get_local_reply_body(c.hostLoggerPtr, &buf) + return accessLogStrResult(buf, ok) +} + +func (c *dymAccessLogContext) GetTraceID() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_access_logger_get_trace_id(c.hostLoggerPtr, &buf) + return accessLogStrResult(buf, ok) +} + +func (c *dymAccessLogContext) GetSpanID() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_access_logger_get_span_id(c.hostLoggerPtr, &buf) + return accessLogStrResult(buf, ok) +} + +func (c *dymAccessLogContext) IsTraceSampled() bool { + return bool(C.envoy_dynamic_module_callback_access_logger_is_trace_sampled(c.hostLoggerPtr)) +} + +// ---- additional stream info ---- + +func (c *dymAccessLogContext) GetJA3Hash() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_access_logger_get_ja3_hash(c.hostLoggerPtr, &buf) + return accessLogStrResult(buf, ok) +} + +func (c *dymAccessLogContext) GetJA4Hash() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_access_logger_get_ja4_hash(c.hostLoggerPtr, &buf) + return accessLogStrResult(buf, ok) +} + +func (c *dymAccessLogContext) GetRequestHeadersBytes() uint64 { + return uint64(C.envoy_dynamic_module_callback_access_logger_get_request_headers_bytes(c.hostLoggerPtr)) +} + +func (c *dymAccessLogContext) GetResponseHeadersBytes() uint64 { + return uint64(C.envoy_dynamic_module_callback_access_logger_get_response_headers_bytes(c.hostLoggerPtr)) +} + +func (c *dymAccessLogContext) GetResponseTrailersBytes() uint64 { + return uint64(C.envoy_dynamic_module_callback_access_logger_get_response_trailers_bytes(c.hostLoggerPtr)) +} + +func (c *dymAccessLogContext) GetUpstreamProtocol() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_access_logger_get_upstream_protocol(c.hostLoggerPtr, &buf) + return accessLogStrResult(buf, ok) +} + +func (c *dymAccessLogContext) GetUpstreamPoolReadyDurationNs() int64 { + return int64(C.envoy_dynamic_module_callback_access_logger_get_upstream_pool_ready_duration_ns(c.hostLoggerPtr)) +} + +func (c *dymAccessLogContext) GetWorkerIndex() uint32 { + return uint32(C.envoy_dynamic_module_callback_access_logger_get_worker_index(c.hostLoggerPtr)) +} + +// ============================================================================= +// Event hooks +// ============================================================================= + +//export envoy_dynamic_module_on_access_logger_config_new +func envoy_dynamic_module_on_access_logger_config_new( + hostConfigPtr C.envoy_dynamic_module_type_access_logger_config_envoy_ptr, + name C.envoy_dynamic_module_type_envoy_buffer, + config C.envoy_dynamic_module_type_envoy_buffer, +) C.envoy_dynamic_module_type_access_logger_config_module_ptr { + nameStr := envoyBufferToStringUnsafe(name) + configBytes := envoyBufferToBytesCopy(config) + + configHandle := &dymAccessLoggerConfigHandle{hostConfigPtr: hostConfigPtr} + configFactory := sdk.GetAccessLoggerConfigFactory(nameStr) + if configFactory == nil { + hostLog(shared.LogLevelWarn, "Failed to load access logger configuration for %q: no factory registered", []any{nameStr}) + return nil + } + factory, err := configFactory.Create(configHandle, configBytes) + if err != nil { + hostLog(shared.LogLevelWarn, "Failed to load access logger configuration for %q: %v", []any{nameStr, err}) + return nil + } + if factory == nil { + hostLog(shared.LogLevelWarn, "Failed to load access logger configuration for %q: factory returned nil", []any{nameStr}) + return nil + } + wrapper := &accessLoggerConfigWrapper{factory: factory, configHandle: configHandle} + configPtr := accessLoggerConfigManager.record(wrapper) + return C.envoy_dynamic_module_type_access_logger_config_module_ptr(configPtr) +} + +//export envoy_dynamic_module_on_access_logger_config_destroy +func envoy_dynamic_module_on_access_logger_config_destroy( + configPtr C.envoy_dynamic_module_type_access_logger_config_module_ptr, +) { + wrapper := accessLoggerConfigManager.unwrap(unsafe.Pointer(configPtr)) + if wrapper == nil || wrapper.destroyed { + return + } + wrapper.destroyed = true + wrapper.factory.OnDestroy() + accessLoggerConfigManager.remove(unsafe.Pointer(configPtr)) +} + +//export envoy_dynamic_module_on_access_logger_new +func envoy_dynamic_module_on_access_logger_new( + configPtr C.envoy_dynamic_module_type_access_logger_config_module_ptr, + _ C.envoy_dynamic_module_type_access_logger_envoy_ptr, +) C.envoy_dynamic_module_type_access_logger_module_ptr { + cfg := accessLoggerConfigManager.unwrap(unsafe.Pointer(configPtr)) + if cfg == nil || cfg.destroyed { + return nil + } + logger := cfg.factory.Create() + if logger == nil { + return nil + } + wrapper := &accessLoggerWrapper{logger: logger} + loggerPtr := accessLoggerManager.record(wrapper) + return C.envoy_dynamic_module_type_access_logger_module_ptr(loggerPtr) +} + +//export envoy_dynamic_module_on_access_logger_log +func envoy_dynamic_module_on_access_logger_log( + hostLoggerPtr C.envoy_dynamic_module_type_access_logger_envoy_ptr, + loggerPtr C.envoy_dynamic_module_type_access_logger_module_ptr, + logType C.envoy_dynamic_module_type_access_log_type, +) { + w := accessLoggerManager.unwrap(unsafe.Pointer(loggerPtr)) + if w == nil || w.logger == nil || w.destroyed { + return + } + ctx := &dymAccessLogContext{hostLoggerPtr: hostLoggerPtr} + w.logger.Log(ctx, shared.AccessLogType(logType)) +} + +//export envoy_dynamic_module_on_access_logger_destroy +func envoy_dynamic_module_on_access_logger_destroy( + loggerPtr C.envoy_dynamic_module_type_access_logger_module_ptr, +) { + w := accessLoggerManager.unwrap(unsafe.Pointer(loggerPtr)) + if w == nil || w.destroyed { + return + } + w.destroyed = true + if w.logger != nil { + w.logger.OnDestroy() + } + accessLoggerManager.remove(unsafe.Pointer(loggerPtr)) +} + +//export envoy_dynamic_module_on_access_logger_flush +func envoy_dynamic_module_on_access_logger_flush( + loggerPtr C.envoy_dynamic_module_type_access_logger_module_ptr, +) { + w := accessLoggerManager.unwrap(unsafe.Pointer(loggerPtr)) + if w == nil || w.logger == nil || w.destroyed { + return + } + w.logger.Flush() +} diff --git a/source/extensions/dynamic_modules/sdk/go/abi/bootstrap.go b/source/extensions/dynamic_modules/sdk/go/abi/bootstrap.go new file mode 100644 index 0000000000000..6d6e1d4e70790 --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/abi/bootstrap.go @@ -0,0 +1,823 @@ +package abi + +/* +#cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup +#cgo linux LDFLAGS: -Wl,--unresolved-symbols=ignore-all +#include +#include +#include +#include +#include "../../../abi/abi.h" + +// cgoInvokeEventCb invokes Envoy's completion callback. The completion callback is a +// plain C function pointer Envoy hands us via the *_shutdown export hooks; calling it +// from Go via cgo would require turning the function-pointer field into a Go-callable +// value, so we route through this tiny C wrapper instead. +static inline void cgoInvokeEventCb(envoy_dynamic_module_type_event_cb cb, void* context) { + if (cb != NULL) { + cb(context); + } +} + +// C trampolines for stats iteration callbacks. Defined in bootstrap_trampolines.c +// (NOT here in the cgo preamble) so they have external linkage; cgo's address-of +// for `C.cgoBootstrap*IteratorC` emits an extern relocation that would not resolve +// against a `static` definition. See bootstrap_trampolines.c for the full rationale. +extern envoy_dynamic_module_type_stats_iteration_action cgoBootstrapCounterIteratorC( + envoy_dynamic_module_type_envoy_buffer name, uint64_t value, void* user_data); +extern envoy_dynamic_module_type_stats_iteration_action cgoBootstrapGaugeIteratorC( + envoy_dynamic_module_type_envoy_buffer name, uint64_t value, void* user_data); +*/ +import "C" +import ( + "runtime" + "sync" + "sync/atomic" + "unsafe" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +type bootstrapConfigWrapper struct { + hostConfigPtr C.envoy_dynamic_module_type_bootstrap_extension_config_envoy_ptr + extension shared.BootstrapExtension + configHandle *dymBootstrapConfigHandle + + // destroyed is set during the config_destroy hook so late callbacks (timer fires, + // file watch events, lifecycle listener notifications, callout completions, admin + // requests, scheduled tasks) become no-ops instead of re-entering user code. + destroyed bool + + // timers indexed by their host pointer address. + timersMu sync.Mutex + timers map[unsafe.Pointer]*dymBootstrapTimer + + // file watchers keyed by registered path. + watchersMu sync.Mutex + watchers map[string]func(path string, events shared.FileWatcherEvent) + + // admin handlers keyed by path prefix. + adminMu sync.Mutex + adminHandlers map[string]shared.BootstrapAdminHandler + + // callout callbacks indexed by callout id. + calloutMu sync.Mutex + calloutCallbacks map[uint64]shared.HttpCalloutCallback + + // scheduler created from the config (lazy). + scheduler *dymScheduler + + // Pending shutdown completion. Stored when OnShutdown is in flight; the runtime invokes + // the C completion_callback exactly once when the module's wrapper completion func is + // called. + shutdownMu sync.Mutex + shutdownCompletion *bootstrapShutdownCompletion +} + +type bootstrapExtensionWrapper struct { + hostExtensionPtr C.envoy_dynamic_module_type_bootstrap_extension_envoy_ptr + extension shared.BootstrapExtension + configRef *bootstrapConfigWrapper + + // destroyed is set during the destroy hook so late callbacks (scheduled tasks, + // shutdown event, file watcher events) become no-ops instead of re-entering user code. + destroyed bool +} + +type bootstrapShutdownCompletion struct { + cb C.envoy_dynamic_module_type_event_cb + context unsafe.Pointer + done atomic.Bool +} + +var bootstrapConfigManager = newManager[bootstrapConfigWrapper]() +var bootstrapExtensionManager = newManager[bootstrapExtensionWrapper]() + +// dymBootstrapConfigHandle implements shared.BootstrapExtensionConfigHandle. +type dymBootstrapConfigHandle struct { + wrapper *bootstrapConfigWrapper +} + +func (h *dymBootstrapConfigHandle) SignalInitComplete() { + C.envoy_dynamic_module_callback_bootstrap_extension_config_signal_init_complete( + h.wrapper.hostConfigPtr) +} + +func (h *dymBootstrapConfigHandle) NewScheduler() shared.Scheduler { + if h.wrapper.scheduler == nil { + schedulerPtr := C.envoy_dynamic_module_callback_bootstrap_extension_config_scheduler_new( + h.wrapper.hostConfigPtr) + h.wrapper.scheduler = newDymScheduler( + unsafe.Pointer(schedulerPtr), + func(p unsafe.Pointer, taskID C.uint64_t) { + C.envoy_dynamic_module_callback_bootstrap_extension_config_scheduler_commit( + (C.envoy_dynamic_module_type_bootstrap_extension_config_scheduler_module_ptr)(p), + taskID, + ) + }, + func(p unsafe.Pointer) { + C.envoy_dynamic_module_callback_bootstrap_extension_config_scheduler_delete( + (C.envoy_dynamic_module_type_bootstrap_extension_config_scheduler_module_ptr)(p), + ) + }, + ) + // Finalizer is a fallback; the synchronous close() in + // envoy_dynamic_module_on_bootstrap_extension_config_destroy is what avoids the + // LeakSanitizer report on test exit (Go GC finalizers don't run on process exit). + runtime.SetFinalizer(h.wrapper.scheduler, func(s *dymScheduler) { s.close() }) + } + return h.wrapper.scheduler +} + +func (h *dymBootstrapConfigHandle) HttpCallout( + clusterName string, headers [][2]string, body []byte, timeoutMs uint64, + cb shared.HttpCalloutCallback, +) (shared.HttpCalloutInitResult, uint64) { + headerViews := headersToModuleHttpHeaderSlice(headers) + var calloutID C.uint64_t + result := C.envoy_dynamic_module_callback_bootstrap_extension_http_callout( + h.wrapper.hostConfigPtr, + &calloutID, + stringToModuleBuffer(clusterName), + unsafe.SliceData(headerViews), + C.size_t(len(headerViews)), + bytesToModuleBuffer(body), + C.uint64_t(timeoutMs), + ) + runtime.KeepAlive(clusterName) + runtime.KeepAlive(headers) + runtime.KeepAlive(headerViews) + runtime.KeepAlive(body) + goResult := shared.HttpCalloutInitResult(result) + if goResult != shared.HttpCalloutInitSuccess { + return goResult, 0 + } + h.wrapper.calloutMu.Lock() + h.wrapper.calloutCallbacks[uint64(calloutID)] = cb + h.wrapper.calloutMu.Unlock() + return goResult, uint64(calloutID) +} + +// dymBootstrapTimer implements shared.BootstrapTimer. +type dymBootstrapTimer struct { + hostTimerPtr C.envoy_dynamic_module_type_bootstrap_extension_timer_module_ptr + onFire func(timer shared.BootstrapTimer) + deleted atomic.Bool +} + +func (t *dymBootstrapTimer) Enable(delayMs uint64) { + C.envoy_dynamic_module_callback_bootstrap_extension_timer_enable(t.hostTimerPtr, C.uint64_t(delayMs)) +} + +func (t *dymBootstrapTimer) Disable() { + C.envoy_dynamic_module_callback_bootstrap_extension_timer_disable(t.hostTimerPtr) +} + +func (t *dymBootstrapTimer) Enabled() bool { + return bool(C.envoy_dynamic_module_callback_bootstrap_extension_timer_enabled(t.hostTimerPtr)) +} + +func (t *dymBootstrapTimer) Delete() { + if t.deleted.Swap(true) { + return + } + C.envoy_dynamic_module_callback_bootstrap_extension_timer_delete(t.hostTimerPtr) +} + +func (h *dymBootstrapConfigHandle) NewTimer(onFire func(timer shared.BootstrapTimer)) shared.BootstrapTimer { + timerPtr := C.envoy_dynamic_module_callback_bootstrap_extension_timer_new(h.wrapper.hostConfigPtr) + if timerPtr == nil { + return nil + } + t := &dymBootstrapTimer{hostTimerPtr: timerPtr, onFire: onFire} + h.wrapper.timersMu.Lock() + h.wrapper.timers[unsafe.Pointer(timerPtr)] = t + h.wrapper.timersMu.Unlock() + return t +} + +func (h *dymBootstrapConfigHandle) AddFileWatch( + path string, events shared.FileWatcherEvent, + onChange func(path string, events shared.FileWatcherEvent), +) bool { + ok := C.envoy_dynamic_module_callback_bootstrap_extension_file_watcher_add_watch( + h.wrapper.hostConfigPtr, stringToModuleBuffer(path), C.uint32_t(events)) + runtime.KeepAlive(path) + if !bool(ok) { + return false + } + h.wrapper.watchersMu.Lock() + h.wrapper.watchers[path] = onChange + h.wrapper.watchersMu.Unlock() + return true +} + +func (h *dymBootstrapConfigHandle) RegisterAdminHandler( + pathPrefix, helpText string, removable, mutatesServerState bool, + handler shared.BootstrapAdminHandler, +) bool { + ok := C.envoy_dynamic_module_callback_bootstrap_extension_register_admin_handler( + h.wrapper.hostConfigPtr, + stringToModuleBuffer(pathPrefix), + stringToModuleBuffer(helpText), + C.bool(removable), + C.bool(mutatesServerState), + ) + runtime.KeepAlive(pathPrefix) + runtime.KeepAlive(helpText) + if !bool(ok) { + return false + } + h.wrapper.adminMu.Lock() + h.wrapper.adminHandlers[pathPrefix] = handler + h.wrapper.adminMu.Unlock() + return true +} + +func (h *dymBootstrapConfigHandle) RemoveAdminHandler(pathPrefix string) bool { + ok := C.envoy_dynamic_module_callback_bootstrap_extension_remove_admin_handler( + h.wrapper.hostConfigPtr, stringToModuleBuffer(pathPrefix)) + runtime.KeepAlive(pathPrefix) + if bool(ok) { + h.wrapper.adminMu.Lock() + delete(h.wrapper.adminHandlers, pathPrefix) + h.wrapper.adminMu.Unlock() + } + return bool(ok) +} + +func (h *dymBootstrapConfigHandle) SetAdminResponse(responseBody []byte) { + C.envoy_dynamic_module_callback_bootstrap_extension_admin_set_response( + h.wrapper.hostConfigPtr, bytesToModuleBuffer(responseBody)) + runtime.KeepAlive(responseBody) +} + +func (h *dymBootstrapConfigHandle) EnableClusterLifecycle() bool { + return bool(C.envoy_dynamic_module_callback_bootstrap_extension_enable_cluster_lifecycle( + h.wrapper.hostConfigPtr)) +} + +func (h *dymBootstrapConfigHandle) EnableListenerLifecycle() bool { + return bool(C.envoy_dynamic_module_callback_bootstrap_extension_enable_listener_lifecycle( + h.wrapper.hostConfigPtr)) +} + +// ---- labeled metrics ---- + +func (h *dymBootstrapConfigHandle) DefineCounter(name string, labelNames []string) (shared.MetricID, shared.MetricsResult) { + labels := stringSlicesToModuleBuffers(labelNames) + var id C.size_t + ret := C.envoy_dynamic_module_callback_bootstrap_extension_config_define_counter( + h.wrapper.hostConfigPtr, stringToModuleBuffer(name), + bufferSlicePtr(labels), C.size_t(len(labels)), &id, + ) + runtime.KeepAlive(name) + runtime.KeepAlive(labelNames) + runtime.KeepAlive(labels) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymBootstrapConfigHandle) IncrementCounter(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_bootstrap_extension_config_increment_counter( + h.wrapper.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +func (h *dymBootstrapConfigHandle) DefineGauge(name string, labelNames []string) (shared.MetricID, shared.MetricsResult) { + labels := stringSlicesToModuleBuffers(labelNames) + var id C.size_t + ret := C.envoy_dynamic_module_callback_bootstrap_extension_config_define_gauge( + h.wrapper.hostConfigPtr, stringToModuleBuffer(name), + bufferSlicePtr(labels), C.size_t(len(labels)), &id, + ) + runtime.KeepAlive(name) + runtime.KeepAlive(labelNames) + runtime.KeepAlive(labels) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymBootstrapConfigHandle) SetGauge(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_bootstrap_extension_config_set_gauge( + h.wrapper.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +func (h *dymBootstrapConfigHandle) IncrementGauge(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_bootstrap_extension_config_increment_gauge( + h.wrapper.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +func (h *dymBootstrapConfigHandle) DecrementGauge(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_bootstrap_extension_config_decrement_gauge( + h.wrapper.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +func (h *dymBootstrapConfigHandle) DefineHistogram(name string, labelNames []string) (shared.MetricID, shared.MetricsResult) { + labels := stringSlicesToModuleBuffers(labelNames) + var id C.size_t + ret := C.envoy_dynamic_module_callback_bootstrap_extension_config_define_histogram( + h.wrapper.hostConfigPtr, stringToModuleBuffer(name), + bufferSlicePtr(labels), C.size_t(len(labels)), &id, + ) + runtime.KeepAlive(name) + runtime.KeepAlive(labelNames) + runtime.KeepAlive(labels) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymBootstrapConfigHandle) RecordHistogramValue(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_bootstrap_extension_config_record_histogram_value( + h.wrapper.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +// dymBootstrapExtensionHandle implements shared.BootstrapExtensionHandle. It exposes +// stats-store access scoped to the extension instance. +type dymBootstrapExtensionHandle struct { + hostExtensionPtr C.envoy_dynamic_module_type_bootstrap_extension_envoy_ptr +} + +func (h *dymBootstrapExtensionHandle) GetCounterValue(name string) (uint64, bool) { + var v C.uint64_t + ok := C.envoy_dynamic_module_callback_bootstrap_extension_get_counter_value( + h.hostExtensionPtr, stringToModuleBuffer(name), &v) + runtime.KeepAlive(name) + if !bool(ok) { + return 0, false + } + return uint64(v), true +} + +func (h *dymBootstrapExtensionHandle) GetGaugeValue(name string) (uint64, bool) { + var v C.uint64_t + ok := C.envoy_dynamic_module_callback_bootstrap_extension_get_gauge_value( + h.hostExtensionPtr, stringToModuleBuffer(name), &v) + runtime.KeepAlive(name) + if !bool(ok) { + return 0, false + } + return uint64(v), true +} + +func (h *dymBootstrapExtensionHandle) GetHistogramSummary(name string) (uint64, float64, bool) { + var sampleCount C.uint64_t + var sampleSum C.double + ok := C.envoy_dynamic_module_callback_bootstrap_extension_get_histogram_summary( + h.hostExtensionPtr, stringToModuleBuffer(name), &sampleCount, &sampleSum) + runtime.KeepAlive(name) + if !bool(ok) { + return 0, 0, false + } + return uint64(sampleCount), float64(sampleSum), true +} + +// IterateCounters and IterateGauges intentionally route through the same callback machinery +// — each call captures the visit closure into a per-call thread-local-style map keyed by an +// opaque user_data pointer. Because Envoy invokes the iterator synchronously on the same +// goroutine and unwinds before returning to us, we can simply pin the Go closure for the +// duration of the call. + +type bootstrapStatsIterContext struct { + visit func(name shared.UnsafeEnvoyBuffer, value uint64) shared.StatsIterationAction +} + +var bootstrapStatsIterManager = newManager[bootstrapStatsIterContext]() + +func (h *dymBootstrapExtensionHandle) IterateCounters(visit func(name shared.UnsafeEnvoyBuffer, value uint64) shared.StatsIterationAction) { + if visit == nil { + return + } + ctx := &bootstrapStatsIterContext{visit: visit} + ctxPtr := bootstrapStatsIterManager.record(ctx) + defer bootstrapStatsIterManager.remove(ctxPtr) + C.envoy_dynamic_module_callback_bootstrap_extension_iterate_counters( + h.hostExtensionPtr, + (C.envoy_dynamic_module_type_counter_iterator_fn)(C.cgoBootstrapCounterIteratorC), + ctxPtr, + ) +} + +func (h *dymBootstrapExtensionHandle) IterateGauges(visit func(name shared.UnsafeEnvoyBuffer, value uint64) shared.StatsIterationAction) { + if visit == nil { + return + } + ctx := &bootstrapStatsIterContext{visit: visit} + ctxPtr := bootstrapStatsIterManager.record(ctx) + defer bootstrapStatsIterManager.remove(ctxPtr) + C.envoy_dynamic_module_callback_bootstrap_extension_iterate_gauges( + h.hostExtensionPtr, + (C.envoy_dynamic_module_type_gauge_iterator_fn)(C.cgoBootstrapGaugeIteratorC), + ctxPtr, + ) +} + +// ============================================================================= +// CGo trampolines for stats iteration +// ============================================================================= + +//export cgoBootstrapCounterIteratorGo +func cgoBootstrapCounterIteratorGo( + name C.envoy_dynamic_module_type_envoy_buffer, + value C.uint64_t, + userData unsafe.Pointer, +) C.envoy_dynamic_module_type_stats_iteration_action { + ctx := bootstrapStatsIterManager.unwrap(userData) + if ctx == nil || ctx.visit == nil { + return C.envoy_dynamic_module_type_stats_iteration_action_Stop + } + action := ctx.visit(envoyBufferToUnsafeEnvoyBuffer(name), uint64(value)) + return C.envoy_dynamic_module_type_stats_iteration_action(action) +} + +//export cgoBootstrapGaugeIteratorGo +func cgoBootstrapGaugeIteratorGo( + name C.envoy_dynamic_module_type_envoy_buffer, + value C.uint64_t, + userData unsafe.Pointer, +) C.envoy_dynamic_module_type_stats_iteration_action { + ctx := bootstrapStatsIterManager.unwrap(userData) + if ctx == nil || ctx.visit == nil { + return C.envoy_dynamic_module_type_stats_iteration_action_Stop + } + action := ctx.visit(envoyBufferToUnsafeEnvoyBuffer(name), uint64(value)) + return C.envoy_dynamic_module_type_stats_iteration_action(action) +} + +// ============================================================================= +// Event hooks +// ============================================================================= + +//export envoy_dynamic_module_on_bootstrap_extension_config_new +func envoy_dynamic_module_on_bootstrap_extension_config_new( + hostConfigPtr C.envoy_dynamic_module_type_bootstrap_extension_config_envoy_ptr, + name C.envoy_dynamic_module_type_envoy_buffer, + config C.envoy_dynamic_module_type_envoy_buffer, +) C.envoy_dynamic_module_type_bootstrap_extension_config_module_ptr { + nameStr := envoyBufferToStringUnsafe(name) + configBytes := envoyBufferToBytesCopy(config) + + configFactory := sdk.GetBootstrapExtensionConfigFactory(nameStr) + if configFactory == nil { + hostLog(shared.LogLevelWarn, "Failed to load bootstrap extension configuration for %q: no factory registered", []any{nameStr}) + return nil + } + wrapper := &bootstrapConfigWrapper{ + hostConfigPtr: hostConfigPtr, + timers: make(map[unsafe.Pointer]*dymBootstrapTimer), + watchers: make(map[string]func(path string, events shared.FileWatcherEvent)), + adminHandlers: make(map[string]shared.BootstrapAdminHandler), + calloutCallbacks: make(map[uint64]shared.HttpCalloutCallback), + } + wrapper.configHandle = &dymBootstrapConfigHandle{wrapper: wrapper} + ext, err := configFactory.Create(wrapper.configHandle, configBytes) + if err != nil { + hostLog(shared.LogLevelWarn, "Failed to load bootstrap extension configuration for %q: %v", []any{nameStr, err}) + return nil + } + if ext == nil { + hostLog(shared.LogLevelWarn, "Failed to load bootstrap extension configuration for %q: factory returned nil", []any{nameStr}) + return nil + } + wrapper.extension = ext + configPtr := bootstrapConfigManager.record(wrapper) + return C.envoy_dynamic_module_type_bootstrap_extension_config_module_ptr(configPtr) +} + +//export envoy_dynamic_module_on_bootstrap_extension_config_destroy +func envoy_dynamic_module_on_bootstrap_extension_config_destroy( + configPtr C.envoy_dynamic_module_type_bootstrap_extension_config_module_ptr, +) { + w := bootstrapConfigManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil || w.destroyed { + return + } + w.destroyed = true + if w.extension != nil { + w.extension.OnDestroy() + } + if w.scheduler != nil { + // Synchronously free the host-side scheduler. We cannot rely on the GC finalizer + // here because (a) under LeakSanitizer the test process exits before finalizers + // run, and (b) the scheduler holds a pointer into a host extension that is about + // to be torn down — leaving the cleanup until later risks a use-after-free. + w.scheduler.close() + w.scheduler = nil + } + bootstrapConfigManager.remove(unsafe.Pointer(configPtr)) +} + +//export envoy_dynamic_module_on_bootstrap_extension_new +func envoy_dynamic_module_on_bootstrap_extension_new( + configPtr C.envoy_dynamic_module_type_bootstrap_extension_config_module_ptr, + hostExtensionPtr C.envoy_dynamic_module_type_bootstrap_extension_envoy_ptr, +) C.envoy_dynamic_module_type_bootstrap_extension_module_ptr { + cfg := bootstrapConfigManager.unwrap(unsafe.Pointer(configPtr)) + if cfg == nil { + return nil + } + wrapper := &bootstrapExtensionWrapper{ + hostExtensionPtr: hostExtensionPtr, + extension: cfg.extension, + configRef: cfg, + } + extPtr := bootstrapExtensionManager.record(wrapper) + handle := &dymBootstrapExtensionHandle{hostExtensionPtr: hostExtensionPtr} + cfg.extension.OnNew(handle) + return C.envoy_dynamic_module_type_bootstrap_extension_module_ptr(extPtr) +} + +//export envoy_dynamic_module_on_bootstrap_extension_server_initialized +func envoy_dynamic_module_on_bootstrap_extension_server_initialized( + hostExtensionPtr C.envoy_dynamic_module_type_bootstrap_extension_envoy_ptr, + extPtr C.envoy_dynamic_module_type_bootstrap_extension_module_ptr, +) { + w := bootstrapExtensionManager.unwrap(unsafe.Pointer(extPtr)) + if w == nil || w.extension == nil || w.destroyed { + return + } + w.hostExtensionPtr = hostExtensionPtr + handle := &dymBootstrapExtensionHandle{hostExtensionPtr: hostExtensionPtr} + w.extension.OnServerInitialized(handle) +} + +//export envoy_dynamic_module_on_bootstrap_extension_worker_thread_initialized +func envoy_dynamic_module_on_bootstrap_extension_worker_thread_initialized( + hostExtensionPtr C.envoy_dynamic_module_type_bootstrap_extension_envoy_ptr, + extPtr C.envoy_dynamic_module_type_bootstrap_extension_module_ptr, +) { + w := bootstrapExtensionManager.unwrap(unsafe.Pointer(extPtr)) + if w == nil || w.extension == nil || w.destroyed { + return + } + handle := &dymBootstrapExtensionHandle{hostExtensionPtr: hostExtensionPtr} + w.extension.OnWorkerThreadInitialized(handle) +} + +//export envoy_dynamic_module_on_bootstrap_extension_drain_started +func envoy_dynamic_module_on_bootstrap_extension_drain_started( + hostExtensionPtr C.envoy_dynamic_module_type_bootstrap_extension_envoy_ptr, + extPtr C.envoy_dynamic_module_type_bootstrap_extension_module_ptr, +) { + w := bootstrapExtensionManager.unwrap(unsafe.Pointer(extPtr)) + if w == nil || w.extension == nil || w.destroyed { + return + } + handle := &dymBootstrapExtensionHandle{hostExtensionPtr: hostExtensionPtr} + w.extension.OnDrainStarted(handle) +} + +//export envoy_dynamic_module_on_bootstrap_extension_shutdown +func envoy_dynamic_module_on_bootstrap_extension_shutdown( + hostExtensionPtr C.envoy_dynamic_module_type_bootstrap_extension_envoy_ptr, + extPtr C.envoy_dynamic_module_type_bootstrap_extension_module_ptr, + completionCallback C.envoy_dynamic_module_type_event_cb, + completionContext unsafe.Pointer, +) { + w := bootstrapExtensionManager.unwrap(unsafe.Pointer(extPtr)) + if w == nil || w.extension == nil { + // We still have to call completion to unblock Envoy. + C.cgoInvokeEventCb(completionCallback, completionContext) + return + } + completion := &bootstrapShutdownCompletion{cb: completionCallback, context: completionContext} + w.configRef.shutdownMu.Lock() + w.configRef.shutdownCompletion = completion + w.configRef.shutdownMu.Unlock() + handle := &dymBootstrapExtensionHandle{hostExtensionPtr: hostExtensionPtr} + w.extension.OnShutdown(handle, func() { + if completion.done.Swap(true) { + return + } + C.cgoInvokeEventCb(completion.cb, completion.context) + }) +} + +//export envoy_dynamic_module_on_bootstrap_extension_destroy +func envoy_dynamic_module_on_bootstrap_extension_destroy( + extPtr C.envoy_dynamic_module_type_bootstrap_extension_module_ptr, +) { + w := bootstrapExtensionManager.unwrap(unsafe.Pointer(extPtr)) + if w == nil || w.destroyed { + return + } + w.destroyed = true + bootstrapExtensionManager.remove(unsafe.Pointer(extPtr)) +} + +//export envoy_dynamic_module_on_bootstrap_extension_config_scheduled +func envoy_dynamic_module_on_bootstrap_extension_config_scheduled( + _ C.envoy_dynamic_module_type_bootstrap_extension_config_envoy_ptr, + configPtr C.envoy_dynamic_module_type_bootstrap_extension_config_module_ptr, + eventID C.uint64_t, +) { + w := bootstrapConfigManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil || w.scheduler == nil || w.destroyed { + return + } + w.scheduler.onScheduled(uint64(eventID)) +} + +//export envoy_dynamic_module_on_bootstrap_extension_timer_fired +func envoy_dynamic_module_on_bootstrap_extension_timer_fired( + _ C.envoy_dynamic_module_type_bootstrap_extension_config_envoy_ptr, + configPtr C.envoy_dynamic_module_type_bootstrap_extension_config_module_ptr, + timerPtr C.envoy_dynamic_module_type_bootstrap_extension_timer_module_ptr, +) { + w := bootstrapConfigManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil || w.destroyed { + return + } + w.timersMu.Lock() + t := w.timers[unsafe.Pointer(timerPtr)] + w.timersMu.Unlock() + if t != nil && t.onFire != nil { + t.onFire(t) + } +} + +//export envoy_dynamic_module_on_bootstrap_extension_file_changed +func envoy_dynamic_module_on_bootstrap_extension_file_changed( + _ C.envoy_dynamic_module_type_bootstrap_extension_config_envoy_ptr, + configPtr C.envoy_dynamic_module_type_bootstrap_extension_config_module_ptr, + path C.envoy_dynamic_module_type_envoy_buffer, + events C.uint32_t, +) { + w := bootstrapConfigManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil || w.destroyed { + return + } + pathStr := envoyBufferToStringUnsafe(path) + w.watchersMu.Lock() + cb := w.watchers[pathStr] + w.watchersMu.Unlock() + if cb != nil { + cb(pathStr, shared.FileWatcherEvent(events)) + } +} + +//export envoy_dynamic_module_on_bootstrap_extension_cluster_add_or_update +func envoy_dynamic_module_on_bootstrap_extension_cluster_add_or_update( + _ C.envoy_dynamic_module_type_bootstrap_extension_config_envoy_ptr, + configPtr C.envoy_dynamic_module_type_bootstrap_extension_config_module_ptr, + clusterName C.envoy_dynamic_module_type_envoy_buffer, +) { + w := bootstrapConfigManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil || w.extension == nil || w.destroyed { + return + } + if l, ok := w.extension.(shared.BootstrapClusterLifecycleListener); ok { + l.OnClusterAddOrUpdate(envoyBufferToStringUnsafe(clusterName)) + } +} + +//export envoy_dynamic_module_on_bootstrap_extension_cluster_removal +func envoy_dynamic_module_on_bootstrap_extension_cluster_removal( + _ C.envoy_dynamic_module_type_bootstrap_extension_config_envoy_ptr, + configPtr C.envoy_dynamic_module_type_bootstrap_extension_config_module_ptr, + clusterName C.envoy_dynamic_module_type_envoy_buffer, +) { + w := bootstrapConfigManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil || w.extension == nil || w.destroyed { + return + } + if l, ok := w.extension.(shared.BootstrapClusterLifecycleListener); ok { + l.OnClusterRemoval(envoyBufferToStringUnsafe(clusterName)) + } +} + +//export envoy_dynamic_module_on_bootstrap_extension_listener_add_or_update +func envoy_dynamic_module_on_bootstrap_extension_listener_add_or_update( + _ C.envoy_dynamic_module_type_bootstrap_extension_config_envoy_ptr, + configPtr C.envoy_dynamic_module_type_bootstrap_extension_config_module_ptr, + listenerName C.envoy_dynamic_module_type_envoy_buffer, +) { + w := bootstrapConfigManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil || w.extension == nil || w.destroyed { + return + } + if l, ok := w.extension.(shared.BootstrapListenerLifecycleListener); ok { + l.OnListenerAddOrUpdate(envoyBufferToStringUnsafe(listenerName)) + } +} + +//export envoy_dynamic_module_on_bootstrap_extension_listener_removal +func envoy_dynamic_module_on_bootstrap_extension_listener_removal( + _ C.envoy_dynamic_module_type_bootstrap_extension_config_envoy_ptr, + configPtr C.envoy_dynamic_module_type_bootstrap_extension_config_module_ptr, + listenerName C.envoy_dynamic_module_type_envoy_buffer, +) { + w := bootstrapConfigManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil || w.extension == nil || w.destroyed { + return + } + if l, ok := w.extension.(shared.BootstrapListenerLifecycleListener); ok { + l.OnListenerRemoval(envoyBufferToStringUnsafe(listenerName)) + } +} + +//export envoy_dynamic_module_on_bootstrap_extension_http_callout_done +func envoy_dynamic_module_on_bootstrap_extension_http_callout_done( + _ C.envoy_dynamic_module_type_bootstrap_extension_config_envoy_ptr, + configPtr C.envoy_dynamic_module_type_bootstrap_extension_config_module_ptr, + calloutID C.uint64_t, + result C.envoy_dynamic_module_type_http_callout_result, + headers *C.envoy_dynamic_module_type_envoy_http_header, + headersSize C.size_t, + chunks *C.envoy_dynamic_module_type_envoy_buffer, + chunksSize C.size_t, +) { + w := bootstrapConfigManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil || w.destroyed { + return + } + resultHeaders := envoyHttpHeaderSliceToUnsafeHeaderSlice(unsafe.Slice(headers, int(headersSize))) + resultChunks := envoyBufferSliceToUnsafeEnvoyBufferSlice(unsafe.Slice(chunks, int(chunksSize))) + w.calloutMu.Lock() + cb := w.calloutCallbacks[uint64(calloutID)] + delete(w.calloutCallbacks, uint64(calloutID)) + w.calloutMu.Unlock() + if cb != nil { + cb.OnHttpCalloutDone(uint64(calloutID), shared.HttpCalloutResult(result), resultHeaders, resultChunks) + } +} + +//export envoy_dynamic_module_on_bootstrap_extension_admin_request +func envoy_dynamic_module_on_bootstrap_extension_admin_request( + _ C.envoy_dynamic_module_type_bootstrap_extension_config_envoy_ptr, + configPtr C.envoy_dynamic_module_type_bootstrap_extension_config_module_ptr, + method C.envoy_dynamic_module_type_envoy_buffer, + path C.envoy_dynamic_module_type_envoy_buffer, + body C.envoy_dynamic_module_type_envoy_buffer, +) C.uint32_t { + w := bootstrapConfigManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil || w.destroyed { + return 500 + } + // path is used both for handler lookup and is passed through to the user; the lookup is + // safe with the unsafe alias, but the user may retain pathStr, so copy before dispatch. + pathView := envoyBufferToStringUnsafe(path) + w.adminMu.Lock() + // Match by exact prefix first; if no exact match, fall back to longest matching prefix. + var handler shared.BootstrapAdminHandler + if h, ok := w.adminHandlers[pathView]; ok { + handler = h + } else { + var bestPrefix string + for prefix, h := range w.adminHandlers { + if len(prefix) > len(bestPrefix) && hasPrefixGo(pathView, prefix) { + bestPrefix = prefix + handler = h + } + } + } + w.adminMu.Unlock() + if handler == nil { + return 404 + } + // Admin handlers may retain method/path/body strings (e.g., persisting them in module + // state); copy out of the Envoy-owned buffers before user code runs. + pathStr := string(pathView) + methodStr := envoyBufferToStringCopy(method) + bodyBytes := envoyBufferToBytesCopy(body) + return C.uint32_t(handler.HandleAdminRequest(w.configHandle, methodStr, pathStr, bodyBytes)) +} + +func hasPrefixGo(s, prefix string) bool { + if len(s) < len(prefix) { + return false + } + return s[:len(prefix)] == prefix +} diff --git a/source/extensions/dynamic_modules/sdk/go/abi/bootstrap_trampolines.c b/source/extensions/dynamic_modules/sdk/go/abi/bootstrap_trampolines.c new file mode 100644 index 0000000000000..7860c441664af --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/abi/bootstrap_trampolines.c @@ -0,0 +1,30 @@ +// C trampolines for cgo callbacks that are passed as function pointers across +// the Envoy dynamic-module ABI. These cannot live in the cgo preamble of +// bootstrap.go: each cgo-translation unit emits its own copy of any `static` +// functions in the preamble, and Go's address-of (`C.cgoBootstrapCounterIteratorC`) +// generates a relocation against an *external* symbol of that name. With +// `static` the symbol has internal linkage and the dynamic linker fails with +// `undefined symbol: cgoBootstrapCounterIteratorC` at dlopen time. Defining +// the trampolines once in a stand-alone .c file gives them external linkage +// and a single definition, satisfying the relocation without duplicate-symbol +// link errors. +#include +#include "../../../abi/abi.h" + +// Forward declarations for the Go-exported callbacks. The Go runtime emits +// these symbols (lower-cased exactly as written) as part of building the +// shared library that contains the cgo package. +extern envoy_dynamic_module_type_stats_iteration_action cgoBootstrapCounterIteratorGo( + envoy_dynamic_module_type_envoy_buffer name, uint64_t value, void* user_data); +extern envoy_dynamic_module_type_stats_iteration_action cgoBootstrapGaugeIteratorGo( + envoy_dynamic_module_type_envoy_buffer name, uint64_t value, void* user_data); + +envoy_dynamic_module_type_stats_iteration_action cgoBootstrapCounterIteratorC( + envoy_dynamic_module_type_envoy_buffer name, uint64_t value, void* user_data) { + return cgoBootstrapCounterIteratorGo(name, value, user_data); +} + +envoy_dynamic_module_type_stats_iteration_action cgoBootstrapGaugeIteratorC( + envoy_dynamic_module_type_envoy_buffer name, uint64_t value, void* user_data) { + return cgoBootstrapGaugeIteratorGo(name, value, user_data); +} diff --git a/source/extensions/dynamic_modules/sdk/go/abi/cert_validator.go b/source/extensions/dynamic_modules/sdk/go/abi/cert_validator.go new file mode 100644 index 0000000000000..c9c96852f0df3 --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/abi/cert_validator.go @@ -0,0 +1,174 @@ +package abi + +/* +#cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup +#cgo linux LDFLAGS: -Wl,--unresolved-symbols=ignore-all +#include +#include +#include +#include +#include "../../../abi/abi.h" +*/ +import "C" +import ( + "runtime" + "unsafe" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +type certValidatorWrapper struct { + validator shared.CertValidator + // digestCache holds the most-recent digest bytes returned from UpdateDigest, keeping the + // memory alive for the duration of the on_update_digest call. + digestCache []byte + + // destroyed is set during config_destroy so any late callbacks (verify_cert_chain, + // get_ssl_verify_mode, update_digest) become no-ops. + destroyed bool +} + +var certValidatorManager = newManager[certValidatorWrapper]() + +// dymCertValidatorContext implements shared.CertValidatorContext. It is bound to a single +// VerifyCertChain call. +type dymCertValidatorContext struct { + hostConfigPtr C.envoy_dynamic_module_type_cert_validator_config_envoy_ptr +} + +func (c *dymCertValidatorContext) SetErrorDetails(errorDetails string) { + C.envoy_dynamic_module_callback_cert_validator_set_error_details( + c.hostConfigPtr, stringToModuleBuffer(errorDetails)) + runtime.KeepAlive(errorDetails) +} + +func (c *dymCertValidatorContext) SetFilterState(key, value string) bool { + ret := C.envoy_dynamic_module_callback_cert_validator_set_filter_state( + c.hostConfigPtr, stringToModuleBuffer(key), stringToModuleBuffer(value)) + runtime.KeepAlive(key) + runtime.KeepAlive(value) + return bool(ret) +} + +func (c *dymCertValidatorContext) GetFilterState(key string) (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ret := C.envoy_dynamic_module_callback_cert_validator_get_filter_state( + c.hostConfigPtr, stringToModuleBuffer(key), &buf) + runtime.KeepAlive(key) + if !bool(ret) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ret) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +// ============================================================================= +// Event hooks +// ============================================================================= + +//export envoy_dynamic_module_on_cert_validator_config_new +func envoy_dynamic_module_on_cert_validator_config_new( + _ C.envoy_dynamic_module_type_cert_validator_config_envoy_ptr, + name C.envoy_dynamic_module_type_envoy_buffer, + config C.envoy_dynamic_module_type_envoy_buffer, +) C.envoy_dynamic_module_type_cert_validator_config_module_ptr { + nameStr := envoyBufferToStringUnsafe(name) + configBytes := envoyBufferToBytesCopy(config) + + configFactory := sdk.GetCertValidatorConfigFactory(nameStr) + if configFactory == nil { + hostLog(shared.LogLevelWarn, "Failed to load cert validator configuration for %q: no factory registered", []any{nameStr}) + return nil + } + v, err := configFactory.Create(nameStr, configBytes) + if err != nil { + hostLog(shared.LogLevelWarn, "Failed to load cert validator configuration for %q: %v", []any{nameStr, err}) + return nil + } + if v == nil { + hostLog(shared.LogLevelWarn, "Failed to load cert validator configuration for %q: factory returned nil", []any{nameStr}) + return nil + } + wrapper := &certValidatorWrapper{validator: v} + configPtr := certValidatorManager.record(wrapper) + return C.envoy_dynamic_module_type_cert_validator_config_module_ptr(configPtr) +} + +//export envoy_dynamic_module_on_cert_validator_config_destroy +func envoy_dynamic_module_on_cert_validator_config_destroy( + configPtr C.envoy_dynamic_module_type_cert_validator_config_module_ptr, +) { + w := certValidatorManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil || w.destroyed { + return + } + w.destroyed = true + if w.validator != nil { + w.validator.OnDestroy() + } + certValidatorManager.remove(unsafe.Pointer(configPtr)) +} + +//export envoy_dynamic_module_on_cert_validator_do_verify_cert_chain +func envoy_dynamic_module_on_cert_validator_do_verify_cert_chain( + hostConfigPtr C.envoy_dynamic_module_type_cert_validator_config_envoy_ptr, + configPtr C.envoy_dynamic_module_type_cert_validator_config_module_ptr, + certs *C.envoy_dynamic_module_type_envoy_buffer, + certsCount C.size_t, + hostName C.envoy_dynamic_module_type_envoy_buffer, + isServer C.bool, +) C.envoy_dynamic_module_type_cert_validator_validation_result { + w := certValidatorManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil || w.validator == nil || w.destroyed { + return C.envoy_dynamic_module_type_cert_validator_validation_result{ + status: C.envoy_dynamic_module_type_cert_validator_validation_status_Failed, + detailed_status: C.envoy_dynamic_module_type_cert_validator_client_validation_status_Failed, + } + } + certBufs := envoyBufferSliceToUnsafeEnvoyBufferSlice(unsafe.Slice(certs, int(certsCount))) + hostNameStr := envoyBufferToStringUnsafe(hostName) + ctx := &dymCertValidatorContext{hostConfigPtr: hostConfigPtr} + result := w.validator.VerifyCertChain(ctx, certBufs, hostNameStr, bool(isServer)) + + out := C.envoy_dynamic_module_type_cert_validator_validation_result{ + status: C.envoy_dynamic_module_type_cert_validator_validation_status(result.Status), + detailed_status: C.envoy_dynamic_module_type_cert_validator_client_validation_status(result.DetailedStatus), + has_tls_alert: C.bool(result.HasTLSAlert), + } + if result.HasTLSAlert { + out.tls_alert = C.uint8_t(result.TLSAlert) + } + return out +} + +//export envoy_dynamic_module_on_cert_validator_get_ssl_verify_mode +func envoy_dynamic_module_on_cert_validator_get_ssl_verify_mode( + configPtr C.envoy_dynamic_module_type_cert_validator_config_module_ptr, + handshakerProvidesCertificates C.bool, +) C.int { + w := certValidatorManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil || w.validator == nil || w.destroyed { + return 0 + } + return C.int(w.validator.GetSSLVerifyMode(bool(handshakerProvidesCertificates))) +} + +//export envoy_dynamic_module_on_cert_validator_update_digest +func envoy_dynamic_module_on_cert_validator_update_digest( + configPtr C.envoy_dynamic_module_type_cert_validator_config_module_ptr, + outData *C.envoy_dynamic_module_type_module_buffer, +) { + w := certValidatorManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil || w.validator == nil || w.destroyed { + return + } + digest := w.validator.UpdateDigest() + w.digestCache = digest // keep alive for the duration of this call + if len(digest) == 0 { + outData.ptr = nil + outData.length = 0 + return + } + outData.ptr = (*C.char)(unsafe.Pointer(unsafe.SliceData(digest))) + outData.length = C.size_t(len(digest)) +} diff --git a/source/extensions/dynamic_modules/sdk/go/abi/cluster.go b/source/extensions/dynamic_modules/sdk/go/abi/cluster.go new file mode 100644 index 0000000000000..a759c8824861e --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/abi/cluster.go @@ -0,0 +1,1013 @@ +package abi + +/* +#cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup +#cgo linux LDFLAGS: -Wl,--unresolved-symbols=ignore-all +#include +#include +#include +#include +#include "../../../abi/abi.h" + +// cgoClusterInvokeEventCb invokes Envoy's cluster-shutdown completion callback. Each cgo +// file has its own preamble, so we duplicate the wrapper from bootstrap.go's preamble here +// to keep the two files independently compilable. +static inline void cgoClusterInvokeEventCb(envoy_dynamic_module_type_event_cb cb, void* context) { + if (cb != NULL) { + cb(context); + } +} +*/ +import "C" +import ( + "runtime" + "sync" + "sync/atomic" + "unsafe" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +type clusterConfigWrapper struct { + hostConfigPtr C.envoy_dynamic_module_type_cluster_config_envoy_ptr + factory shared.ClusterFactory + configHandle *dymClusterConfigHandle +} + +type clusterWrapper struct { + hostClusterPtr C.envoy_dynamic_module_type_cluster_envoy_ptr + cluster shared.Cluster + configRef *clusterConfigWrapper + + scheduler *dymScheduler + + calloutMu sync.Mutex + calloutCallbacks map[uint64]shared.HttpCalloutCallback + + shutdownMu sync.Mutex + shutdownCompletion *clusterShutdownCompletion + + // destroyed is set during the destroy hook so late callbacks (scheduled tasks, + // callouts that fire as the cluster is being torn down) become no-ops instead of + // re-entering user code. + destroyed bool +} + +type clusterShutdownCompletion struct { + cb C.envoy_dynamic_module_type_event_cb + context unsafe.Pointer + done atomic.Bool +} + +type clusterLbWrapper struct { + hostLbPtr C.envoy_dynamic_module_type_cluster_lb_envoy_ptr + lb shared.ClusterLoadBalancer + clusterRef *clusterWrapper + + asyncMu sync.Mutex + asyncHandles map[*dymClusterAsyncCompletion]struct{} + + // destroyed is set during the destroy hook to prevent late callbacks (cancel, + // host-membership updates) from re-entering user code after OnDestroy. + destroyed bool +} + +var clusterConfigManager = newManager[clusterConfigWrapper]() +var clusterManager = newManager[clusterWrapper]() +var clusterLbManager = newManager[clusterLbWrapper]() +var clusterAsyncCompletionManager = newManager[dymClusterAsyncCompletion]() + +// dymClusterConfigHandle implements shared.ClusterConfigHandle (labeled metrics). +type dymClusterConfigHandle struct { + hostConfigPtr C.envoy_dynamic_module_type_cluster_config_envoy_ptr +} + +func (h *dymClusterConfigHandle) DefineCounter(name string, labelNames []string) (shared.MetricID, shared.MetricsResult) { + labels := stringSlicesToModuleBuffers(labelNames) + var id C.size_t + ret := C.envoy_dynamic_module_callback_cluster_config_define_counter( + h.hostConfigPtr, stringToModuleBuffer(name), + bufferSlicePtr(labels), C.size_t(len(labels)), &id, + ) + runtime.KeepAlive(name) + runtime.KeepAlive(labelNames) + runtime.KeepAlive(labels) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymClusterConfigHandle) IncrementCounter(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_cluster_config_increment_counter( + h.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +func (h *dymClusterConfigHandle) DefineGauge(name string, labelNames []string) (shared.MetricID, shared.MetricsResult) { + labels := stringSlicesToModuleBuffers(labelNames) + var id C.size_t + ret := C.envoy_dynamic_module_callback_cluster_config_define_gauge( + h.hostConfigPtr, stringToModuleBuffer(name), + bufferSlicePtr(labels), C.size_t(len(labels)), &id, + ) + runtime.KeepAlive(name) + runtime.KeepAlive(labelNames) + runtime.KeepAlive(labels) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymClusterConfigHandle) SetGauge(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_cluster_config_set_gauge( + h.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +func (h *dymClusterConfigHandle) IncrementGauge(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_cluster_config_increment_gauge( + h.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +func (h *dymClusterConfigHandle) DecrementGauge(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_cluster_config_decrement_gauge( + h.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +func (h *dymClusterConfigHandle) DefineHistogram(name string, labelNames []string) (shared.MetricID, shared.MetricsResult) { + labels := stringSlicesToModuleBuffers(labelNames) + var id C.size_t + ret := C.envoy_dynamic_module_callback_cluster_config_define_histogram( + h.hostConfigPtr, stringToModuleBuffer(name), + bufferSlicePtr(labels), C.size_t(len(labels)), &id, + ) + runtime.KeepAlive(name) + runtime.KeepAlive(labelNames) + runtime.KeepAlive(labels) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymClusterConfigHandle) RecordHistogramValue(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_cluster_config_record_histogram_value( + h.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +// dymClusterHandle implements shared.ClusterHandle. +type dymClusterHandle struct { + wrapper *clusterWrapper +} + +func (h *dymClusterHandle) PreInitComplete() { + C.envoy_dynamic_module_callback_cluster_pre_init_complete(h.wrapper.hostClusterPtr) +} + +func (h *dymClusterHandle) AddHosts(priority uint32, specs []shared.ClusterHostSpec) ([]shared.ClusterHost, bool) { + if len(specs) == 0 { + return nil, true + } + addresses := make([]C.envoy_dynamic_module_type_module_buffer, len(specs)) + weights := make([]C.uint32_t, len(specs)) + regions := make([]C.envoy_dynamic_module_type_module_buffer, len(specs)) + zones := make([]C.envoy_dynamic_module_type_module_buffer, len(specs)) + subZones := make([]C.envoy_dynamic_module_type_module_buffer, len(specs)) + + // metadataPairsPerHost MUST be the same for all specs and each slice length must be an exact + // multiple of 3 (filterName, key, value triples). A non-multiple-of-3 length or cross-spec + // mismatch is a caller error; return failure rather than silently truncating. + pairsPer := uint64(0) + for i := range specs { + if len(specs[i].MetadataPairs)%3 != 0 { + return nil, false + } + n := uint64(len(specs[i].MetadataPairs)) / 3 + if i == 0 { + pairsPer = n + } else if n != pairsPer { + return nil, false + } + } + var metadataPairs []C.envoy_dynamic_module_type_module_buffer + if pairsPer > 0 { + metadataPairs = make([]C.envoy_dynamic_module_type_module_buffer, 0, len(specs)*int(pairsPer)*3) + } + + for i, s := range specs { + addresses[i] = stringToModuleBuffer(s.Address) + weights[i] = C.uint32_t(s.Weight) + regions[i] = stringToModuleBuffer(s.Region) + zones[i] = stringToModuleBuffer(s.Zone) + subZones[i] = stringToModuleBuffer(s.SubZone) + for _, p := range s.MetadataPairs { + metadataPairs = append(metadataPairs, stringToModuleBuffer(p)) + } + } + + hostPtrs := make([]C.envoy_dynamic_module_type_cluster_host_envoy_ptr, len(specs)) + var metadataPtr *C.envoy_dynamic_module_type_module_buffer + if len(metadataPairs) > 0 { + metadataPtr = unsafe.SliceData(metadataPairs) + } + ok := C.envoy_dynamic_module_callback_cluster_add_hosts( + h.wrapper.hostClusterPtr, + C.uint32_t(priority), + unsafe.SliceData(addresses), + unsafe.SliceData(weights), + unsafe.SliceData(regions), + unsafe.SliceData(zones), + unsafe.SliceData(subZones), + metadataPtr, + C.size_t(pairsPer), + C.size_t(len(specs)), + unsafe.SliceData(hostPtrs), + ) + runtime.KeepAlive(specs) + runtime.KeepAlive(addresses) + runtime.KeepAlive(weights) + runtime.KeepAlive(regions) + runtime.KeepAlive(zones) + runtime.KeepAlive(subZones) + runtime.KeepAlive(metadataPairs) + if !bool(ok) { + return nil, false + } + out := make([]shared.ClusterHost, len(specs)) + for i := range hostPtrs { + out[i] = shared.UnsafeClusterHost(unsafe.Pointer(hostPtrs[i])) + } + return out, true +} + +func (h *dymClusterHandle) RemoveHosts(hosts []shared.ClusterHost) uint64 { + if len(hosts) == 0 { + return 0 + } + cHosts := make([]C.envoy_dynamic_module_type_cluster_host_envoy_ptr, len(hosts)) + for i, host := range hosts { + cHosts[i] = C.envoy_dynamic_module_type_cluster_host_envoy_ptr(shared.UnsafeClusterHostPtr(host)) + } + n := C.envoy_dynamic_module_callback_cluster_remove_hosts( + h.wrapper.hostClusterPtr, unsafe.SliceData(cHosts), C.size_t(len(cHosts))) + runtime.KeepAlive(cHosts) + return uint64(n) +} + +func (h *dymClusterHandle) UpdateHostHealth(host shared.ClusterHost, status shared.HostHealth) bool { + return bool(C.envoy_dynamic_module_callback_cluster_update_host_health( + h.wrapper.hostClusterPtr, + C.envoy_dynamic_module_type_cluster_host_envoy_ptr(shared.UnsafeClusterHostPtr(host)), + C.envoy_dynamic_module_type_host_health(status))) +} + +func (h *dymClusterHandle) FindHostByAddress(address string) shared.ClusterHost { + hp := C.envoy_dynamic_module_callback_cluster_find_host_by_address( + h.wrapper.hostClusterPtr, stringToModuleBuffer(address)) + runtime.KeepAlive(address) + return shared.UnsafeClusterHost(unsafe.Pointer(hp)) +} + +func (h *dymClusterHandle) HttpCallout( + clusterName string, headers [][2]string, body []byte, timeoutMs uint64, + cb shared.HttpCalloutCallback, +) (shared.HttpCalloutInitResult, uint64) { + headerViews := headersToModuleHttpHeaderSlice(headers) + var calloutID C.uint64_t + result := C.envoy_dynamic_module_callback_cluster_http_callout( + h.wrapper.hostClusterPtr, + &calloutID, + stringToModuleBuffer(clusterName), + unsafe.SliceData(headerViews), + C.size_t(len(headerViews)), + bytesToModuleBuffer(body), + C.uint64_t(timeoutMs), + ) + runtime.KeepAlive(clusterName) + runtime.KeepAlive(headers) + runtime.KeepAlive(headerViews) + runtime.KeepAlive(body) + goResult := shared.HttpCalloutInitResult(result) + if goResult != shared.HttpCalloutInitSuccess { + return goResult, 0 + } + h.wrapper.calloutMu.Lock() + h.wrapper.calloutCallbacks[uint64(calloutID)] = cb + h.wrapper.calloutMu.Unlock() + return goResult, uint64(calloutID) +} + +func (h *dymClusterHandle) NewScheduler() shared.Scheduler { + if h.wrapper.scheduler == nil { + schedulerPtr := C.envoy_dynamic_module_callback_cluster_scheduler_new(h.wrapper.hostClusterPtr) + h.wrapper.scheduler = newDymScheduler( + unsafe.Pointer(schedulerPtr), + func(p unsafe.Pointer, taskID C.uint64_t) { + C.envoy_dynamic_module_callback_cluster_scheduler_commit( + (C.envoy_dynamic_module_type_cluster_scheduler_module_ptr)(p), taskID) + }, + func(p unsafe.Pointer) { + C.envoy_dynamic_module_callback_cluster_scheduler_delete( + (C.envoy_dynamic_module_type_cluster_scheduler_module_ptr)(p)) + }, + ) + // Finalizer is a fallback; the destroy hook should call close() synchronously. + runtime.SetFinalizer(h.wrapper.scheduler, func(s *dymScheduler) { s.close() }) + } + return h.wrapper.scheduler +} + +// dymClusterLoadBalancerHandle implements shared.ClusterLoadBalancerHandle. +type dymClusterLoadBalancerHandle struct { + wrapper *clusterLbWrapper +} + +func (h *dymClusterLoadBalancerHandle) GetClusterName() shared.UnsafeEnvoyBuffer { + var buf C.envoy_dynamic_module_type_envoy_buffer + C.envoy_dynamic_module_callback_cluster_lb_get_cluster_name(h.wrapper.hostLbPtr, &buf) + if buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{} + } + return envoyBufferToUnsafeEnvoyBuffer(buf) +} + +func (h *dymClusterLoadBalancerHandle) GetHostsCount(priority uint32) uint64 { + return uint64(C.envoy_dynamic_module_callback_cluster_lb_get_hosts_count(h.wrapper.hostLbPtr, C.uint32_t(priority))) +} + +func (h *dymClusterLoadBalancerHandle) GetHealthyHostCount(priority uint32) uint64 { + return uint64(C.envoy_dynamic_module_callback_cluster_lb_get_healthy_host_count(h.wrapper.hostLbPtr, C.uint32_t(priority))) +} + +func (h *dymClusterLoadBalancerHandle) GetDegradedHostsCount(priority uint32) uint64 { + return uint64(C.envoy_dynamic_module_callback_cluster_lb_get_degraded_hosts_count(h.wrapper.hostLbPtr, C.uint32_t(priority))) +} + +func (h *dymClusterLoadBalancerHandle) GetPrioritySetSize() uint64 { + return uint64(C.envoy_dynamic_module_callback_cluster_lb_get_priority_set_size(h.wrapper.hostLbPtr)) +} + +func (h *dymClusterLoadBalancerHandle) GetHealthyHost(priority uint32, index uint64) shared.ClusterHost { + hp := C.envoy_dynamic_module_callback_cluster_lb_get_healthy_host(h.wrapper.hostLbPtr, C.uint32_t(priority), C.size_t(index)) + return shared.UnsafeClusterHost(unsafe.Pointer(hp)) +} + +func (h *dymClusterLoadBalancerHandle) GetHealthyHostAddress(priority uint32, index uint64) (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_cluster_lb_get_healthy_host_address(h.wrapper.hostLbPtr, C.uint32_t(priority), C.size_t(index), &buf) + if !bool(ok) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ok) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func (h *dymClusterLoadBalancerHandle) GetHealthyHostWeight(priority uint32, index uint64) uint32 { + return uint32(C.envoy_dynamic_module_callback_cluster_lb_get_healthy_host_weight(h.wrapper.hostLbPtr, C.uint32_t(priority), C.size_t(index))) +} + +func (h *dymClusterLoadBalancerHandle) GetHost(priority uint32, index uint64) shared.ClusterHost { + hp := C.envoy_dynamic_module_callback_cluster_lb_get_host(h.wrapper.hostLbPtr, C.uint32_t(priority), C.size_t(index)) + return shared.UnsafeClusterHost(unsafe.Pointer(hp)) +} + +func (h *dymClusterLoadBalancerHandle) GetHostAddress(priority uint32, index uint64) (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_cluster_lb_get_host_address(h.wrapper.hostLbPtr, C.uint32_t(priority), C.size_t(index), &buf) + if !bool(ok) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ok) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func (h *dymClusterLoadBalancerHandle) GetHostWeight(priority uint32, index uint64) uint32 { + return uint32(C.envoy_dynamic_module_callback_cluster_lb_get_host_weight(h.wrapper.hostLbPtr, C.uint32_t(priority), C.size_t(index))) +} + +func (h *dymClusterLoadBalancerHandle) GetHostHealth(priority uint32, index uint64) shared.HostHealth { + return shared.HostHealth(C.envoy_dynamic_module_callback_cluster_lb_get_host_health(h.wrapper.hostLbPtr, C.uint32_t(priority), C.size_t(index))) +} + +func (h *dymClusterLoadBalancerHandle) GetHostHealthByAddress(address string) (shared.HostHealth, bool) { + var v C.envoy_dynamic_module_type_host_health + ok := C.envoy_dynamic_module_callback_cluster_lb_get_host_health_by_address(h.wrapper.hostLbPtr, stringToModuleBuffer(address), &v) + runtime.KeepAlive(address) + if !bool(ok) { + return 0, false + } + return shared.HostHealth(v), true +} + +func (h *dymClusterLoadBalancerHandle) GetHostStat(priority uint32, index uint64, stat shared.HostStat) uint64 { + return uint64(C.envoy_dynamic_module_callback_cluster_lb_get_host_stat( + h.wrapper.hostLbPtr, C.uint32_t(priority), C.size_t(index), + C.envoy_dynamic_module_type_host_stat(stat))) +} + +func (h *dymClusterLoadBalancerHandle) GetHostLocality(priority uint32, index uint64) (shared.UnsafeEnvoyBuffer, shared.UnsafeEnvoyBuffer, shared.UnsafeEnvoyBuffer, bool) { + var region, zone, subZone C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_cluster_lb_get_host_locality( + h.wrapper.hostLbPtr, C.uint32_t(priority), C.size_t(index), ®ion, &zone, &subZone) + if !bool(ok) { + return shared.UnsafeEnvoyBuffer{}, shared.UnsafeEnvoyBuffer{}, shared.UnsafeEnvoyBuffer{}, false + } + return envoyBufferToUnsafeEnvoyBuffer(region), + envoyBufferToUnsafeEnvoyBuffer(zone), + envoyBufferToUnsafeEnvoyBuffer(subZone), + true +} + +func (h *dymClusterLoadBalancerHandle) FindHostByAddress(address string) shared.ClusterHost { + hp := C.envoy_dynamic_module_callback_cluster_lb_find_host_by_address(h.wrapper.hostLbPtr, stringToModuleBuffer(address)) + runtime.KeepAlive(address) + return shared.UnsafeClusterHost(unsafe.Pointer(hp)) +} + +func (h *dymClusterLoadBalancerHandle) SetHostData(priority uint32, index uint64, data uintptr) bool { + return bool(C.envoy_dynamic_module_callback_cluster_lb_set_host_data( + h.wrapper.hostLbPtr, C.uint32_t(priority), C.size_t(index), C.uintptr_t(data))) +} + +func (h *dymClusterLoadBalancerHandle) GetHostData(priority uint32, index uint64) (uintptr, bool) { + var data C.uintptr_t + ok := C.envoy_dynamic_module_callback_cluster_lb_get_host_data( + h.wrapper.hostLbPtr, C.uint32_t(priority), C.size_t(index), &data) + if !bool(ok) { + return 0, false + } + return uintptr(data), true +} + +func (h *dymClusterLoadBalancerHandle) GetHostMetadataString(priority uint32, index uint64, filterName, key string) (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_cluster_lb_get_host_metadata_string( + h.wrapper.hostLbPtr, C.uint32_t(priority), C.size_t(index), + stringToModuleBuffer(filterName), stringToModuleBuffer(key), &buf) + runtime.KeepAlive(filterName) + runtime.KeepAlive(key) + if !bool(ok) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ok) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func (h *dymClusterLoadBalancerHandle) GetHostMetadataNumber(priority uint32, index uint64, filterName, key string) (float64, bool) { + var v C.double + ok := C.envoy_dynamic_module_callback_cluster_lb_get_host_metadata_number( + h.wrapper.hostLbPtr, C.uint32_t(priority), C.size_t(index), + stringToModuleBuffer(filterName), stringToModuleBuffer(key), &v) + runtime.KeepAlive(filterName) + runtime.KeepAlive(key) + if !bool(ok) { + return 0, false + } + return float64(v), true +} + +func (h *dymClusterLoadBalancerHandle) GetHostMetadataBool(priority uint32, index uint64, filterName, key string) (bool, bool) { + var v C.bool + ok := C.envoy_dynamic_module_callback_cluster_lb_get_host_metadata_bool( + h.wrapper.hostLbPtr, C.uint32_t(priority), C.size_t(index), + stringToModuleBuffer(filterName), stringToModuleBuffer(key), &v) + runtime.KeepAlive(filterName) + runtime.KeepAlive(key) + if !bool(ok) { + return false, false + } + return bool(v), true +} + +func (h *dymClusterLoadBalancerHandle) GetLocalityCount(priority uint32) uint64 { + return uint64(C.envoy_dynamic_module_callback_cluster_lb_get_locality_count(h.wrapper.hostLbPtr, C.uint32_t(priority))) +} + +func (h *dymClusterLoadBalancerHandle) GetLocalityHostCount(priority uint32, localityIndex uint64) uint64 { + return uint64(C.envoy_dynamic_module_callback_cluster_lb_get_locality_host_count( + h.wrapper.hostLbPtr, C.uint32_t(priority), C.size_t(localityIndex))) +} + +func (h *dymClusterLoadBalancerHandle) GetLocalityHostAddress(priority uint32, localityIndex, hostIndex uint64) (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_cluster_lb_get_locality_host_address( + h.wrapper.hostLbPtr, C.uint32_t(priority), C.size_t(localityIndex), C.size_t(hostIndex), &buf) + if !bool(ok) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ok) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func (h *dymClusterLoadBalancerHandle) GetLocalityWeight(priority uint32, localityIndex uint64) uint32 { + return uint32(C.envoy_dynamic_module_callback_cluster_lb_get_locality_weight( + h.wrapper.hostLbPtr, C.uint32_t(priority), C.size_t(localityIndex))) +} + +func (h *dymClusterLoadBalancerHandle) GetMemberUpdateHostAddress(index uint64, isAdded bool) (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_cluster_lb_get_member_update_host_address( + h.wrapper.hostLbPtr, C.size_t(index), C.bool(isAdded), &buf) + if !bool(ok) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ok) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +// dymClusterLbContext implements shared.ClusterLoadBalancerContext. +type dymClusterLbContext struct { + hostCtxPtr C.envoy_dynamic_module_type_cluster_lb_context_envoy_ptr + lbWrapper *clusterLbWrapper +} + +func (c *dymClusterLbContext) ComputeHashKey() (uint64, bool) { + var v C.uint64_t + ok := C.envoy_dynamic_module_callback_cluster_lb_context_compute_hash_key(c.hostCtxPtr, &v) + if !bool(ok) { + return 0, false + } + return uint64(v), true +} + +func (c *dymClusterLbContext) GetDownstreamHeadersSize() uint64 { + return uint64(C.envoy_dynamic_module_callback_cluster_lb_context_get_downstream_headers_size(c.hostCtxPtr)) +} + +func (c *dymClusterLbContext) GetDownstreamHeaders() [][2]shared.UnsafeEnvoyBuffer { + size := C.envoy_dynamic_module_callback_cluster_lb_context_get_downstream_headers_size(c.hostCtxPtr) + if size == 0 { + return nil + } + hdrs := make([]C.envoy_dynamic_module_type_envoy_http_header, int(size)) + if !bool(C.envoy_dynamic_module_callback_cluster_lb_context_get_downstream_headers(c.hostCtxPtr, unsafe.SliceData(hdrs))) { + return nil + } + out := envoyHttpHeaderSliceToUnsafeHeaderSlice(hdrs) + runtime.KeepAlive(hdrs) + return out +} + +func (c *dymClusterLbContext) GetDownstreamHeader(key string, index uint64) (shared.UnsafeEnvoyBuffer, uint64, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var total C.size_t + ok := C.envoy_dynamic_module_callback_cluster_lb_context_get_downstream_header( + c.hostCtxPtr, stringToModuleBuffer(key), &buf, C.size_t(index), &total) + runtime.KeepAlive(key) + if !bool(ok) { + return shared.UnsafeEnvoyBuffer{}, uint64(total), false + } + if buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, uint64(total), true + } + return envoyBufferToUnsafeEnvoyBuffer(buf), uint64(total), true +} + +func (c *dymClusterLbContext) GetHostSelectionRetryCount() uint32 { + return uint32(C.envoy_dynamic_module_callback_cluster_lb_context_get_host_selection_retry_count(c.hostCtxPtr)) +} + +// ShouldSelectAnotherHost uses the LB pointer captured at context-construction time rather than +// the handle argument; the handle parameter exists only to satisfy the interface (which exposes +// it so callers can pass a handle obtained from a different scope). The captured lbWrapper is +// always the correct LB for this request's worker. +func (c *dymClusterLbContext) ShouldSelectAnotherHost(_ shared.ClusterLoadBalancerHandle, priority uint32, index uint64) bool { + if c.lbWrapper == nil { + return false + } + return bool(C.envoy_dynamic_module_callback_cluster_lb_context_should_select_another_host( + c.lbWrapper.hostLbPtr, c.hostCtxPtr, C.uint32_t(priority), C.size_t(index))) +} + +func (c *dymClusterLbContext) GetOverrideHost() (shared.UnsafeEnvoyBuffer, bool, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var strict C.bool + ok := C.envoy_dynamic_module_callback_cluster_lb_context_get_override_host(c.hostCtxPtr, &buf, &strict) + if !bool(ok) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(strict), bool(ok) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), bool(strict), true +} + +func (c *dymClusterLbContext) GetDownstreamConnectionSNI() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_cluster_lb_context_get_downstream_connection_sni(c.hostCtxPtr, &buf) + if !bool(ok) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ok) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +// dymClusterAsyncCompletion is the SDK-provided shared.ClusterAsyncCompletion handed to +// ClusterLoadBalancer.ChooseHost. The module calls Complete (possibly from another goroutine) +// when async host selection finishes; the SDK then dispatches to Envoy's async-completion +// callback. +// +// The wrapper carries enough state to (a) deliver the result to Envoy, (b) cancel itself +// from both the per-LB tracking map and the global async manager, and (c) coordinate with +// the cancel path so completion-after-cancel and double-completion are safe no-ops. +type dymClusterAsyncCompletion struct { + lbWrapper *clusterLbWrapper + hostCtxPtr C.envoy_dynamic_module_type_cluster_lb_context_envoy_ptr + // userSelection is the module's optional ClusterAsyncHostSelection; Cancel is dispatched + // to it when the SDK observes a cancellation from Envoy. nil if the module returned + // nil for the async handle. + userSelection shared.ClusterAsyncHostSelection + // managerPtr is this completion's key in clusterAsyncCompletionManager. Stored so + // Complete (called from arbitrary goroutines) can remove itself. + managerPtr unsafe.Pointer + // done is set on first Complete OR Cancel; further calls in either direction are + // no-ops. This makes Complete↔Cancel safe regardless of order. + done atomic.Bool +} + +func (a *dymClusterAsyncCompletion) Complete(host shared.ClusterHost, details string) { + if a.done.Swap(true) { + return + } + if a.lbWrapper == nil { + return + } + C.envoy_dynamic_module_callback_cluster_lb_async_host_selection_complete( + a.lbWrapper.hostLbPtr, + a.hostCtxPtr, + C.envoy_dynamic_module_type_cluster_host_envoy_ptr(shared.UnsafeClusterHostPtr(host)), + stringToModuleBuffer(details), + ) + runtime.KeepAlive(details) + // Drop from per-LB tracking and from the global async manager so the wrapper can be GC'd. + // asyncHandles may already be nil if lb_destroy drained it first. + a.lbWrapper.asyncMu.Lock() + if a.lbWrapper.asyncHandles != nil { + delete(a.lbWrapper.asyncHandles, a) + } + a.lbWrapper.asyncMu.Unlock() + if a.managerPtr != nil { + clusterAsyncCompletionManager.remove(a.managerPtr) + } +} + +// ============================================================================= +// Event hooks +// ============================================================================= + +//export envoy_dynamic_module_on_cluster_config_new +func envoy_dynamic_module_on_cluster_config_new( + hostConfigPtr C.envoy_dynamic_module_type_cluster_config_envoy_ptr, + name C.envoy_dynamic_module_type_envoy_buffer, + config C.envoy_dynamic_module_type_envoy_buffer, +) C.envoy_dynamic_module_type_cluster_config_module_ptr { + nameStr := envoyBufferToStringUnsafe(name) + configBytes := envoyBufferToBytesCopy(config) + + configHandle := &dymClusterConfigHandle{hostConfigPtr: hostConfigPtr} + configFactory := sdk.GetClusterConfigFactory(nameStr) + if configFactory == nil { + hostLog(shared.LogLevelWarn, "Failed to load cluster configuration for %q: no factory registered", []any{nameStr}) + return nil + } + factory, err := configFactory.Create(configHandle, configBytes) + if err != nil { + hostLog(shared.LogLevelWarn, "Failed to load cluster configuration for %q: %v", []any{nameStr, err}) + return nil + } + if factory == nil { + hostLog(shared.LogLevelWarn, "Failed to load cluster configuration for %q: factory returned nil", []any{nameStr}) + return nil + } + wrapper := &clusterConfigWrapper{ + hostConfigPtr: hostConfigPtr, + factory: factory, + configHandle: configHandle, + } + configPtr := clusterConfigManager.record(wrapper) + return C.envoy_dynamic_module_type_cluster_config_module_ptr(configPtr) +} + +//export envoy_dynamic_module_on_cluster_config_destroy +func envoy_dynamic_module_on_cluster_config_destroy( + configPtr C.envoy_dynamic_module_type_cluster_config_module_ptr, +) { + w := clusterConfigManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil { + return + } + w.factory.OnDestroy() + clusterConfigManager.remove(unsafe.Pointer(configPtr)) +} + +//export envoy_dynamic_module_on_cluster_new +func envoy_dynamic_module_on_cluster_new( + configPtr C.envoy_dynamic_module_type_cluster_config_module_ptr, + hostClusterPtr C.envoy_dynamic_module_type_cluster_envoy_ptr, +) C.envoy_dynamic_module_type_cluster_module_ptr { + cfg := clusterConfigManager.unwrap(unsafe.Pointer(configPtr)) + if cfg == nil { + return nil + } + wrapper := &clusterWrapper{ + hostClusterPtr: hostClusterPtr, + configRef: cfg, + calloutCallbacks: make(map[uint64]shared.HttpCalloutCallback), + } + cluster := cfg.factory.Create(cfg.configHandle) + if cluster == nil { + return nil + } + wrapper.cluster = cluster + clusterPtr := clusterManager.record(wrapper) + return C.envoy_dynamic_module_type_cluster_module_ptr(clusterPtr) +} + +// Init is the first lifecycle callback after _new; it must not see a destroyed wrapper +// (Envoy serializes _new -> _init -> _destroy on the main thread). The destroyed-flag +// guards on subsequent hooks below cover the case where a late callback races with the +// destroy hook running concurrently on the same thread. +// +//export envoy_dynamic_module_on_cluster_init +func envoy_dynamic_module_on_cluster_init( + hostClusterPtr C.envoy_dynamic_module_type_cluster_envoy_ptr, + clusterPtr C.envoy_dynamic_module_type_cluster_module_ptr, +) { + w := clusterManager.unwrap(unsafe.Pointer(clusterPtr)) + if w == nil || w.cluster == nil { + return + } + w.hostClusterPtr = hostClusterPtr + w.cluster.OnInit(&dymClusterHandle{wrapper: w}) +} + +//export envoy_dynamic_module_on_cluster_destroy +func envoy_dynamic_module_on_cluster_destroy( + clusterPtr C.envoy_dynamic_module_type_cluster_module_ptr, +) { + w := clusterManager.unwrap(unsafe.Pointer(clusterPtr)) + if w == nil || w.destroyed { + return + } + w.destroyed = true + if w.cluster != nil { + w.cluster.OnDestroy() + } + if w.scheduler != nil { + // See bootstrap config destroy for why we close synchronously. + w.scheduler.close() + w.scheduler = nil + } + clusterManager.remove(unsafe.Pointer(clusterPtr)) +} + +//export envoy_dynamic_module_on_cluster_lb_new +func envoy_dynamic_module_on_cluster_lb_new( + clusterPtr C.envoy_dynamic_module_type_cluster_module_ptr, + hostLbPtr C.envoy_dynamic_module_type_cluster_lb_envoy_ptr, +) C.envoy_dynamic_module_type_cluster_lb_module_ptr { + w := clusterManager.unwrap(unsafe.Pointer(clusterPtr)) + if w == nil || w.cluster == nil { + return nil + } + lbWrapper := &clusterLbWrapper{ + hostLbPtr: hostLbPtr, + clusterRef: w, + asyncHandles: make(map[*dymClusterAsyncCompletion]struct{}), + } + handle := &dymClusterLoadBalancerHandle{wrapper: lbWrapper} + lb := w.cluster.NewLoadBalancer(handle) + if lb == nil { + return nil + } + lbWrapper.lb = lb + lbPtr := clusterLbManager.record(lbWrapper) + return C.envoy_dynamic_module_type_cluster_lb_module_ptr(lbPtr) +} + +//export envoy_dynamic_module_on_cluster_lb_destroy +func envoy_dynamic_module_on_cluster_lb_destroy( + lbPtr C.envoy_dynamic_module_type_cluster_lb_module_ptr, +) { + w := clusterLbManager.unwrap(unsafe.Pointer(lbPtr)) + if w == nil || w.destroyed { + return + } + w.destroyed = true + // Cancel all in-flight async completions before calling OnDestroy. Any goroutine that + // subsequently calls Complete will see done=true and skip the C callback, which is + // necessary because hostLbPtr is freed immediately after this hook returns. + w.asyncMu.Lock() + pending := make([]*dymClusterAsyncCompletion, 0, len(w.asyncHandles)) + for a := range w.asyncHandles { + pending = append(pending, a) + } + w.asyncHandles = nil + w.asyncMu.Unlock() + for _, a := range pending { + if !a.done.Swap(true) { + if a.userSelection != nil { + a.userSelection.Cancel() + } + if a.managerPtr != nil { + clusterAsyncCompletionManager.remove(a.managerPtr) + } + } + } + if w.lb != nil { + w.lb.OnDestroy() + } + clusterLbManager.remove(unsafe.Pointer(lbPtr)) +} + +//export envoy_dynamic_module_on_cluster_lb_choose_host +func envoy_dynamic_module_on_cluster_lb_choose_host( + lbPtr C.envoy_dynamic_module_type_cluster_lb_module_ptr, + hostCtxPtr C.envoy_dynamic_module_type_cluster_lb_context_envoy_ptr, + hostOut *C.envoy_dynamic_module_type_cluster_host_envoy_ptr, + asyncOut *C.envoy_dynamic_module_type_cluster_lb_async_handle_module_ptr, +) { + w := clusterLbManager.unwrap(unsafe.Pointer(lbPtr)) + if w == nil || w.lb == nil || w.destroyed { + *hostOut = nil + *asyncOut = nil + return + } + ctx := &dymClusterLbContext{hostCtxPtr: hostCtxPtr, lbWrapper: w} + // Pre-allocate the SDK completion handle so the module always has a real callable + // object to store; if the module returns sync, we discard it without registering. + completion := &dymClusterAsyncCompletion{lbWrapper: w, hostCtxPtr: hostCtxPtr} + host, userAsync, ok := w.lb.ChooseHost(&dymClusterLoadBalancerHandle{wrapper: w}, ctx, completion) + if !ok { + *hostOut = nil + *asyncOut = nil + return + } + if userAsync != nil { + // Async path. Bind the completion to the user's selection (used for cancel + // dispatch), then register so Envoy can address it via the returned async-handle + // pointer. + completion.userSelection = userAsync + w.asyncMu.Lock() + w.asyncHandles[completion] = struct{}{} + w.asyncMu.Unlock() + ptr := clusterAsyncCompletionManager.record(completion) + completion.managerPtr = ptr + *hostOut = nil + *asyncOut = C.envoy_dynamic_module_type_cluster_lb_async_handle_module_ptr(ptr) + return + } + *hostOut = C.envoy_dynamic_module_type_cluster_host_envoy_ptr(shared.UnsafeClusterHostPtr(host)) + *asyncOut = nil +} + +//export envoy_dynamic_module_on_cluster_lb_cancel_host_selection +func envoy_dynamic_module_on_cluster_lb_cancel_host_selection( + lbPtr C.envoy_dynamic_module_type_cluster_lb_module_ptr, + asyncPtr C.envoy_dynamic_module_type_cluster_lb_async_handle_module_ptr, +) { + w := clusterLbManager.unwrap(unsafe.Pointer(lbPtr)) + if w == nil || w.lb == nil || w.destroyed { + return + } + a := clusterAsyncCompletionManager.unwrap(unsafe.Pointer(asyncPtr)) + if a == nil { + return + } + // Mark done first to prevent a racing Complete from also dispatching to Envoy. + if a.done.Swap(true) { + // Already completed; Complete already removed itself, nothing to do. + return + } + if a.userSelection != nil { + a.userSelection.Cancel() + } + w.asyncMu.Lock() + delete(w.asyncHandles, a) + w.asyncMu.Unlock() + clusterAsyncCompletionManager.remove(unsafe.Pointer(asyncPtr)) +} + +//export envoy_dynamic_module_on_cluster_scheduled +func envoy_dynamic_module_on_cluster_scheduled( + hostClusterPtr C.envoy_dynamic_module_type_cluster_envoy_ptr, + clusterPtr C.envoy_dynamic_module_type_cluster_module_ptr, + eventID C.uint64_t, +) { + w := clusterManager.unwrap(unsafe.Pointer(clusterPtr)) + if w == nil || w.scheduler == nil || w.destroyed { + return + } + w.hostClusterPtr = hostClusterPtr + w.scheduler.onScheduled(uint64(eventID)) +} + +//export envoy_dynamic_module_on_cluster_server_initialized +func envoy_dynamic_module_on_cluster_server_initialized( + hostClusterPtr C.envoy_dynamic_module_type_cluster_envoy_ptr, + clusterPtr C.envoy_dynamic_module_type_cluster_module_ptr, +) { + w := clusterManager.unwrap(unsafe.Pointer(clusterPtr)) + if w == nil || w.cluster == nil || w.destroyed { + return + } + w.hostClusterPtr = hostClusterPtr + w.cluster.OnServerInitialized(&dymClusterHandle{wrapper: w}) +} + +//export envoy_dynamic_module_on_cluster_drain_started +func envoy_dynamic_module_on_cluster_drain_started( + hostClusterPtr C.envoy_dynamic_module_type_cluster_envoy_ptr, + clusterPtr C.envoy_dynamic_module_type_cluster_module_ptr, +) { + w := clusterManager.unwrap(unsafe.Pointer(clusterPtr)) + if w == nil || w.cluster == nil || w.destroyed { + return + } + w.hostClusterPtr = hostClusterPtr + w.cluster.OnDrainStarted(&dymClusterHandle{wrapper: w}) +} + +//export envoy_dynamic_module_on_cluster_shutdown +func envoy_dynamic_module_on_cluster_shutdown( + hostClusterPtr C.envoy_dynamic_module_type_cluster_envoy_ptr, + clusterPtr C.envoy_dynamic_module_type_cluster_module_ptr, + completionCallback C.envoy_dynamic_module_type_event_cb, + completionContext unsafe.Pointer, +) { + w := clusterManager.unwrap(unsafe.Pointer(clusterPtr)) + if w == nil || w.cluster == nil || w.destroyed { + C.cgoClusterInvokeEventCb(completionCallback, completionContext) + return + } + completion := &clusterShutdownCompletion{cb: completionCallback, context: completionContext} + w.shutdownMu.Lock() + w.shutdownCompletion = completion + w.shutdownMu.Unlock() + w.hostClusterPtr = hostClusterPtr + w.cluster.OnShutdown(&dymClusterHandle{wrapper: w}, func() { + if completion.done.Swap(true) { + return + } + C.cgoClusterInvokeEventCb(completion.cb, completion.context) + }) +} + +//export envoy_dynamic_module_on_cluster_http_callout_done +func envoy_dynamic_module_on_cluster_http_callout_done( + _ C.envoy_dynamic_module_type_cluster_envoy_ptr, + clusterPtr C.envoy_dynamic_module_type_cluster_module_ptr, + calloutID C.uint64_t, + result C.envoy_dynamic_module_type_http_callout_result, + headers *C.envoy_dynamic_module_type_envoy_http_header, + headersSize C.size_t, + chunks *C.envoy_dynamic_module_type_envoy_buffer, + chunksSize C.size_t, +) { + w := clusterManager.unwrap(unsafe.Pointer(clusterPtr)) + if w == nil || w.destroyed { + return + } + resultHeaders := envoyHttpHeaderSliceToUnsafeHeaderSlice(unsafe.Slice(headers, int(headersSize))) + resultChunks := envoyBufferSliceToUnsafeEnvoyBufferSlice(unsafe.Slice(chunks, int(chunksSize))) + w.calloutMu.Lock() + cb := w.calloutCallbacks[uint64(calloutID)] + delete(w.calloutCallbacks, uint64(calloutID)) + w.calloutMu.Unlock() + if cb != nil { + cb.OnHttpCalloutDone(uint64(calloutID), shared.HttpCalloutResult(result), resultHeaders, resultChunks) + } +} + +//export envoy_dynamic_module_on_cluster_lb_on_host_membership_update +func envoy_dynamic_module_on_cluster_lb_on_host_membership_update( + hostLbPtr C.envoy_dynamic_module_type_cluster_lb_envoy_ptr, + lbPtr C.envoy_dynamic_module_type_cluster_lb_module_ptr, + numHostsAdded C.size_t, + numHostsRemoved C.size_t, +) { + w := clusterLbManager.unwrap(unsafe.Pointer(lbPtr)) + if w == nil || w.lb == nil || w.destroyed { + return + } + w.hostLbPtr = hostLbPtr + w.lb.OnHostMembershipUpdate(&dymClusterLoadBalancerHandle{wrapper: w}, uint64(numHostsAdded), uint64(numHostsRemoved)) +} diff --git a/source/extensions/dynamic_modules/sdk/go/abi/dns_resolver.go b/source/extensions/dynamic_modules/sdk/go/abi/dns_resolver.go new file mode 100644 index 0000000000000..001b4a3b337be --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/abi/dns_resolver.go @@ -0,0 +1,345 @@ +package abi + +/* +#cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup +#cgo linux LDFLAGS: -Wl,--unresolved-symbols=ignore-all +#include +#include +#include +#include +#include "../../../abi/abi.h" +*/ +import "C" +import ( + "runtime" + "unsafe" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +type dnsResolverConfigWrapper struct { + factory shared.DnsResolverFactory + configHandle *dymDnsResolverConfigHandle + + // destroyed is set during config_destroy so any late resolver creation becomes a + // no-op instead of re-entering user code. + destroyed bool +} + +type dnsResolverWrapper struct { + resolver shared.DnsResolver + configRef *dnsResolverConfigWrapper // for ResolveComplete via the config handle + + // destroyed is set during resolver_destroy so any late resolve / cancel / + // reset_networking callbacks become no-ops. + destroyed bool +} + +// dnsQueryWrapper is a stable Go-allocated cell that backs the opaque query pointer handed to +// Envoy. Because the manager pins these via its internal map, the wrapper's address is a safe, +// stable handle to round-trip through Envoy's ABI. +type dnsQueryWrapper struct { + query any +} + +var dnsResolverConfigManager = newManager[dnsResolverConfigWrapper]() +var dnsResolverManager = newManager[dnsResolverWrapper]() +var dnsQueryManager = newManager[dnsQueryWrapper]() + +// dymDnsResolverConfigHandle implements shared.DnsResolverConfigHandle. The same handle is used +// for ResolveComplete (the Envoy-side resolver pointer) and for metric callbacks (the +// Envoy-side config pointer). +type dymDnsResolverConfigHandle struct { + hostConfigPtr C.envoy_dynamic_module_type_dns_resolver_config_envoy_ptr + hostResolverPtr C.envoy_dynamic_module_type_dns_resolver_envoy_ptr +} + +func (h *dymDnsResolverConfigHandle) ResolveComplete( + queryID uint64, status shared.DnsResolutionStatus, details string, addresses []shared.DnsAddress, +) { + // Convert addresses to C array. + cAddrs := make([]C.envoy_dynamic_module_type_dns_address, len(addresses)) + // Pin Go strings for the duration of the C call by keeping them in a slice. + for i := range addresses { + s := addresses[i].Address + cAddrs[i] = C.envoy_dynamic_module_type_dns_address{ + address_ptr: (*C.char)(unsafe.Pointer(unsafe.StringData(s))), + address_length: C.size_t(len(s)), + ttl_seconds: C.uint32_t(addresses[i].TTLSeconds), + } + } + var cAddrPtr *C.envoy_dynamic_module_type_dns_address + if len(cAddrs) > 0 { + cAddrPtr = unsafe.SliceData(cAddrs) + } + C.envoy_dynamic_module_callback_dns_resolve_complete( + h.hostResolverPtr, + C.uint64_t(queryID), + C.envoy_dynamic_module_type_dns_resolution_status(status), + stringToModuleBuffer(details), + cAddrPtr, + C.size_t(len(cAddrs)), + ) + runtime.KeepAlive(addresses) + runtime.KeepAlive(details) + runtime.KeepAlive(cAddrs) +} + +// stringSlicesToModuleBuffers prepares a Go []string as a C buffer array. Caller must +// runtime.KeepAlive both the input and the returned slice for the duration of the C call. +func stringSlicesToModuleBuffers(values []string) []C.envoy_dynamic_module_type_module_buffer { + if len(values) == 0 { + return nil + } + out := make([]C.envoy_dynamic_module_type_module_buffer, len(values)) + for i, v := range values { + out[i] = stringToModuleBuffer(v) + } + return out +} + +func bufferSlicePtr(b []C.envoy_dynamic_module_type_module_buffer) *C.envoy_dynamic_module_type_module_buffer { + if len(b) == 0 { + return nil + } + return unsafe.SliceData(b) +} + +func (h *dymDnsResolverConfigHandle) DefineCounter(name string, labelNames []string) (shared.MetricID, shared.MetricsResult) { + labels := stringSlicesToModuleBuffers(labelNames) + var id C.size_t + ret := C.envoy_dynamic_module_callback_dns_resolver_config_define_counter( + h.hostConfigPtr, stringToModuleBuffer(name), + bufferSlicePtr(labels), C.size_t(len(labels)), + &id, + ) + runtime.KeepAlive(name) + runtime.KeepAlive(labelNames) + runtime.KeepAlive(labels) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymDnsResolverConfigHandle) IncrementCounter(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_dns_resolver_config_increment_counter( + h.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +func (h *dymDnsResolverConfigHandle) DefineGauge(name string, labelNames []string) (shared.MetricID, shared.MetricsResult) { + labels := stringSlicesToModuleBuffers(labelNames) + var id C.size_t + ret := C.envoy_dynamic_module_callback_dns_resolver_config_define_gauge( + h.hostConfigPtr, stringToModuleBuffer(name), + bufferSlicePtr(labels), C.size_t(len(labels)), + &id, + ) + runtime.KeepAlive(name) + runtime.KeepAlive(labelNames) + runtime.KeepAlive(labels) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymDnsResolverConfigHandle) SetGauge(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_dns_resolver_config_set_gauge( + h.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +func (h *dymDnsResolverConfigHandle) IncrementGauge(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_dns_resolver_config_increment_gauge( + h.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +func (h *dymDnsResolverConfigHandle) DecrementGauge(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_dns_resolver_config_decrement_gauge( + h.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +func (h *dymDnsResolverConfigHandle) DefineHistogram(name string, labelNames []string) (shared.MetricID, shared.MetricsResult) { + labels := stringSlicesToModuleBuffers(labelNames) + var id C.size_t + ret := C.envoy_dynamic_module_callback_dns_resolver_config_define_histogram( + h.hostConfigPtr, stringToModuleBuffer(name), + bufferSlicePtr(labels), C.size_t(len(labels)), + &id, + ) + runtime.KeepAlive(name) + runtime.KeepAlive(labelNames) + runtime.KeepAlive(labels) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymDnsResolverConfigHandle) RecordHistogramValue(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_dns_resolver_config_record_histogram_value( + h.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +// ============================================================================= +// Event hooks +// ============================================================================= + +//export envoy_dynamic_module_on_dns_resolver_config_new +func envoy_dynamic_module_on_dns_resolver_config_new( + hostConfigPtr C.envoy_dynamic_module_type_dns_resolver_config_envoy_ptr, + name C.envoy_dynamic_module_type_envoy_buffer, + config C.envoy_dynamic_module_type_envoy_buffer, +) C.envoy_dynamic_module_type_dns_resolver_config_module_ptr { + nameStr := envoyBufferToStringUnsafe(name) + configBytes := envoyBufferToBytesCopy(config) + + configHandle := &dymDnsResolverConfigHandle{hostConfigPtr: hostConfigPtr} + configFactory := sdk.GetDnsResolverConfigFactory(nameStr) + if configFactory == nil { + hostLog(shared.LogLevelWarn, "Failed to load DNS resolver configuration for %q: no factory registered", []any{nameStr}) + return nil + } + factory, err := configFactory.Create(configHandle, configBytes) + if err != nil { + hostLog(shared.LogLevelWarn, "Failed to load DNS resolver configuration for %q: %v", []any{nameStr, err}) + return nil + } + if factory == nil { + hostLog(shared.LogLevelWarn, "Failed to load DNS resolver configuration for %q: factory returned nil", []any{nameStr}) + return nil + } + wrapper := &dnsResolverConfigWrapper{factory: factory, configHandle: configHandle} + configPtr := dnsResolverConfigManager.record(wrapper) + return C.envoy_dynamic_module_type_dns_resolver_config_module_ptr(configPtr) +} + +//export envoy_dynamic_module_on_dns_resolver_config_destroy +func envoy_dynamic_module_on_dns_resolver_config_destroy( + configPtr C.envoy_dynamic_module_type_dns_resolver_config_module_ptr, +) { + w := dnsResolverConfigManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil || w.destroyed { + return + } + w.destroyed = true + w.factory.OnDestroy() + dnsResolverConfigManager.remove(unsafe.Pointer(configPtr)) +} + +//export envoy_dynamic_module_on_dns_resolver_new +func envoy_dynamic_module_on_dns_resolver_new( + configPtr C.envoy_dynamic_module_type_dns_resolver_config_module_ptr, + hostResolverPtr C.envoy_dynamic_module_type_dns_resolver_envoy_ptr, +) C.envoy_dynamic_module_type_dns_resolver_module_ptr { + cfg := dnsResolverConfigManager.unwrap(unsafe.Pointer(configPtr)) + if cfg == nil || cfg.destroyed { + return nil + } + // Bind the resolver pointer for ResolveComplete callbacks. + cfg.configHandle.hostResolverPtr = hostResolverPtr + + r := cfg.factory.Create(cfg.configHandle) + if r == nil { + return nil + } + wrapper := &dnsResolverWrapper{ + resolver: r, + configRef: cfg, + } + resolverPtr := dnsResolverManager.record(wrapper) + return C.envoy_dynamic_module_type_dns_resolver_module_ptr(resolverPtr) +} + +//export envoy_dynamic_module_on_dns_resolver_destroy +func envoy_dynamic_module_on_dns_resolver_destroy( + resolverPtr C.envoy_dynamic_module_type_dns_resolver_module_ptr, +) { + w := dnsResolverManager.unwrap(unsafe.Pointer(resolverPtr)) + if w == nil || w.destroyed { + return + } + w.destroyed = true + if w.resolver != nil { + w.resolver.OnDestroy() + } + dnsResolverManager.remove(unsafe.Pointer(resolverPtr)) +} + +//export envoy_dynamic_module_on_dns_resolve +func envoy_dynamic_module_on_dns_resolve( + resolverPtr C.envoy_dynamic_module_type_dns_resolver_module_ptr, + dnsName C.envoy_dynamic_module_type_envoy_buffer, + family C.envoy_dynamic_module_type_dns_lookup_family, + queryID C.uint64_t, +) C.envoy_dynamic_module_type_dns_query_module_ptr { + w := dnsResolverManager.unwrap(unsafe.Pointer(resolverPtr)) + if w == nil || w.resolver == nil || w.destroyed { + return nil + } + dnsNameStr := envoyBufferToStringUnsafe(dnsName) + query := w.resolver.Resolve(w.configRef.configHandle, dnsNameStr, shared.DnsLookupFamily(family), uint64(queryID)) + if query == nil { + return nil + } + // Wrap the module-side query in a stable Go-allocated cell whose address can safely be + // round-tripped through Envoy's ABI as the opaque query handle. + wrapper := &dnsQueryWrapper{query: query} + queryPtr := dnsQueryManager.record(wrapper) + return C.envoy_dynamic_module_type_dns_query_module_ptr(queryPtr) +} + +//export envoy_dynamic_module_on_dns_resolve_cancel +func envoy_dynamic_module_on_dns_resolve_cancel( + resolverPtr C.envoy_dynamic_module_type_dns_resolver_module_ptr, + queryPtr C.envoy_dynamic_module_type_dns_query_module_ptr, +) { + w := dnsResolverManager.unwrap(unsafe.Pointer(resolverPtr)) + if w == nil || w.resolver == nil || w.destroyed { + return + } + q := dnsQueryManager.unwrap(unsafe.Pointer(queryPtr)) + if q == nil { + return + } + w.resolver.Cancel(q.query) + dnsQueryManager.remove(unsafe.Pointer(queryPtr)) +} + +//export envoy_dynamic_module_on_dns_resolver_reset_networking +func envoy_dynamic_module_on_dns_resolver_reset_networking( + resolverPtr C.envoy_dynamic_module_type_dns_resolver_module_ptr, +) { + w := dnsResolverManager.unwrap(unsafe.Pointer(resolverPtr)) + if w == nil || w.resolver == nil || w.destroyed { + return + } + w.resolver.ResetNetworking() +} diff --git a/source/extensions/dynamic_modules/sdk/go/abi/internal.go b/source/extensions/dynamic_modules/sdk/go/abi/http.go similarity index 82% rename from source/extensions/dynamic_modules/sdk/go/abi/internal.go rename to source/extensions/dynamic_modules/sdk/go/abi/http.go index d0dd58b972607..b5d63c3fd92f8 100644 --- a/source/extensions/dynamic_modules/sdk/go/abi/internal.go +++ b/source/extensions/dynamic_modules/sdk/go/abi/http.go @@ -2,6 +2,7 @@ package abi /* #cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup +#cgo linux LDFLAGS: -Wl,--unresolved-symbols=ignore-all #include #include #include @@ -15,7 +16,6 @@ import ( _ "embed" "fmt" "runtime" - "strconv" "sync" "unsafe" @@ -32,12 +32,6 @@ type httpFilterConfigWrapperPerRoute struct { config any } -type httpFilterWrapper = dymHttpFilterHandle - -type httpFilterSharedDataWrapper struct { - data any -} - const numManagerShards = 32 // The managers to keep track of configs and plugins. @@ -51,16 +45,19 @@ func (m *manager[T]) record(item *T) unsafe.Pointer { index := uintptr(pointer) % numManagerShards m.mutex[index].Lock() defer m.mutex[index].Unlock() - // Assume the map is initialized. m.data[index][uintptr(pointer)] = item return pointer } +// unwrap returns the live wrapper for the given pointer key, or nil if the wrapper is no +// longer registered (e.g., remove already ran). Callers MUST handle a nil return — a stale +// pointer cast would otherwise alias freed memory and crash or corrupt unrelated state when +// Envoy delivers a late callback after destroy. func (m *manager[T]) unwrap(itemPtr unsafe.Pointer) *T { - return (*T)(itemPtr) -} - -func (m *manager[T]) search(key uintptr) *T { + if itemPtr == nil { + return nil + } + key := uintptr(itemPtr) index := key % numManagerShards m.mutex[index].Lock() defer m.mutex[index].Unlock() @@ -84,8 +81,7 @@ func newManager[T any]() *manager[T] { var configManager = newManager[httpFilterConfigWrapper]() var configPerRouteManager = newManager[httpFilterConfigWrapperPerRoute]() -var pluginManager = newManager[httpFilterWrapper]() -var sharedDataManager = newManager[httpFilterSharedDataWrapper]() +var pluginManager = newManager[dymHttpFilterHandle]() //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -145,6 +141,28 @@ func envoyBufferToBytesUnsafe(buf C.envoy_dynamic_module_type_envoy_buffer) []by return unsafe.Slice((*byte)(unsafe.Pointer(buf.ptr)), buf.length) } +// envoyBufferToBytesCopy returns a Go-owned copy of the bytes in the Envoy-owned buffer. +// Use this whenever the bytes will be handed to user code that may retain them past the +// current cgo call (e.g., factory Create methods that store raw config). Adds a single +// allocation + memcpy; appropriate for config-time paths but not for data-path hot loops. +func envoyBufferToBytesCopy(buf C.envoy_dynamic_module_type_envoy_buffer) []byte { + if buf.ptr == nil || buf.length == 0 { + return nil + } + out := make([]byte, buf.length) + copy(out, unsafe.Slice((*byte)(unsafe.Pointer(buf.ptr)), buf.length)) + return out +} + +// envoyBufferToStringCopy returns a Go-owned copy of the bytes as a string. Same lifetime +// concerns as envoyBufferToBytesCopy. +func envoyBufferToStringCopy(buf C.envoy_dynamic_module_type_envoy_buffer) string { + if buf.ptr == nil || buf.length == 0 { + return "" + } + return string(unsafe.Slice((*byte)(unsafe.Pointer(buf.ptr)), buf.length)) +} + func envoyBufferToUnsafeEnvoyBuffer(buf C.envoy_dynamic_module_type_envoy_buffer) shared.UnsafeEnvoyBuffer { return shared.UnsafeEnvoyBuffer{ Ptr: (*byte)(unsafe.Pointer(buf.ptr)), @@ -348,194 +366,6 @@ func (b *dymBodyBuffer) Drain(size uint64) { ) } -type dymScheduler struct { - schedulerPtr unsafe.Pointer - schedulerLock sync.Mutex - nextTaskID uint64 - tasks map[uint64]func() - commitFunc func(unsafe.Pointer, C.uint64_t) -} - -func newDymScheduler( - schedulerPtr unsafe.Pointer, - commitFunc func(unsafe.Pointer, C.uint64_t), -) *dymScheduler { - return &dymScheduler{ - schedulerPtr: schedulerPtr, - tasks: make(map[uint64]func()), - commitFunc: commitFunc, - } -} - -func (s *dymScheduler) Schedule(task func()) { - // Lock the scheduler to prevent concurrent access - s.schedulerLock.Lock() - taskID := s.nextTaskID - s.nextTaskID++ - s.tasks[taskID] = task - s.schedulerLock.Unlock() - - // Call the host to schedule the task, passing the task ID as context - s.commitFunc(s.schedulerPtr, C.uint64_t(taskID)) -} - -func (s *dymScheduler) onScheduled(taskID uint64) { - s.schedulerLock.Lock() - task := s.tasks[taskID] - delete(s.tasks, taskID) - s.schedulerLock.Unlock() - if task != nil { - task() - } -} - -type dymSpan struct { - hostPluginPtr C.envoy_dynamic_module_type_http_filter_envoy_ptr - spanPtr C.envoy_dynamic_module_type_span_envoy_ptr -} - -func (s *dymSpan) SetTag(key, value string) { - if s == nil || s.spanPtr == nil { - return - } - C.envoy_dynamic_module_callback_http_span_set_tag( - s.spanPtr, - stringToModuleBuffer(key), - stringToModuleBuffer(value), - ) - runtime.KeepAlive(key) - runtime.KeepAlive(value) -} - -func (s *dymSpan) SetOperation(operation string) { - if s == nil || s.spanPtr == nil { - return - } - C.envoy_dynamic_module_callback_http_span_set_operation( - s.spanPtr, - stringToModuleBuffer(operation), - ) - runtime.KeepAlive(operation) -} - -func (s *dymSpan) Log(event string) { - if s == nil || s.spanPtr == nil { - return - } - C.envoy_dynamic_module_callback_http_span_log( - s.hostPluginPtr, - s.spanPtr, - stringToModuleBuffer(event), - ) - runtime.KeepAlive(event) -} - -func (s *dymSpan) SetSampled(sampled bool) { - if s == nil || s.spanPtr == nil { - return - } - C.envoy_dynamic_module_callback_http_span_set_sampled(s.spanPtr, C.bool(sampled)) -} - -func (s *dymSpan) GetBaggage(key string) (shared.UnsafeEnvoyBuffer, bool) { - if s == nil || s.spanPtr == nil { - return shared.UnsafeEnvoyBuffer{}, false - } - var valueView C.envoy_dynamic_module_type_envoy_buffer - ret := C.envoy_dynamic_module_callback_http_span_get_baggage( - s.spanPtr, - stringToModuleBuffer(key), - &valueView, - ) - runtime.KeepAlive(key) - if !bool(ret) { - return shared.UnsafeEnvoyBuffer{}, false - } - if valueView.ptr == nil || valueView.length == 0 { - return shared.UnsafeEnvoyBuffer{}, true - } - return envoyBufferToUnsafeEnvoyBuffer(valueView), true -} - -func (s *dymSpan) SetBaggage(key, value string) { - if s == nil || s.spanPtr == nil { - return - } - C.envoy_dynamic_module_callback_http_span_set_baggage( - s.spanPtr, - stringToModuleBuffer(key), - stringToModuleBuffer(value), - ) - runtime.KeepAlive(key) - runtime.KeepAlive(value) -} - -func (s *dymSpan) GetTraceID() (shared.UnsafeEnvoyBuffer, bool) { - if s == nil || s.spanPtr == nil { - return shared.UnsafeEnvoyBuffer{}, false - } - var valueView C.envoy_dynamic_module_type_envoy_buffer - ret := C.envoy_dynamic_module_callback_http_span_get_trace_id(s.spanPtr, &valueView) - if !bool(ret) { - return shared.UnsafeEnvoyBuffer{}, false - } - if valueView.ptr == nil || valueView.length == 0 { - return shared.UnsafeEnvoyBuffer{}, true - } - return envoyBufferToUnsafeEnvoyBuffer(valueView), true -} - -func (s *dymSpan) GetSpanID() (shared.UnsafeEnvoyBuffer, bool) { - if s == nil || s.spanPtr == nil { - return shared.UnsafeEnvoyBuffer{}, false - } - var valueView C.envoy_dynamic_module_type_envoy_buffer - ret := C.envoy_dynamic_module_callback_http_span_get_span_id(s.spanPtr, &valueView) - if !bool(ret) { - return shared.UnsafeEnvoyBuffer{}, false - } - if valueView.ptr == nil || valueView.length == 0 { - return shared.UnsafeEnvoyBuffer{}, true - } - return envoyBufferToUnsafeEnvoyBuffer(valueView), true -} - -func (s *dymSpan) SpawnChild(operation string) shared.ChildSpan { - if s == nil || s.spanPtr == nil { - return nil - } - childPtr := C.envoy_dynamic_module_callback_http_span_spawn_child( - s.hostPluginPtr, - s.spanPtr, - stringToModuleBuffer(operation), - ) - runtime.KeepAlive(operation) - if childPtr == nil { - return nil - } - return &dymChildSpan{ - dymSpan: dymSpan{ - hostPluginPtr: s.hostPluginPtr, - spanPtr: C.envoy_dynamic_module_type_span_envoy_ptr(childPtr), - }, - childPtr: childPtr, - } -} - -type dymChildSpan struct { - dymSpan - childPtr C.envoy_dynamic_module_type_child_span_module_ptr -} - -func (s *dymChildSpan) Finish() { - if s == nil || s.childPtr == nil { - return - } - C.envoy_dynamic_module_callback_http_child_span_finish(s.childPtr) - s.childPtr = nil - s.spanPtr = nil -} - type dymHttpFilterHandle struct { hostPluginPtr C.envoy_dynamic_module_type_http_filter_envoy_ptr @@ -551,16 +381,77 @@ type dymHttpFilterHandle struct { plugin shared.HttpFilter scheduler *dymScheduler streamCompleted bool - streamDestoried bool + streamDestroyed bool localResponseSent bool // nextCalloutID was removed because callout ID is now returned by the host. + // calloutMu guards calloutCallbacks and streamCallbacks. Per-stream HTTP filter + // processing is single-threaded today, so this lock is uncontended in normal use; it's + // held to match the cluster/bootstrap callout maps and to keep the SDK safe if a future + // change introduces cross-thread callout dispatch. + calloutMu sync.Mutex calloutCallbacks map[uint64]shared.HttpCalloutCallback streamCallbacks map[uint64]shared.HttpStreamCallback - recordedSharedData []unsafe.Pointer + // data backs SetData/GetData — per-stream module-private state for cross-phase + // communication. Held in Go memory rather than smuggled through Envoy dynamic metadata + // (which would expose the storage to other filters and observers). + dataMu sync.Mutex + data map[string]any downstreamWatermarkCallbacks shared.DownstreamWatermarkCallbacks + + // childSpans tracks unfinished child spans owned by user code so they can be marked + // as finished (without dispatching to Envoy) when the parent stream is destroyed. + // Without this, a finalizer-driven Finish() after stream destroy would invoke + // envoy_dynamic_module_callback_http_child_span_finish against a freed span pointer. + childSpansMu sync.Mutex + childSpans map[*dymChildSpan]struct{} +} + +// trackChildSpan registers a child span so it will be safely retired if the stream is +// destroyed before the module calls Finish on it. +func (h *dymHttpFilterHandle) trackChildSpan(c *dymChildSpan) { + h.childSpansMu.Lock() + if h.streamDestroyed { + // Stream is already gone. Mark the child as finished without dispatching to Envoy + // and skip finalizer registration. + h.childSpansMu.Unlock() + c.finishedMu.Lock() + c.finished = true + c.finishedMu.Unlock() + return + } + if h.childSpans == nil { + h.childSpans = make(map[*dymChildSpan]struct{}) + } + h.childSpans[c] = struct{}{} + h.childSpansMu.Unlock() + runtime.SetFinalizer(c, func(c *dymChildSpan) { c.Finish() }) +} + +// untrackChildSpan removes a child span from the wrapper's tracking set after the user +// calls Finish on it. +func (h *dymHttpFilterHandle) untrackChildSpan(c *dymChildSpan) { + h.childSpansMu.Lock() + delete(h.childSpans, c) + h.childSpansMu.Unlock() +} + +// retireChildSpansOnDestroy is called from the stream-destroy hook. It marks every +// unfinished child span as finished (so the finalizer, if any, becomes a no-op) and +// clears the finalizer to release the GC anchor. +func (h *dymHttpFilterHandle) retireChildSpansOnDestroy() { + h.childSpansMu.Lock() + spans := h.childSpans + h.childSpans = nil + h.childSpansMu.Unlock() + for c := range spans { + c.finishedMu.Lock() + c.finished = true + c.finishedMu.Unlock() + runtime.SetFinalizer(c, nil) + } } func (h *dymHttpFilterHandle) GetMetadataString(source shared.MetadataSourceType, metadataNamespace, key string) (shared.UnsafeEnvoyBuffer, bool) { @@ -862,7 +753,7 @@ func (h *dymHttpFilterHandle) SetMetadata(metadataNamespace, key string, value a func (h *dymHttpFilterHandle) GetAttributeNumber( attributeID shared.AttributeID, -) (float64, bool) { +) (uint64, bool) { var value C.uint64_t = 0 ret := C.envoy_dynamic_module_callback_http_filter_get_attribute_int( @@ -874,7 +765,7 @@ func (h *dymHttpFilterHandle) GetAttributeNumber( return 0, false } - return float64(value), true + return uint64(value), true } func (h *dymHttpFilterHandle) GetAttributeString( @@ -911,24 +802,6 @@ func (h *dymHttpFilterHandle) GetAttributeBool( return bool(value), true } -func (h *dymHttpFilterHandle) GetFilterStateTyped(key string) (shared.UnsafeEnvoyBuffer, bool) { - var valueView C.envoy_dynamic_module_type_envoy_buffer - - ret := C.envoy_dynamic_module_callback_http_get_filter_state_typed( - h.hostPluginPtr, - stringToModuleBuffer(key), - &valueView, - ) - runtime.KeepAlive(key) - if !bool(ret) { - return shared.UnsafeEnvoyBuffer{}, false - } - if valueView.ptr == nil || valueView.length == 0 { - return shared.UnsafeEnvoyBuffer{}, true - } - return envoyBufferToUnsafeEnvoyBuffer(valueView), true -} - func (h *dymHttpFilterHandle) GetFilterState(key string) (shared.UnsafeEnvoyBuffer, bool) { var valueView C.envoy_dynamic_module_type_envoy_buffer @@ -955,52 +828,22 @@ func (h *dymHttpFilterHandle) SetFilterState(key string, value []byte) { runtime.KeepAlive(value) } -func (h *dymHttpFilterHandle) SetFilterStateTyped(key string, value []byte) bool { - ret := C.envoy_dynamic_module_callback_http_set_filter_state_typed( - h.hostPluginPtr, - stringToModuleBuffer(key), - bytesToModuleBuffer(value), - ) - runtime.KeepAlive(key) - runtime.KeepAlive(value) - return bool(ret) -} - func (h *dymHttpFilterHandle) GetData(key string) any { - buf, found := h.GetMetadataString(shared.MetadataSourceTypeDynamic, - "composer.shared_data", key) - if !found { - return nil - } - // Convert string back to uintptr safely. - uintValue, err := strconv.ParseUint(buf.ToUnsafeString(), 10, 64) - if err != nil { - return nil - } - pointer := uintptr(uintValue) - // Use search rather than unwrap because the go runtime will complain - // the pointer parsed from string `pointer arithmetic result points to invalid allocation`. - wrapper := sharedDataManager.search(pointer) - if wrapper == nil { + h.dataMu.Lock() + defer h.dataMu.Unlock() + if h.data == nil { return nil } - return wrapper.data + return h.data[key] } func (h *dymHttpFilterHandle) SetData(key string, value any) { - wrapper := &httpFilterSharedDataWrapper{data: value} - pointer := sharedDataManager.record(wrapper) - h.recordedSharedData = append(h.recordedSharedData, pointer) - - // Covert pointer to uintptr to string safely. - stringValue := strconv.FormatUint(uint64(uintptr(pointer)), 10) - h.SetMetadata("composer.shared_data", key, stringValue) -} - -func (h *dymHttpFilterHandle) clearData() { - for _, pointer := range h.recordedSharedData { - sharedDataManager.remove(pointer) + h.dataMu.Lock() + defer h.dataMu.Unlock() + if h.data == nil { + h.data = make(map[string]any) } + h.data[key] = value } func (h *dymHttpFilterHandle) SendLocalResponse( @@ -1094,204 +937,32 @@ func (h *dymHttpFilterHandle) RefreshRouteCluster() { C.envoy_dynamic_module_callback_http_clear_route_cluster_cache(h.hostPluginPtr) } -func (h *dymHttpFilterHandle) GetWorkerIndex() uint32 { - return uint32(C.envoy_dynamic_module_callback_http_filter_get_worker_index(h.hostPluginPtr)) +func (h *dymHttpFilterHandle) RequestHeaders() shared.HeaderMap { + return &h.requestHeaderMap } -func (h *dymHttpFilterHandle) SetSocketOptionInt( - level, name int64, - state shared.SocketOptionState, - direction shared.SocketDirection, - value int64, -) bool { - ret := C.envoy_dynamic_module_callback_http_set_socket_option_int( - h.hostPluginPtr, - C.int64_t(level), - C.int64_t(name), - C.envoy_dynamic_module_type_socket_option_state(state), - C.envoy_dynamic_module_type_socket_direction(direction), - C.int64_t(value), - ) - return bool(ret) +func (h *dymHttpFilterHandle) BufferedRequestBody() shared.BodyBuffer { + return &h.bufferedRequestBody } -func (h *dymHttpFilterHandle) SetSocketOptionBytes( - level, name int64, - state shared.SocketOptionState, - direction shared.SocketDirection, - value []byte, -) bool { - ret := C.envoy_dynamic_module_callback_http_set_socket_option_bytes( - h.hostPluginPtr, - C.int64_t(level), - C.int64_t(name), - C.envoy_dynamic_module_type_socket_option_state(state), - C.envoy_dynamic_module_type_socket_direction(direction), - bytesToModuleBuffer(value), - ) - runtime.KeepAlive(value) - return bool(ret) +func (h *dymHttpFilterHandle) ReceivedRequestBody() shared.BodyBuffer { + return &h.receivedRequestBody } -func (h *dymHttpFilterHandle) GetSocketOptionInt( - level, name int64, - state shared.SocketOptionState, - direction shared.SocketDirection, -) (int64, bool) { - var value C.int64_t - ret := C.envoy_dynamic_module_callback_http_get_socket_option_int( - h.hostPluginPtr, - C.int64_t(level), - C.int64_t(name), - C.envoy_dynamic_module_type_socket_option_state(state), - C.envoy_dynamic_module_type_socket_direction(direction), - &value, - ) - if !bool(ret) { - return 0, false - } - return int64(value), true +func (h *dymHttpFilterHandle) RequestTrailers() shared.HeaderMap { + return &h.requestTrailerMap } -func (h *dymHttpFilterHandle) GetSocketOptionBytes( - level, name int64, - state shared.SocketOptionState, - direction shared.SocketDirection, -) (shared.UnsafeEnvoyBuffer, bool) { - var valueView C.envoy_dynamic_module_type_envoy_buffer - ret := C.envoy_dynamic_module_callback_http_get_socket_option_bytes( - h.hostPluginPtr, - C.int64_t(level), - C.int64_t(name), - C.envoy_dynamic_module_type_socket_option_state(state), - C.envoy_dynamic_module_type_socket_direction(direction), - &valueView, - ) - if !bool(ret) { - return shared.UnsafeEnvoyBuffer{}, false - } - if valueView.ptr == nil || valueView.length == 0 { - return shared.UnsafeEnvoyBuffer{}, true - } - return envoyBufferToUnsafeEnvoyBuffer(valueView), true +func (h *dymHttpFilterHandle) ResponseHeaders() shared.HeaderMap { + return &h.responseHeaderMap } -func (h *dymHttpFilterHandle) GetBufferLimit() uint64 { - return uint64(C.envoy_dynamic_module_callback_http_get_buffer_limit(h.hostPluginPtr)) +func (h *dymHttpFilterHandle) BufferedResponseBody() shared.BodyBuffer { + return &h.bufferedResponseBody } -func (h *dymHttpFilterHandle) SetBufferLimit(limit uint64) { - C.envoy_dynamic_module_callback_http_set_buffer_limit(h.hostPluginPtr, C.uint64_t(limit)) -} - -func (h *dymHttpFilterHandle) GetActiveSpan() shared.Span { - spanPtr := C.envoy_dynamic_module_callback_http_get_active_span(h.hostPluginPtr) - if spanPtr == nil { - return nil - } - return &dymSpan{ - hostPluginPtr: h.hostPluginPtr, - spanPtr: spanPtr, - } -} - -func (h *dymHttpFilterHandle) GetClusterName() (shared.UnsafeEnvoyBuffer, bool) { - var valueView C.envoy_dynamic_module_type_envoy_buffer - ret := C.envoy_dynamic_module_callback_http_get_cluster_name(h.hostPluginPtr, &valueView) - if !bool(ret) { - return shared.UnsafeEnvoyBuffer{}, false - } - if valueView.ptr == nil || valueView.length == 0 { - return shared.UnsafeEnvoyBuffer{}, true - } - return envoyBufferToUnsafeEnvoyBuffer(valueView), true -} - -func (h *dymHttpFilterHandle) GetClusterHostCounts(priority uint32) (shared.ClusterHostCounts, bool) { - var total C.size_t - var healthy C.size_t - var degraded C.size_t - ret := C.envoy_dynamic_module_callback_http_get_cluster_host_count( - h.hostPluginPtr, - C.uint32_t(priority), - &total, - &healthy, - °raded, - ) - if !bool(ret) { - return shared.ClusterHostCounts{}, false - } - return shared.ClusterHostCounts{ - Total: uint64(total), - Healthy: uint64(healthy), - Degraded: uint64(degraded), - }, true -} - -func (h *dymHttpFilterHandle) SetUpstreamOverrideHost(host string, strict bool) bool { - ret := C.envoy_dynamic_module_callback_http_set_upstream_override_host( - h.hostPluginPtr, - stringToModuleBuffer(host), - C.bool(strict), - ) - runtime.KeepAlive(host) - return bool(ret) -} - -func (h *dymHttpFilterHandle) ResetStream(reason shared.HttpFilterStreamResetReason, details string) { - C.envoy_dynamic_module_callback_http_filter_reset_stream( - h.hostPluginPtr, - C.envoy_dynamic_module_type_http_filter_stream_reset_reason(reason), - stringToModuleBuffer(details), - ) - runtime.KeepAlive(details) -} - -func (h *dymHttpFilterHandle) SendGoAwayAndClose(graceful bool) { - C.envoy_dynamic_module_callback_http_filter_send_go_away_and_close( - h.hostPluginPtr, - C.bool(graceful), - ) -} - -func (h *dymHttpFilterHandle) RecreateStream(headers [][2]string) bool { - headerViews := headersToModuleHttpHeaderSlice(headers) - ret := C.envoy_dynamic_module_callback_http_filter_recreate_stream( - h.hostPluginPtr, - unsafe.SliceData(headerViews), - C.size_t(len(headerViews)), - ) - runtime.KeepAlive(headers) - runtime.KeepAlive(headerViews) - return bool(ret) -} - -func (h *dymHttpFilterHandle) RequestHeaders() shared.HeaderMap { - return &h.requestHeaderMap -} - -func (h *dymHttpFilterHandle) BufferedRequestBody() shared.BodyBuffer { - return &h.bufferedRequestBody -} - -func (h *dymHttpFilterHandle) ReceivedRequestBody() shared.BodyBuffer { - return &h.receivedRequestBody -} - -func (h *dymHttpFilterHandle) RequestTrailers() shared.HeaderMap { - return &h.requestTrailerMap -} - -func (h *dymHttpFilterHandle) ResponseHeaders() shared.HeaderMap { - return &h.responseHeaderMap -} - -func (h *dymHttpFilterHandle) BufferedResponseBody() shared.BodyBuffer { - return &h.bufferedResponseBody -} - -func (h *dymHttpFilterHandle) ReceivedResponseBody() shared.BodyBuffer { - return &h.receivedResponseBody +func (h *dymHttpFilterHandle) ReceivedResponseBody() shared.BodyBuffer { + return &h.receivedResponseBody } func (h *dymHttpFilterHandle) ReceivedBufferedRequestBody() bool { @@ -1335,13 +1006,18 @@ func (h *dymHttpFilterHandle) GetScheduler() shared.Scheduler { taskID, ) }, + func(p unsafe.Pointer) { + C.envoy_dynamic_module_callback_http_filter_scheduler_delete( + (C.envoy_dynamic_module_type_http_filter_scheduler_module_ptr)(p), + ) + }, ) - runtime.SetFinalizer(h.scheduler, func(s *dymScheduler) { - C.envoy_dynamic_module_callback_http_filter_scheduler_delete( - (C.envoy_dynamic_module_type_http_filter_scheduler_module_ptr)(s.schedulerPtr), - ) - }) + // Finalizer is a fallback for the case where the host never invokes the destroy + // hook (e.g., embedded use cases). The synchronous close() in the destroy hook is + // the primary path; once close() runs, schedulerPtr is nil and the finalizer is + // a no-op. + runtime.SetFinalizer(h.scheduler, func(s *dymScheduler) { s.close() }) } return h.scheduler } @@ -1377,10 +1053,12 @@ func (h *dymHttpFilterHandle) HttpCallout( return goResult, 0 } + h.calloutMu.Lock() if h.calloutCallbacks == nil { h.calloutCallbacks = make(map[uint64]shared.HttpCalloutCallback) } h.calloutCallbacks[uint64(calloutID)] = cb + h.calloutMu.Unlock() return goResult, uint64(calloutID) } @@ -1394,165 +1072,591 @@ func (h *dymHttpFilterHandle) StartHttpStream( result := C.envoy_dynamic_module_callback_http_filter_start_http_stream( h.hostPluginPtr, - &streamID, - stringToModuleBuffer(cluster), + &streamID, + stringToModuleBuffer(cluster), + unsafe.SliceData(headerViews), + (C.size_t)(len(headerViews)), + bytesToModuleBuffer(body), + (C.bool)(endOfStream), + (C.uint64_t)(timeoutMs), + ) + + runtime.KeepAlive(cluster) + runtime.KeepAlive(headers) + runtime.KeepAlive(body) + runtime.KeepAlive(headerViews) + + goResult := shared.HttpCalloutInitResult(result) + if goResult != shared.HttpCalloutInitSuccess { + return goResult, 0 + } + + h.calloutMu.Lock() + if h.streamCallbacks == nil { + h.streamCallbacks = make(map[uint64]shared.HttpStreamCallback) + } + h.streamCallbacks[uint64(streamID)] = cb + h.calloutMu.Unlock() + + return goResult, uint64(streamID) +} + +func (h *dymHttpFilterHandle) SendHttpStreamData( + streamID uint64, data []byte, endOfStream bool, +) bool { + ret := C.envoy_dynamic_module_callback_http_stream_send_data( + h.hostPluginPtr, + (C.uint64_t)(streamID), + bytesToModuleBuffer(data), + (C.bool)(endOfStream), + ) + runtime.KeepAlive(data) + return bool(ret) +} + +func (h *dymHttpFilterHandle) SendHttpStreamTrailers( + streamID uint64, trailers [][2]string, +) bool { + // Prepare trailers. + trailerViews := headersToModuleHttpHeaderSlice(trailers) + ret := C.envoy_dynamic_module_callback_http_stream_send_trailers( + h.hostPluginPtr, + (C.uint64_t)(streamID), + unsafe.SliceData(trailerViews), + (C.size_t)(len(trailerViews)), + ) + runtime.KeepAlive(trailers) + runtime.KeepAlive(trailerViews) + return bool(ret) +} + +func (h *dymHttpFilterHandle) ResetHttpStream( + streamID uint64, +) { + C.envoy_dynamic_module_callback_http_filter_reset_http_stream( + h.hostPluginPtr, + (C.uint64_t)(streamID), + ) +} + +func (h *dymHttpFilterHandle) SetDownstreamWatermarkCallbacks( + cbs shared.DownstreamWatermarkCallbacks, +) { + h.downstreamWatermarkCallbacks = cbs +} + +func (h *dymHttpFilterHandle) ClearDownstreamWatermarkCallbacks() { + h.downstreamWatermarkCallbacks = nil +} + +func (h *dymHttpFilterHandle) RecordHistogramValue(id shared.MetricID, + value uint64, tagsValues ...string) shared.MetricsResult { + idUint64 := uint64(id) + // Prepare tag values. + tagValueViews := stringArrayToModuleBufferSlice(tagsValues) + + ret := C.envoy_dynamic_module_callback_http_filter_record_histogram_value( + h.hostPluginPtr, + (C.size_t)(idUint64), + unsafe.SliceData(tagValueViews), + (C.size_t)(len(tagValueViews)), + (C.uint64_t)(value), + ) + + runtime.KeepAlive(tagsValues) + runtime.KeepAlive(tagValueViews) + return shared.MetricsResult(ret) +} + +func (h *dymHttpFilterHandle) SetGaugeValue(id shared.MetricID, + value uint64, tagsValues ...string) shared.MetricsResult { + idUint64 := uint64(id) + // Prepare tag values. + tagValueViews := stringArrayToModuleBufferSlice(tagsValues) + + ret := C.envoy_dynamic_module_callback_http_filter_set_gauge( + h.hostPluginPtr, + (C.size_t)(idUint64), + unsafe.SliceData(tagValueViews), + (C.size_t)(len(tagValueViews)), + (C.uint64_t)(value), + ) + + runtime.KeepAlive(tagsValues) + runtime.KeepAlive(tagValueViews) + return shared.MetricsResult(ret) +} + +func (h *dymHttpFilterHandle) IncrementGaugeValue(id shared.MetricID, + value uint64, tagsValues ...string) shared.MetricsResult { + // Prepare tag values. + tagValueViews := stringArrayToModuleBufferSlice(tagsValues) + ret := C.envoy_dynamic_module_callback_http_filter_increment_gauge( + h.hostPluginPtr, + (C.size_t)(uint64(id)), + unsafe.SliceData(tagValueViews), + (C.size_t)(len(tagValueViews)), + (C.uint64_t)(value), + ) + runtime.KeepAlive(tagsValues) + runtime.KeepAlive(tagValueViews) + return shared.MetricsResult(ret) +} + +func (h *dymHttpFilterHandle) DecrementGaugeValue(id shared.MetricID, + value uint64, tagsValues ...string) shared.MetricsResult { + // Prepare tag values. + tagValueViews := stringArrayToModuleBufferSlice(tagsValues) + ret := C.envoy_dynamic_module_callback_http_filter_decrement_gauge( + h.hostPluginPtr, + (C.size_t)(uint64(id)), + unsafe.SliceData(tagValueViews), + (C.size_t)(len(tagValueViews)), + (C.uint64_t)(value), + ) + runtime.KeepAlive(tagsValues) + runtime.KeepAlive(tagValueViews) + return shared.MetricsResult(ret) +} + +func (h *dymHttpFilterHandle) IncrementCounterValue(id shared.MetricID, + value uint64, tagsValues ...string) shared.MetricsResult { + // Prepare tag values. + tagValueViews := stringArrayToModuleBufferSlice(tagsValues) + ret := C.envoy_dynamic_module_callback_http_filter_increment_counter( + h.hostPluginPtr, + (C.size_t)(uint64(id)), + unsafe.SliceData(tagValueViews), + (C.size_t)(len(tagValueViews)), + (C.uint64_t)(value), + ) + runtime.KeepAlive(tagsValues) + runtime.KeepAlive(tagValueViews) + return shared.MetricsResult(ret) +} + +func (h *dymHttpFilterHandle) GetWorkerIndex() uint32 { + return uint32(C.envoy_dynamic_module_callback_http_filter_get_worker_index( + h.hostPluginPtr, + )) +} + +func (h *dymHttpFilterHandle) GetFilterStateTyped(key string) (shared.UnsafeEnvoyBuffer, bool) { + var valueView C.envoy_dynamic_module_type_envoy_buffer + + ret := C.envoy_dynamic_module_callback_http_get_filter_state_typed( + h.hostPluginPtr, + stringToModuleBuffer(key), + &valueView, + ) + runtime.KeepAlive(key) + if !bool(ret) || valueView.ptr == nil || valueView.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ret) + } + return envoyBufferToUnsafeEnvoyBuffer(valueView), true +} + +func (h *dymHttpFilterHandle) SetFilterStateTyped(key string, value []byte) bool { + ret := C.envoy_dynamic_module_callback_http_set_filter_state_typed( + h.hostPluginPtr, + stringToModuleBuffer(key), + bytesToModuleBuffer(value), + ) + runtime.KeepAlive(key) + runtime.KeepAlive(value) + return bool(ret) +} + +func (h *dymHttpFilterHandle) SetSocketOptionInt( + level, name int64, state shared.SocketOptionState, + direction shared.SocketDirection, value int64, +) bool { + return bool(C.envoy_dynamic_module_callback_http_set_socket_option_int( + h.hostPluginPtr, + (C.int64_t)(level), + (C.int64_t)(name), + (C.envoy_dynamic_module_type_socket_option_state)(state), + (C.envoy_dynamic_module_type_socket_direction)(direction), + (C.int64_t)(value), + )) +} + +func (h *dymHttpFilterHandle) SetSocketOptionBytes( + level, name int64, state shared.SocketOptionState, + direction shared.SocketDirection, value []byte, +) bool { + ret := C.envoy_dynamic_module_callback_http_set_socket_option_bytes( + h.hostPluginPtr, + (C.int64_t)(level), + (C.int64_t)(name), + (C.envoy_dynamic_module_type_socket_option_state)(state), + (C.envoy_dynamic_module_type_socket_direction)(direction), + bytesToModuleBuffer(value), + ) + runtime.KeepAlive(value) + return bool(ret) +} + +func (h *dymHttpFilterHandle) GetSocketOptionInt( + level, name int64, state shared.SocketOptionState, + direction shared.SocketDirection, +) (int64, bool) { + var value C.int64_t = 0 + ret := C.envoy_dynamic_module_callback_http_get_socket_option_int( + h.hostPluginPtr, + (C.int64_t)(level), + (C.int64_t)(name), + (C.envoy_dynamic_module_type_socket_option_state)(state), + (C.envoy_dynamic_module_type_socket_direction)(direction), + &value, + ) + if !bool(ret) { + return 0, false + } + return int64(value), true +} + +func (h *dymHttpFilterHandle) GetSocketOptionBytes( + level, name int64, state shared.SocketOptionState, + direction shared.SocketDirection, +) (shared.UnsafeEnvoyBuffer, bool) { + var valueView C.envoy_dynamic_module_type_envoy_buffer + ret := C.envoy_dynamic_module_callback_http_get_socket_option_bytes( + h.hostPluginPtr, + (C.int64_t)(level), + (C.int64_t)(name), + (C.envoy_dynamic_module_type_socket_option_state)(state), + (C.envoy_dynamic_module_type_socket_direction)(direction), + &valueView, + ) + if !bool(ret) || valueView.ptr == nil || valueView.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ret) + } + return envoyBufferToUnsafeEnvoyBuffer(valueView), true +} + +func (h *dymHttpFilterHandle) GetBufferLimit() uint64 { + return uint64(C.envoy_dynamic_module_callback_http_get_buffer_limit( + h.hostPluginPtr, + )) +} + +func (h *dymHttpFilterHandle) SetBufferLimit(limit uint64) { + C.envoy_dynamic_module_callback_http_set_buffer_limit( + h.hostPluginPtr, + (C.uint64_t)(limit), + ) +} + +func (h *dymHttpFilterHandle) GetActiveSpan() shared.Span { + spanPtr := C.envoy_dynamic_module_callback_http_get_active_span( + h.hostPluginPtr, + ) + if spanPtr == nil { + return nil + } + return &dymSpan{ + spanPtr: spanPtr, + hostPluginPtr: h.hostPluginPtr, + filter: h, + } +} + +func (h *dymHttpFilterHandle) GetClusterName() (shared.UnsafeEnvoyBuffer, bool) { + var valueView C.envoy_dynamic_module_type_envoy_buffer + ret := C.envoy_dynamic_module_callback_http_get_cluster_name( + h.hostPluginPtr, + &valueView, + ) + if !bool(ret) || valueView.ptr == nil || valueView.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ret) + } + return envoyBufferToUnsafeEnvoyBuffer(valueView), true +} + +func (h *dymHttpFilterHandle) GetClusterHostCounts(priority uint32) (shared.ClusterHostCounts, bool) { + var total, healthy, degraded C.size_t + ret := C.envoy_dynamic_module_callback_http_get_cluster_host_count( + h.hostPluginPtr, + (C.uint32_t)(priority), + &total, + &healthy, + °raded, + ) + if !bool(ret) { + return shared.ClusterHostCounts{}, false + } + return shared.ClusterHostCounts{ + Total: uint64(total), + Healthy: uint64(healthy), + Degraded: uint64(degraded), + }, true +} + +func (h *dymHttpFilterHandle) SetUpstreamOverrideHost(host string, strict bool) bool { + ret := C.envoy_dynamic_module_callback_http_set_upstream_override_host( + h.hostPluginPtr, + stringToModuleBuffer(host), + (C.bool)(strict), + ) + runtime.KeepAlive(host) + return bool(ret) +} + +func (h *dymHttpFilterHandle) ResetStream(reason shared.HttpFilterStreamResetReason, details string) { + C.envoy_dynamic_module_callback_http_filter_reset_stream( + h.hostPluginPtr, + (C.envoy_dynamic_module_type_http_filter_stream_reset_reason)(reason), + stringToModuleBuffer(details), + ) + runtime.KeepAlive(details) +} + +func (h *dymHttpFilterHandle) SendGoAwayAndClose(graceful bool) { + C.envoy_dynamic_module_callback_http_filter_send_go_away_and_close( + h.hostPluginPtr, + (C.bool)(graceful), + ) +} + +func (h *dymHttpFilterHandle) RecreateStream(headers [][2]string) bool { + if len(headers) == 0 { + return bool(C.envoy_dynamic_module_callback_http_filter_recreate_stream( + h.hostPluginPtr, + nil, + 0, + )) + } + headerViews := headersToModuleHttpHeaderSlice(headers) + ret := C.envoy_dynamic_module_callback_http_filter_recreate_stream( + h.hostPluginPtr, unsafe.SliceData(headerViews), (C.size_t)(len(headerViews)), - bytesToModuleBuffer(body), - (C.bool)(endOfStream), - (C.uint64_t)(timeoutMs), ) - - runtime.KeepAlive(cluster) runtime.KeepAlive(headers) - runtime.KeepAlive(body) runtime.KeepAlive(headerViews) + return bool(ret) +} - goResult := shared.HttpCalloutInitResult(result) - if goResult != shared.HttpCalloutInitSuccess { - return goResult, 0 - } +// dymSpan implements shared.Span by wrapping the active span pointer for the current stream. +// The pointer is owned by Envoy and must not be finished by the module. filter is retained so +// child spans spawned from this span can be tracked on the owning filter handle and safely +// retired when the stream is destroyed. +type dymSpan struct { + spanPtr C.envoy_dynamic_module_type_span_envoy_ptr + hostPluginPtr C.envoy_dynamic_module_type_http_filter_envoy_ptr + filter *dymHttpFilterHandle +} - if h.streamCallbacks == nil { - h.streamCallbacks = make(map[uint64]shared.HttpStreamCallback) - } - h.streamCallbacks[uint64(streamID)] = cb +func (s *dymSpan) SetTag(key, value string) { + C.envoy_dynamic_module_callback_http_span_set_tag( + s.spanPtr, + stringToModuleBuffer(key), + stringToModuleBuffer(value), + ) + runtime.KeepAlive(key) + runtime.KeepAlive(value) +} - return goResult, uint64(streamID) +func (s *dymSpan) SetOperation(operation string) { + C.envoy_dynamic_module_callback_http_span_set_operation( + s.spanPtr, + stringToModuleBuffer(operation), + ) + runtime.KeepAlive(operation) } -func (h *dymHttpFilterHandle) SendHttpStreamData( - streamID uint64, data []byte, endOfStream bool, -) bool { - ret := C.envoy_dynamic_module_callback_http_stream_send_data( - h.hostPluginPtr, - (C.uint64_t)(streamID), - bytesToModuleBuffer(data), - (C.bool)(endOfStream), +func (s *dymSpan) Log(event string) { + C.envoy_dynamic_module_callback_http_span_log( + s.hostPluginPtr, + s.spanPtr, + stringToModuleBuffer(event), ) - runtime.KeepAlive(data) - return bool(ret) + runtime.KeepAlive(event) } -func (h *dymHttpFilterHandle) SendHttpStreamTrailers( - streamID uint64, trailers [][2]string, -) bool { - // Prepare trailers. - trailerViews := headersToModuleHttpHeaderSlice(trailers) - ret := C.envoy_dynamic_module_callback_http_stream_send_trailers( - h.hostPluginPtr, - (C.uint64_t)(streamID), - unsafe.SliceData(trailerViews), - (C.size_t)(len(trailerViews)), +func (s *dymSpan) SetSampled(sampled bool) { + C.envoy_dynamic_module_callback_http_span_set_sampled( + s.spanPtr, + (C.bool)(sampled), ) - runtime.KeepAlive(trailers) - runtime.KeepAlive(trailerViews) - return bool(ret) } -func (h *dymHttpFilterHandle) ResetHttpStream( - streamID uint64, -) { - C.envoy_dynamic_module_callback_http_filter_reset_http_stream( - h.hostPluginPtr, - (C.uint64_t)(streamID), +func (s *dymSpan) GetBaggage(key string) (shared.UnsafeEnvoyBuffer, bool) { + var valueView C.envoy_dynamic_module_type_envoy_buffer + ret := C.envoy_dynamic_module_callback_http_span_get_baggage( + s.spanPtr, + stringToModuleBuffer(key), + &valueView, ) + runtime.KeepAlive(key) + if !bool(ret) || valueView.ptr == nil || valueView.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ret) + } + return envoyBufferToUnsafeEnvoyBuffer(valueView), true } -func (h *dymHttpFilterHandle) SetDownstreamWatermarkCallbacks( - cbs shared.DownstreamWatermarkCallbacks, -) { - h.downstreamWatermarkCallbacks = cbs +func (s *dymSpan) SetBaggage(key, value string) { + C.envoy_dynamic_module_callback_http_span_set_baggage( + s.spanPtr, + stringToModuleBuffer(key), + stringToModuleBuffer(value), + ) + runtime.KeepAlive(key) + runtime.KeepAlive(value) } -func (h *dymHttpFilterHandle) ClearDownstreamWatermarkCallbacks() { - h.downstreamWatermarkCallbacks = nil +func (s *dymSpan) GetTraceID() (shared.UnsafeEnvoyBuffer, bool) { + var valueView C.envoy_dynamic_module_type_envoy_buffer + ret := C.envoy_dynamic_module_callback_http_span_get_trace_id( + s.spanPtr, + &valueView, + ) + if !bool(ret) || valueView.ptr == nil || valueView.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ret) + } + return envoyBufferToUnsafeEnvoyBuffer(valueView), true } -func (h *dymHttpFilterHandle) RecordHistogramValue(id shared.MetricID, - value uint64, tagsValues ...string) shared.MetricsResult { - idUint64 := uint64(id) - // Prepare tag values. - tagValueViews := stringArrayToModuleBufferSlice(tagsValues) +func (s *dymSpan) GetSpanID() (shared.UnsafeEnvoyBuffer, bool) { + var valueView C.envoy_dynamic_module_type_envoy_buffer + ret := C.envoy_dynamic_module_callback_http_span_get_span_id( + s.spanPtr, + &valueView, + ) + if !bool(ret) || valueView.ptr == nil || valueView.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ret) + } + return envoyBufferToUnsafeEnvoyBuffer(valueView), true +} - ret := C.envoy_dynamic_module_callback_http_filter_record_histogram_value( - h.hostPluginPtr, - (C.size_t)(idUint64), - unsafe.SliceData(tagValueViews), - (C.size_t)(len(tagValueViews)), - (C.uint64_t)(value), +func (s *dymSpan) SpawnChild(operationName string) shared.ChildSpan { + childPtr := C.envoy_dynamic_module_callback_http_span_spawn_child( + s.hostPluginPtr, + s.spanPtr, + stringToModuleBuffer(operationName), ) + runtime.KeepAlive(operationName) + if childPtr == nil { + return nil + } + child := &dymChildSpan{ + childPtr: childPtr, + hostPluginPtr: s.hostPluginPtr, + filter: s.filter, + } + // trackChildSpan installs the GC finalizer (so a forgotten Finish is recovered) AND + // records the child on the filter so we can safely retire it if the stream is destroyed + // before the module finishes the span. + if s.filter != nil { + s.filter.trackChildSpan(child) + } else { + runtime.SetFinalizer(child, func(c *dymChildSpan) { c.Finish() }) + } + return child +} - runtime.KeepAlive(tagsValues) - runtime.KeepAlive(tagValueViews) - return shared.MetricsResult(ret) +// dymChildSpan implements shared.ChildSpan. The module owns the underlying span and must call +// Finish exactly once. Finish is also installed as a finalizer to avoid leaks if the module +// forgets. filter, when non-nil, is the owning filter handle: it tracks unfinished children +// so they can be marked finished (without invoking Envoy) when the stream is destroyed. +type dymChildSpan struct { + childPtr C.envoy_dynamic_module_type_child_span_module_ptr + hostPluginPtr C.envoy_dynamic_module_type_http_filter_envoy_ptr + filter *dymHttpFilterHandle + finishedMu sync.Mutex + finished bool } -func (h *dymHttpFilterHandle) SetGaugeValue(id shared.MetricID, - value uint64, tagsValues ...string) shared.MetricsResult { - idUint64 := uint64(id) - // Prepare tag values. - tagValueViews := stringArrayToModuleBufferSlice(tagsValues) +// asSpanPtr returns the child span as the generic span pointer accepted by the span_* callbacks. +// In the ABI both pointer types are void*, and Envoy resolves the right type via dynamic_cast. +func (c *dymChildSpan) asSpanPtr() C.envoy_dynamic_module_type_span_envoy_ptr { + return C.envoy_dynamic_module_type_span_envoy_ptr(c.childPtr) +} - ret := C.envoy_dynamic_module_callback_http_filter_set_gauge( - h.hostPluginPtr, - (C.size_t)(idUint64), - unsafe.SliceData(tagValueViews), - (C.size_t)(len(tagValueViews)), - (C.uint64_t)(value), +func (c *dymChildSpan) SetTag(key, value string) { + C.envoy_dynamic_module_callback_http_span_set_tag( + c.asSpanPtr(), + stringToModuleBuffer(key), + stringToModuleBuffer(value), ) + runtime.KeepAlive(key) + runtime.KeepAlive(value) +} - runtime.KeepAlive(tagsValues) - runtime.KeepAlive(tagValueViews) - return shared.MetricsResult(ret) +func (c *dymChildSpan) SetOperation(operation string) { + C.envoy_dynamic_module_callback_http_span_set_operation( + c.asSpanPtr(), + stringToModuleBuffer(operation), + ) + runtime.KeepAlive(operation) } -func (h *dymHttpFilterHandle) IncrementGaugeValue(id shared.MetricID, - value uint64, tagsValues ...string) shared.MetricsResult { - // Prepare tag values. - tagValueViews := stringArrayToModuleBufferSlice(tagsValues) - ret := C.envoy_dynamic_module_callback_http_filter_increment_gauge( - h.hostPluginPtr, - (C.size_t)(uint64(id)), - unsafe.SliceData(tagValueViews), - (C.size_t)(len(tagValueViews)), - (C.uint64_t)(value), +func (c *dymChildSpan) Log(event string) { + C.envoy_dynamic_module_callback_http_span_log( + c.hostPluginPtr, + c.asSpanPtr(), + stringToModuleBuffer(event), ) - runtime.KeepAlive(tagsValues) - runtime.KeepAlive(tagValueViews) - return shared.MetricsResult(ret) + runtime.KeepAlive(event) } -func (h *dymHttpFilterHandle) DecrementGaugeValue(id shared.MetricID, - value uint64, tagsValues ...string) shared.MetricsResult { - // Prepare tag values. - tagValueViews := stringArrayToModuleBufferSlice(tagsValues) - ret := C.envoy_dynamic_module_callback_http_filter_decrement_gauge( - h.hostPluginPtr, - (C.size_t)(uint64(id)), - unsafe.SliceData(tagValueViews), - (C.size_t)(len(tagValueViews)), - (C.uint64_t)(value), +func (c *dymChildSpan) SetSampled(sampled bool) { + C.envoy_dynamic_module_callback_http_span_set_sampled( + c.asSpanPtr(), + (C.bool)(sampled), ) - runtime.KeepAlive(tagsValues) - runtime.KeepAlive(tagValueViews) - return shared.MetricsResult(ret) } -func (h *dymHttpFilterHandle) IncrementCounterValue(id shared.MetricID, - value uint64, tagsValues ...string) shared.MetricsResult { - // Prepare tag values. - tagValueViews := stringArrayToModuleBufferSlice(tagsValues) - ret := C.envoy_dynamic_module_callback_http_filter_increment_counter( - h.hostPluginPtr, - (C.size_t)(uint64(id)), - unsafe.SliceData(tagValueViews), - (C.size_t)(len(tagValueViews)), - (C.uint64_t)(value), +func (c *dymChildSpan) SetBaggage(key, value string) { + C.envoy_dynamic_module_callback_http_span_set_baggage( + c.asSpanPtr(), + stringToModuleBuffer(key), + stringToModuleBuffer(value), ) - runtime.KeepAlive(tagsValues) - runtime.KeepAlive(tagValueViews) - return shared.MetricsResult(ret) + runtime.KeepAlive(key) + runtime.KeepAlive(value) +} + +func (c *dymChildSpan) SpawnChild(operationName string) shared.ChildSpan { + childPtr := C.envoy_dynamic_module_callback_http_span_spawn_child( + c.hostPluginPtr, + c.asSpanPtr(), + stringToModuleBuffer(operationName), + ) + runtime.KeepAlive(operationName) + if childPtr == nil { + return nil + } + child := &dymChildSpan{ + childPtr: childPtr, + hostPluginPtr: c.hostPluginPtr, + filter: c.filter, + } + if c.filter != nil { + c.filter.trackChildSpan(child) + } else { + runtime.SetFinalizer(child, func(c *dymChildSpan) { c.Finish() }) + } + return child +} + +func (c *dymChildSpan) Finish() { + c.finishedMu.Lock() + if c.finished { + c.finishedMu.Unlock() + return + } + c.finished = true + c.finishedMu.Unlock() + // Drop from the filter's tracking set BEFORE dispatching so a concurrent stream destroy + // won't double-process this span. + if c.filter != nil { + c.filter.untrackChildSpan(c) + } + C.envoy_dynamic_module_callback_http_child_span_finish(c.childPtr) + // Clear the finalizer so the GC anchor is released; harmless if no finalizer was set. + runtime.SetFinalizer(c, nil) } func newDymStreamPluginHandle( @@ -1597,7 +1701,10 @@ func newDymStreamPluginHandle( } type dymConfigHandle struct { - hostConfigPtr C.envoy_dynamic_module_type_http_filter_config_envoy_ptr + hostConfigPtr C.envoy_dynamic_module_type_http_filter_config_envoy_ptr + // Config-context callouts can complete on a different thread than the one that started + // them, so callout/stream maps need explicit synchronization. + calloutMu sync.Mutex calloutCallbacks map[uint64]shared.HttpCalloutCallback streamCallbacks map[uint64]shared.HttpStreamCallback scheduler *dymScheduler @@ -1709,10 +1816,12 @@ func (h *dymConfigHandle) HttpCallout( return goResult, 0 } + h.calloutMu.Lock() if h.calloutCallbacks == nil { h.calloutCallbacks = make(map[uint64]shared.HttpCalloutCallback) } h.calloutCallbacks[uint64(calloutID)] = cb + h.calloutMu.Unlock() return goResult, uint64(calloutID) } @@ -1744,10 +1853,12 @@ func (h *dymConfigHandle) StartHttpStream( return goResult, 0 } + h.calloutMu.Lock() if h.streamCallbacks == nil { h.streamCallbacks = make(map[uint64]shared.HttpStreamCallback) } h.streamCallbacks[uint64(streamID)] = cb + h.calloutMu.Unlock() return goResult, uint64(streamID) } @@ -1797,13 +1908,15 @@ func (h *dymConfigHandle) GetScheduler() shared.Scheduler { taskID, ) }, + func(p unsafe.Pointer) { + C.envoy_dynamic_module_callback_http_filter_config_scheduler_delete( + (C.envoy_dynamic_module_type_http_filter_config_scheduler_module_ptr)(p), + ) + }, ) - runtime.SetFinalizer(h.scheduler, func(s *dymScheduler) { - C.envoy_dynamic_module_callback_http_filter_config_scheduler_delete( - (C.envoy_dynamic_module_type_http_filter_config_scheduler_module_ptr)(s.schedulerPtr), - ) - }) + // See dymHttpFilterHandle.GetScheduler for why the finalizer is a fallback. + runtime.SetFinalizer(h.scheduler, func(s *dymScheduler) { s.close() }) } return h.scheduler } @@ -1841,12 +1954,16 @@ func envoy_dynamic_module_on_http_filter_config_new( config C.envoy_dynamic_module_type_envoy_buffer, ) C.envoy_dynamic_module_type_http_filter_config_module_ptr { nameString := envoyBufferToStringUnsafe(name) - configBytes := envoyBufferToBytesUnsafe(config) + configBytes := envoyBufferToBytesCopy(config) configHandle := &dymConfigHandle{hostConfigPtr: hostConfigPtr} factory, err := sdk.NewHttpFilterFactory(configHandle, nameString, configBytes) - if err != nil || factory == nil { - configHandle.Log(shared.LogLevelWarn, "Failed to load configuration: %v", err) + if err != nil { + configHandle.Log(shared.LogLevelWarn, "Failed to load HTTP filter configuration for %q: %v", nameString, err) + return nil + } + if factory == nil { + configHandle.Log(shared.LogLevelWarn, "Failed to load HTTP filter configuration for %q: factory returned nil", nameString) return nil } configPtr := configManager.record(&httpFilterConfigWrapper{pluginFactory: factory, configHandle: configHandle}) @@ -1861,7 +1978,12 @@ func envoy_dynamic_module_on_http_filter_config_destroy( if factoryWrapper == nil { return } - factoryWrapper.configHandle.scheduler = nil + if factoryWrapper.configHandle.scheduler != nil { + // See bootstrap config destroy for why we close synchronously instead of + // dropping the reference and waiting for the GC finalizer. + factoryWrapper.configHandle.scheduler.close() + factoryWrapper.configHandle.scheduler = nil + } factoryWrapper.pluginFactory.OnDestroy() configManager.remove(unsafe.Pointer(configPtr)) } @@ -1872,7 +1994,7 @@ func envoy_dynamic_module_on_http_filter_per_route_config_new( config C.envoy_dynamic_module_type_envoy_buffer, ) C.envoy_dynamic_module_type_http_filter_per_route_config_module_ptr { nameStr := envoyBufferToStringUnsafe(name) - configBytes := envoyBufferToBytesUnsafe(config) + configBytes := envoyBufferToBytesCopy(config) // The route config handle only make logging available. configHandle := &dymRouteConfigHandle{} @@ -1880,13 +2002,18 @@ func envoy_dynamic_module_on_http_filter_per_route_config_new( configFactory := sdk.GetHttpFilterConfigFactory(nameStr) if configFactory == nil { configHandle.Log(shared.LogLevelWarn, - "Failed to load configuration: no factory for %s", nameStr) + "Failed to load HTTP per-route configuration for %q: no factory registered", nameStr) return nil } parsedConfig, err := configFactory.CreatePerRoute(configBytes) - if err != nil || parsedConfig == nil { + if err != nil { + configHandle.Log(shared.LogLevelWarn, + "Failed to load HTTP per-route configuration for %q: %v", nameStr, err) + return nil + } + if parsedConfig == nil { configHandle.Log(shared.LogLevelWarn, - "Failed to load per-route configuration: %v", err) + "Failed to load HTTP per-route configuration for %q: factory returned nil", nameStr) return nil } @@ -1924,10 +2051,15 @@ func envoy_dynamic_module_on_http_filter_destroy( pluginPtr C.envoy_dynamic_module_type_http_filter_module_ptr, ) { pluginWrapper := pluginManager.unwrap(unsafe.Pointer(pluginPtr)) - if pluginWrapper == nil || pluginWrapper.streamDestoried { + if pluginWrapper == nil || pluginWrapper.streamDestroyed { return } - pluginWrapper.streamDestoried = true + // Mark destroyed FIRST so a concurrent SpawnChild seen via trackChildSpan won't register + // a new finalizer that would later fire against a freed span pointer. + pluginWrapper.childSpansMu.Lock() + pluginWrapper.streamDestroyed = true + pluginWrapper.childSpansMu.Unlock() + pluginWrapper.retireChildSpansOnDestroy() if pluginWrapper.plugin != nil { pluginWrapper.plugin.OnDestroy() } @@ -2028,9 +2160,14 @@ func envoy_dynamic_module_on_http_filter_stream_complete( return } pluginWrapper.streamCompleted = true - pluginWrapper.clearData() - pluginWrapper.scheduler = nil + if pluginWrapper.scheduler != nil { + // See bootstrap config destroy for why we close synchronously. + pluginWrapper.scheduler.close() + pluginWrapper.scheduler = nil + } pluginWrapper.plugin.OnStreamComplete() + // data is held in Go memory and is freed when the wrapper is GC'd after pluginManager + // removes it; no explicit teardown is needed. } //export envoy_dynamic_module_on_http_filter_scheduled @@ -2066,9 +2203,13 @@ func envoy_dynamic_module_on_http_filter_http_callout_done( resultHeaders := envoyHttpHeaderSliceToUnsafeHeaderSlice(unsafe.Slice(headers, int(headersSize))) resultChunks := envoyBufferSliceToUnsafeEnvoyBufferSlice(unsafe.Slice(chunks, int(chunksSize))) + pluginWrapper.calloutMu.Lock() cb := pluginWrapper.calloutCallbacks[uint64(calloutID)] if cb != nil { delete(pluginWrapper.calloutCallbacks, uint64(calloutID)) + } + pluginWrapper.calloutMu.Unlock() + if cb != nil { cb.OnHttpCalloutDone(uint64(calloutID), shared.HttpCalloutResult(result), resultHeaders, @@ -2094,7 +2235,9 @@ func envoy_dynamic_module_on_http_filter_http_stream_headers( // Prepare headers. resultHeaders := envoyHttpHeaderSliceToUnsafeHeaderSlice(unsafe.Slice(headers, int(headersSize))) + pluginWrapper.calloutMu.Lock() cb := pluginWrapper.streamCallbacks[uint64(streamID)] + pluginWrapper.calloutMu.Unlock() if cb != nil { cb.OnHttpStreamHeaders(uint64(streamID), resultHeaders, bool(endOfStream)) } @@ -2117,7 +2260,9 @@ func envoy_dynamic_module_on_http_filter_http_stream_data( // Prepare data. resultData := envoyBufferSliceToUnsafeEnvoyBufferSlice(unsafe.Slice(chunks, int(chunksSize))) + pluginWrapper.calloutMu.Lock() cb := pluginWrapper.streamCallbacks[uint64(streamID)] + pluginWrapper.calloutMu.Unlock() if cb != nil { cb.OnHttpStreamData(uint64(streamID), resultData, bool(endOfStream)) } @@ -2139,7 +2284,9 @@ func envoy_dynamic_module_on_http_filter_http_stream_trailers( // Prepare trailers. resultTrailers := envoyHttpHeaderSliceToUnsafeHeaderSlice(unsafe.Slice(trailers, int(trailersSize))) + pluginWrapper.calloutMu.Lock() cb := pluginWrapper.streamCallbacks[uint64(streamID)] + pluginWrapper.calloutMu.Unlock() if cb != nil { cb.OnHttpStreamTrailers(uint64(streamID), resultTrailers) } @@ -2156,9 +2303,13 @@ func envoy_dynamic_module_on_http_filter_http_stream_complete( return } + pluginWrapper.calloutMu.Lock() cb := pluginWrapper.streamCallbacks[uint64(streamID)] if cb != nil { delete(pluginWrapper.streamCallbacks, uint64(streamID)) + } + pluginWrapper.calloutMu.Unlock() + if cb != nil { cb.OnHttpStreamComplete(uint64(streamID)) } } @@ -2175,9 +2326,13 @@ func envoy_dynamic_module_on_http_filter_http_stream_reset( return } + pluginWrapper.calloutMu.Lock() cb := pluginWrapper.streamCallbacks[uint64(streamID)] if cb != nil { delete(pluginWrapper.streamCallbacks, uint64(streamID)) + } + pluginWrapper.calloutMu.Unlock() + if cb != nil { cb.OnHttpStreamReset(uint64(streamID), shared.HttpStreamResetReason(reason)) } } @@ -2214,20 +2369,18 @@ func envoy_dynamic_module_on_http_filter_downstream_below_write_buffer_low_water //export envoy_dynamic_module_on_http_filter_local_reply func envoy_dynamic_module_on_http_filter_local_reply( - filter_envoy_ptr C.envoy_dynamic_module_type_http_filter_envoy_ptr, + _ C.envoy_dynamic_module_type_http_filter_envoy_ptr, filter_module_ptr C.envoy_dynamic_module_type_http_filter_module_ptr, response_code C.uint32_t, details C.envoy_dynamic_module_type_envoy_buffer, reset_imminent C.bool, ) C.envoy_dynamic_module_type_on_http_filter_local_reply_status { - _ = filter_envoy_ptr pluginWrapper := pluginManager.unwrap(unsafe.Pointer(filter_module_ptr)) if pluginWrapper == nil || pluginWrapper.plugin == nil { return C.envoy_dynamic_module_type_on_http_filter_local_reply_status( shared.LocalReplyStatusContinue, ) } - return C.envoy_dynamic_module_type_on_http_filter_local_reply_status( pluginWrapper.plugin.OnLocalReply( uint32(response_code), @@ -2257,9 +2410,13 @@ func envoy_dynamic_module_on_http_filter_config_http_callout_done( resultHeaders := envoyHttpHeaderSliceToUnsafeHeaderSlice(unsafe.Slice(headers, int(headersSize))) resultChunks := envoyBufferSliceToUnsafeEnvoyBufferSlice(unsafe.Slice(chunks, int(chunksSize))) + ch.calloutMu.Lock() cb := ch.calloutCallbacks[uint64(calloutID)] if cb != nil { delete(ch.calloutCallbacks, uint64(calloutID)) + } + ch.calloutMu.Unlock() + if cb != nil { cb.OnHttpCalloutDone(uint64(calloutID), shared.HttpCalloutResult(result), resultHeaders, resultChunks) } } @@ -2281,7 +2438,9 @@ func envoy_dynamic_module_on_http_filter_config_http_stream_headers( resultHeaders := envoyHttpHeaderSliceToUnsafeHeaderSlice(unsafe.Slice(headers, int(headersSize))) + ch.calloutMu.Lock() cb := ch.streamCallbacks[uint64(streamID)] + ch.calloutMu.Unlock() if cb != nil { cb.OnHttpStreamHeaders(uint64(streamID), resultHeaders, bool(endOfStream)) } @@ -2304,7 +2463,9 @@ func envoy_dynamic_module_on_http_filter_config_http_stream_data( resultData := envoyBufferSliceToUnsafeEnvoyBufferSlice(unsafe.Slice(chunks, int(chunksSize))) + ch.calloutMu.Lock() cb := ch.streamCallbacks[uint64(streamID)] + ch.calloutMu.Unlock() if cb != nil { cb.OnHttpStreamData(uint64(streamID), resultData, bool(endOfStream)) } @@ -2326,7 +2487,9 @@ func envoy_dynamic_module_on_http_filter_config_http_stream_trailers( resultTrailers := envoyHttpHeaderSliceToUnsafeHeaderSlice(unsafe.Slice(trailers, int(trailersSize))) + ch.calloutMu.Lock() cb := ch.streamCallbacks[uint64(streamID)] + ch.calloutMu.Unlock() if cb != nil { cb.OnHttpStreamTrailers(uint64(streamID), resultTrailers) } @@ -2344,9 +2507,13 @@ func envoy_dynamic_module_on_http_filter_config_http_stream_complete( } ch := configWrapper.configHandle + ch.calloutMu.Lock() cb := ch.streamCallbacks[uint64(streamID)] if cb != nil { delete(ch.streamCallbacks, uint64(streamID)) + } + ch.calloutMu.Unlock() + if cb != nil { cb.OnHttpStreamComplete(uint64(streamID)) } } @@ -2364,9 +2531,13 @@ func envoy_dynamic_module_on_http_filter_config_http_stream_reset( } ch := configWrapper.configHandle + ch.calloutMu.Lock() cb := ch.streamCallbacks[uint64(streamID)] if cb != nil { delete(ch.streamCallbacks, uint64(streamID)) + } + ch.calloutMu.Unlock() + if cb != nil { cb.OnHttpStreamReset(uint64(streamID), shared.HttpStreamResetReason(reason)) } } diff --git a/source/extensions/dynamic_modules/sdk/go/abi/listener.go b/source/extensions/dynamic_modules/sdk/go/abi/listener.go new file mode 100644 index 0000000000000..2723fe154cf54 --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/abi/listener.go @@ -0,0 +1,744 @@ +package abi + +/* +#cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup +#cgo linux LDFLAGS: -Wl,--unresolved-symbols=ignore-all +#include +#include +#include +#include +#include "../../../abi/abi.h" +*/ +import "C" +import ( + "runtime" + "sync" + "unsafe" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +// listenerFilterConfigWrapper holds the module-side state for a listener filter configuration. +type listenerFilterConfigWrapper struct { + factory shared.ListenerFilterFactory + configHandle *dymListenerConfigHandle +} + +var listenerConfigManager = newManager[listenerFilterConfigWrapper]() +var listenerFilterManager = newManager[dymListenerFilterHandle]() + +// dymListenerConfigHandle implements shared.ListenerFilterConfigHandle. +type dymListenerConfigHandle struct { + hostConfigPtr C.envoy_dynamic_module_type_listener_filter_config_envoy_ptr + scheduler *dymScheduler +} + +func (h *dymListenerConfigHandle) DefineCounter(name string) (shared.MetricID, shared.MetricsResult) { + var id C.size_t + ret := C.envoy_dynamic_module_callback_listener_filter_config_define_counter( + h.hostConfigPtr, + stringToModuleBuffer(name), + &id, + ) + runtime.KeepAlive(name) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymListenerConfigHandle) DefineGauge(name string) (shared.MetricID, shared.MetricsResult) { + var id C.size_t + ret := C.envoy_dynamic_module_callback_listener_filter_config_define_gauge( + h.hostConfigPtr, + stringToModuleBuffer(name), + &id, + ) + runtime.KeepAlive(name) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymListenerConfigHandle) DefineHistogram(name string) (shared.MetricID, shared.MetricsResult) { + var id C.size_t + ret := C.envoy_dynamic_module_callback_listener_filter_config_define_histogram( + h.hostConfigPtr, + stringToModuleBuffer(name), + &id, + ) + runtime.KeepAlive(name) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymListenerConfigHandle) GetScheduler() shared.Scheduler { + if h.scheduler == nil { + schedulerPtr := C.envoy_dynamic_module_callback_listener_filter_config_scheduler_new( + h.hostConfigPtr) + h.scheduler = newDymScheduler( + unsafe.Pointer(schedulerPtr), + func(p unsafe.Pointer, taskID C.uint64_t) { + C.envoy_dynamic_module_callback_listener_filter_config_scheduler_commit( + (C.envoy_dynamic_module_type_listener_filter_config_scheduler_module_ptr)(p), + taskID, + ) + }, + func(p unsafe.Pointer) { + C.envoy_dynamic_module_callback_listener_filter_config_scheduler_delete( + (C.envoy_dynamic_module_type_listener_filter_config_scheduler_module_ptr)(p), + ) + }, + ) + // Finalizer is a fallback; the destroy hook should call close() synchronously. + runtime.SetFinalizer(h.scheduler, func(s *dymScheduler) { s.close() }) + } + return h.scheduler +} + +// dymListenerFilterHandle implements shared.ListenerFilterHandle. +type dymListenerFilterHandle struct { + hostFilterPtr C.envoy_dynamic_module_type_listener_filter_envoy_ptr + + plugin shared.ListenerFilter + scheduler *dymScheduler + destroyed bool + + calloutCallbacks map[uint64]shared.HttpCalloutCallback + calloutMu sync.Mutex +} + +// ---- buffer access ---- + +func (h *dymListenerFilterHandle) GetBufferChunk() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ret := C.envoy_dynamic_module_callback_listener_filter_get_buffer_chunk(h.hostFilterPtr, &buf) + if !bool(ret) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ret) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func (h *dymListenerFilterHandle) DrainBuffer(length uint64) bool { + return bool(C.envoy_dynamic_module_callback_listener_filter_drain_buffer( + h.hostFilterPtr, C.size_t(length))) +} + +// ---- protocol detection setters ---- + +func (h *dymListenerFilterHandle) SetDetectedTransportProtocol(protocol string) { + C.envoy_dynamic_module_callback_listener_filter_set_detected_transport_protocol( + h.hostFilterPtr, stringToModuleBuffer(protocol)) + runtime.KeepAlive(protocol) +} + +func (h *dymListenerFilterHandle) SetRequestedServerName(name string) { + C.envoy_dynamic_module_callback_listener_filter_set_requested_server_name( + h.hostFilterPtr, stringToModuleBuffer(name)) + runtime.KeepAlive(name) +} + +func (h *dymListenerFilterHandle) SetRequestedApplicationProtocols(protocols []string) { + if len(protocols) == 0 { + C.envoy_dynamic_module_callback_listener_filter_set_requested_application_protocols( + h.hostFilterPtr, nil, 0) + return + } + views := stringArrayToModuleBufferSlice(protocols) + C.envoy_dynamic_module_callback_listener_filter_set_requested_application_protocols( + h.hostFilterPtr, unsafe.SliceData(views), C.size_t(len(views))) + runtime.KeepAlive(protocols) + runtime.KeepAlive(views) +} + +func (h *dymListenerFilterHandle) SetJA3Hash(hash string) { + C.envoy_dynamic_module_callback_listener_filter_set_ja3_hash( + h.hostFilterPtr, stringToModuleBuffer(hash)) + runtime.KeepAlive(hash) +} + +func (h *dymListenerFilterHandle) SetJA4Hash(hash string) { + C.envoy_dynamic_module_callback_listener_filter_set_ja4_hash( + h.hostFilterPtr, stringToModuleBuffer(hash)) + runtime.KeepAlive(hash) +} + +// ---- protocol detection getters / SSL info ---- + +func (h *dymListenerFilterHandle) GetRequestedServerName() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + if !bool(C.envoy_dynamic_module_callback_listener_filter_get_requested_server_name(h.hostFilterPtr, &buf)) { + return shared.UnsafeEnvoyBuffer{}, false + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func (h *dymListenerFilterHandle) GetDetectedTransportProtocol() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + if !bool(C.envoy_dynamic_module_callback_listener_filter_get_detected_transport_protocol(h.hostFilterPtr, &buf)) { + return shared.UnsafeEnvoyBuffer{}, false + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func (h *dymListenerFilterHandle) GetRequestedApplicationProtocols() []shared.UnsafeEnvoyBuffer { + size := C.envoy_dynamic_module_callback_listener_filter_get_requested_application_protocols_size(h.hostFilterPtr) + if size == 0 { + return nil + } + bufs := make([]C.envoy_dynamic_module_type_envoy_buffer, int(size)) + if !bool(C.envoy_dynamic_module_callback_listener_filter_get_requested_application_protocols( + h.hostFilterPtr, unsafe.SliceData(bufs))) { + return nil + } + out := envoyBufferSliceToUnsafeEnvoyBufferSlice(bufs) + runtime.KeepAlive(bufs) + return out +} + +func (h *dymListenerFilterHandle) GetJA3Hash() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + if !bool(C.envoy_dynamic_module_callback_listener_filter_get_ja3_hash(h.hostFilterPtr, &buf)) { + return shared.UnsafeEnvoyBuffer{}, false + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func (h *dymListenerFilterHandle) GetJA4Hash() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + if !bool(C.envoy_dynamic_module_callback_listener_filter_get_ja4_hash(h.hostFilterPtr, &buf)) { + return shared.UnsafeEnvoyBuffer{}, false + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func (h *dymListenerFilterHandle) IsSSL() bool { + return bool(C.envoy_dynamic_module_callback_listener_filter_is_ssl(h.hostFilterPtr)) +} + +func (h *dymListenerFilterHandle) GetSSLURISans() []shared.UnsafeEnvoyBuffer { + size := C.envoy_dynamic_module_callback_listener_filter_get_ssl_uri_sans_size(h.hostFilterPtr) + if size == 0 { + return nil + } + bufs := make([]C.envoy_dynamic_module_type_envoy_buffer, int(size)) + if !bool(C.envoy_dynamic_module_callback_listener_filter_get_ssl_uri_sans(h.hostFilterPtr, unsafe.SliceData(bufs))) { + return nil + } + out := envoyBufferSliceToUnsafeEnvoyBufferSlice(bufs) + runtime.KeepAlive(bufs) + return out +} + +func (h *dymListenerFilterHandle) GetSSLDNSSans() []shared.UnsafeEnvoyBuffer { + size := C.envoy_dynamic_module_callback_listener_filter_get_ssl_dns_sans_size(h.hostFilterPtr) + if size == 0 { + return nil + } + bufs := make([]C.envoy_dynamic_module_type_envoy_buffer, int(size)) + if !bool(C.envoy_dynamic_module_callback_listener_filter_get_ssl_dns_sans(h.hostFilterPtr, unsafe.SliceData(bufs))) { + return nil + } + out := envoyBufferSliceToUnsafeEnvoyBufferSlice(bufs) + runtime.KeepAlive(bufs) + return out +} + +func (h *dymListenerFilterHandle) GetSSLSubject() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + if !bool(C.envoy_dynamic_module_callback_listener_filter_get_ssl_subject(h.hostFilterPtr, &buf)) { + return shared.UnsafeEnvoyBuffer{}, false + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +// ---- addresses ---- + +func listenerAddressOrEmpty(buf C.envoy_dynamic_module_type_envoy_buffer, port C.uint32_t, ok C.bool) (shared.UnsafeEnvoyBuffer, uint32, bool) { + if !bool(ok) { + return shared.UnsafeEnvoyBuffer{}, 0, false + } + return envoyBufferToUnsafeEnvoyBuffer(buf), uint32(port), true +} + +func (h *dymListenerFilterHandle) GetRemoteAddress() (shared.UnsafeEnvoyBuffer, uint32, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var port C.uint32_t + ok := C.envoy_dynamic_module_callback_listener_filter_get_remote_address(h.hostFilterPtr, &buf, &port) + return listenerAddressOrEmpty(buf, port, ok) +} + +func (h *dymListenerFilterHandle) GetDirectRemoteAddress() (shared.UnsafeEnvoyBuffer, uint32, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var port C.uint32_t + ok := C.envoy_dynamic_module_callback_listener_filter_get_direct_remote_address(h.hostFilterPtr, &buf, &port) + return listenerAddressOrEmpty(buf, port, ok) +} + +func (h *dymListenerFilterHandle) GetLocalAddress() (shared.UnsafeEnvoyBuffer, uint32, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var port C.uint32_t + ok := C.envoy_dynamic_module_callback_listener_filter_get_local_address(h.hostFilterPtr, &buf, &port) + return listenerAddressOrEmpty(buf, port, ok) +} + +func (h *dymListenerFilterHandle) GetDirectLocalAddress() (shared.UnsafeEnvoyBuffer, uint32, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var port C.uint32_t + ok := C.envoy_dynamic_module_callback_listener_filter_get_direct_local_address(h.hostFilterPtr, &buf, &port) + return listenerAddressOrEmpty(buf, port, ok) +} + +func (h *dymListenerFilterHandle) GetOriginalDst() (shared.UnsafeEnvoyBuffer, uint32, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var port C.uint32_t + ok := C.envoy_dynamic_module_callback_listener_filter_get_original_dst(h.hostFilterPtr, &buf, &port) + return listenerAddressOrEmpty(buf, port, ok) +} + +func (h *dymListenerFilterHandle) GetAddressType() shared.AddressType { + return shared.AddressType(C.envoy_dynamic_module_callback_listener_filter_get_address_type(h.hostFilterPtr)) +} + +func (h *dymListenerFilterHandle) IsLocalAddressRestored() bool { + return bool(C.envoy_dynamic_module_callback_listener_filter_is_local_address_restored(h.hostFilterPtr)) +} + +func (h *dymListenerFilterHandle) SetRemoteAddress(address string, port uint32, isIPv6 bool) bool { + ret := C.envoy_dynamic_module_callback_listener_filter_set_remote_address( + h.hostFilterPtr, stringToModuleBuffer(address), C.uint32_t(port), C.bool(isIPv6)) + runtime.KeepAlive(address) + return bool(ret) +} + +func (h *dymListenerFilterHandle) RestoreLocalAddress(address string, port uint32, isIPv6 bool) bool { + ret := C.envoy_dynamic_module_callback_listener_filter_restore_local_address( + h.hostFilterPtr, stringToModuleBuffer(address), C.uint32_t(port), C.bool(isIPv6)) + runtime.KeepAlive(address) + return bool(ret) +} + +// ---- filter chain control ---- + +func (h *dymListenerFilterHandle) ContinueFilterChain(success bool) { + C.envoy_dynamic_module_callback_listener_filter_continue_filter_chain(h.hostFilterPtr, C.bool(success)) +} + +func (h *dymListenerFilterHandle) UseOriginalDst(useOriginalDst bool) { + C.envoy_dynamic_module_callback_listener_filter_use_original_dst(h.hostFilterPtr, C.bool(useOriginalDst)) +} + +func (h *dymListenerFilterHandle) CloseSocket(details string) { + C.envoy_dynamic_module_callback_listener_filter_close_socket(h.hostFilterPtr, stringToModuleBuffer(details)) + runtime.KeepAlive(details) +} + +func (h *dymListenerFilterHandle) WriteToSocket(data []byte) int64 { + ret := C.envoy_dynamic_module_callback_listener_filter_write_to_socket( + h.hostFilterPtr, bytesToModuleBuffer(data)) + runtime.KeepAlive(data) + return int64(ret) +} + +// ---- socket fd / options ---- + +func (h *dymListenerFilterHandle) GetSocketFD() int64 { + return int64(C.envoy_dynamic_module_callback_listener_filter_get_socket_fd(h.hostFilterPtr)) +} + +func (h *dymListenerFilterHandle) SetSocketOptionInt(level, name, value int64) bool { + return bool(C.envoy_dynamic_module_callback_listener_filter_set_socket_option_int( + h.hostFilterPtr, C.int64_t(level), C.int64_t(name), C.int64_t(value))) +} + +func (h *dymListenerFilterHandle) SetSocketOptionBytes(level, name int64, value []byte) bool { + ret := C.envoy_dynamic_module_callback_listener_filter_set_socket_option_bytes( + h.hostFilterPtr, C.int64_t(level), C.int64_t(name), bytesToModuleBuffer(value)) + runtime.KeepAlive(value) + return bool(ret) +} + +func (h *dymListenerFilterHandle) GetSocketOptionInt(level, name int64) (int64, bool) { + var v C.int64_t + ret := C.envoy_dynamic_module_callback_listener_filter_get_socket_option_int( + h.hostFilterPtr, C.int64_t(level), C.int64_t(name), &v) + if !bool(ret) { + return 0, false + } + return int64(v), true +} + +func (h *dymListenerFilterHandle) GetSocketOptionBytes(level, name int64, maxSize uint64) ([]byte, bool) { + if maxSize == 0 { + return nil, false + } + buf := make([]byte, int(maxSize)) + var actual C.size_t + ret := C.envoy_dynamic_module_callback_listener_filter_get_socket_option_bytes( + h.hostFilterPtr, C.int64_t(level), C.int64_t(name), + (*C.char)(unsafe.Pointer(unsafe.SliceData(buf))), C.size_t(maxSize), &actual) + if !bool(ret) { + return nil, false + } + if uint64(actual) < uint64(len(buf)) { + buf = buf[:int(actual)] + } + return buf, true +} + +// ---- filter state & dynamic metadata ---- + +func (h *dymListenerFilterHandle) SetFilterState(key, value string) bool { + ret := C.envoy_dynamic_module_callback_listener_filter_set_filter_state( + h.hostFilterPtr, stringToModuleBuffer(key), stringToModuleBuffer(value)) + runtime.KeepAlive(key) + runtime.KeepAlive(value) + return bool(ret) +} + +func (h *dymListenerFilterHandle) GetFilterState(key string) (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ret := C.envoy_dynamic_module_callback_listener_filter_get_filter_state( + h.hostFilterPtr, stringToModuleBuffer(key), &buf) + runtime.KeepAlive(key) + if !bool(ret) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ret) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func (h *dymListenerFilterHandle) SetDynamicMetadataString(metadataNamespace, key, value string) { + C.envoy_dynamic_module_callback_listener_filter_set_dynamic_metadata_string( + h.hostFilterPtr, stringToModuleBuffer(metadataNamespace), + stringToModuleBuffer(key), stringToModuleBuffer(value)) + runtime.KeepAlive(metadataNamespace) + runtime.KeepAlive(key) + runtime.KeepAlive(value) +} + +func (h *dymListenerFilterHandle) GetDynamicMetadataString(metadataNamespace, key string) (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ret := C.envoy_dynamic_module_callback_listener_filter_get_dynamic_metadata_string( + h.hostFilterPtr, stringToModuleBuffer(metadataNamespace), + stringToModuleBuffer(key), &buf) + runtime.KeepAlive(metadataNamespace) + runtime.KeepAlive(key) + if !bool(ret) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ret) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func (h *dymListenerFilterHandle) SetDynamicMetadataNumber(metadataNamespace, key string, value float64) { + C.envoy_dynamic_module_callback_listener_filter_set_dynamic_metadata_number( + h.hostFilterPtr, stringToModuleBuffer(metadataNamespace), + stringToModuleBuffer(key), C.double(value)) + runtime.KeepAlive(metadataNamespace) + runtime.KeepAlive(key) +} + +func (h *dymListenerFilterHandle) GetDynamicMetadataNumber(metadataNamespace, key string) (float64, bool) { + var v C.double + ret := C.envoy_dynamic_module_callback_listener_filter_get_dynamic_metadata_number( + h.hostFilterPtr, stringToModuleBuffer(metadataNamespace), + stringToModuleBuffer(key), &v) + runtime.KeepAlive(metadataNamespace) + runtime.KeepAlive(key) + if !bool(ret) { + return 0, false + } + return float64(v), true +} + +// ---- stream info ---- + +func (h *dymListenerFilterHandle) SetDownstreamTransportFailureReason(reason string) { + C.envoy_dynamic_module_callback_listener_filter_set_downstream_transport_failure_reason( + h.hostFilterPtr, stringToModuleBuffer(reason)) + runtime.KeepAlive(reason) +} + +func (h *dymListenerFilterHandle) GetConnectionStartTimeMs() uint64 { + return uint64(C.envoy_dynamic_module_callback_listener_filter_get_connection_start_time_ms(h.hostFilterPtr)) +} + +func (h *dymListenerFilterHandle) MaxReadBytes() uint64 { + return uint64(C.envoy_dynamic_module_callback_listener_filter_max_read_bytes(h.hostFilterPtr)) +} + +// ---- HTTP callout ---- + +func (h *dymListenerFilterHandle) HttpCallout( + clusterName string, headers [][2]string, body []byte, timeoutMs uint64, + cb shared.HttpCalloutCallback, +) (shared.HttpCalloutInitResult, uint64) { + headerViews := headersToModuleHttpHeaderSlice(headers) + var calloutID C.uint64_t + + result := C.envoy_dynamic_module_callback_listener_filter_http_callout( + h.hostFilterPtr, + &calloutID, + stringToModuleBuffer(clusterName), + unsafe.SliceData(headerViews), + C.size_t(len(headerViews)), + bytesToModuleBuffer(body), + C.uint64_t(timeoutMs), + ) + runtime.KeepAlive(clusterName) + runtime.KeepAlive(headers) + runtime.KeepAlive(headerViews) + runtime.KeepAlive(body) + + goResult := shared.HttpCalloutInitResult(result) + if goResult != shared.HttpCalloutInitSuccess { + return goResult, 0 + } + h.calloutMu.Lock() + if h.calloutCallbacks == nil { + h.calloutCallbacks = make(map[uint64]shared.HttpCalloutCallback) + } + h.calloutCallbacks[uint64(calloutID)] = cb + h.calloutMu.Unlock() + return goResult, uint64(calloutID) +} + +// ---- metrics ---- + +func (h *dymListenerFilterHandle) IncrementCounter(id shared.MetricID, value uint64) shared.MetricsResult { + return shared.MetricsResult(C.envoy_dynamic_module_callback_listener_filter_increment_counter( + h.hostFilterPtr, C.size_t(uint64(id)), C.uint64_t(value))) +} + +func (h *dymListenerFilterHandle) SetGauge(id shared.MetricID, value uint64) shared.MetricsResult { + return shared.MetricsResult(C.envoy_dynamic_module_callback_listener_filter_set_gauge( + h.hostFilterPtr, C.size_t(uint64(id)), C.uint64_t(value))) +} + +func (h *dymListenerFilterHandle) IncrementGauge(id shared.MetricID, value uint64) shared.MetricsResult { + return shared.MetricsResult(C.envoy_dynamic_module_callback_listener_filter_increment_gauge( + h.hostFilterPtr, C.size_t(uint64(id)), C.uint64_t(value))) +} + +func (h *dymListenerFilterHandle) DecrementGauge(id shared.MetricID, value uint64) shared.MetricsResult { + return shared.MetricsResult(C.envoy_dynamic_module_callback_listener_filter_decrement_gauge( + h.hostFilterPtr, C.size_t(uint64(id)), C.uint64_t(value))) +} + +func (h *dymListenerFilterHandle) RecordHistogramValue(id shared.MetricID, value uint64) shared.MetricsResult { + return shared.MetricsResult(C.envoy_dynamic_module_callback_listener_filter_record_histogram_value( + h.hostFilterPtr, C.size_t(uint64(id)), C.uint64_t(value))) +} + +// ---- scheduling / misc ---- + +func (h *dymListenerFilterHandle) GetScheduler() shared.Scheduler { + if h.scheduler == nil { + schedulerPtr := C.envoy_dynamic_module_callback_listener_filter_scheduler_new(h.hostFilterPtr) + h.scheduler = newDymScheduler( + unsafe.Pointer(schedulerPtr), + func(p unsafe.Pointer, taskID C.uint64_t) { + C.envoy_dynamic_module_callback_listener_filter_scheduler_commit( + (C.envoy_dynamic_module_type_listener_filter_scheduler_module_ptr)(p), + taskID, + ) + }, + func(p unsafe.Pointer) { + C.envoy_dynamic_module_callback_listener_filter_scheduler_delete( + (C.envoy_dynamic_module_type_listener_filter_scheduler_module_ptr)(p), + ) + }, + ) + // Finalizer is a fallback; the destroy hook should call close() synchronously. + runtime.SetFinalizer(h.scheduler, func(s *dymScheduler) { s.close() }) + } + return h.scheduler +} + +func (h *dymListenerFilterHandle) GetWorkerIndex() uint32 { + return uint32(C.envoy_dynamic_module_callback_listener_filter_get_worker_index(h.hostFilterPtr)) +} + +// ============================================================================= +// Event hooks (//export entry points called by Envoy) +// ============================================================================= + +//export envoy_dynamic_module_on_listener_filter_config_new +func envoy_dynamic_module_on_listener_filter_config_new( + hostConfigPtr C.envoy_dynamic_module_type_listener_filter_config_envoy_ptr, + name C.envoy_dynamic_module_type_envoy_buffer, + config C.envoy_dynamic_module_type_envoy_buffer, +) C.envoy_dynamic_module_type_listener_filter_config_module_ptr { + nameStr := envoyBufferToStringUnsafe(name) + configBytes := envoyBufferToBytesCopy(config) + + configHandle := &dymListenerConfigHandle{hostConfigPtr: hostConfigPtr} + configFactory := sdk.GetListenerFilterConfigFactory(nameStr) + if configFactory == nil { + hostLog(shared.LogLevelWarn, "Failed to load listener filter configuration for %q: no factory registered", []any{nameStr}) + return nil + } + factory, err := configFactory.Create(configHandle, configBytes) + if err != nil { + hostLog(shared.LogLevelWarn, "Failed to load listener filter configuration for %q: %v", []any{nameStr, err}) + return nil + } + if factory == nil { + hostLog(shared.LogLevelWarn, "Failed to load listener filter configuration for %q: factory returned nil", []any{nameStr}) + return nil + } + wrapper := &listenerFilterConfigWrapper{factory: factory, configHandle: configHandle} + configPtr := listenerConfigManager.record(wrapper) + return C.envoy_dynamic_module_type_listener_filter_config_module_ptr(configPtr) +} + +//export envoy_dynamic_module_on_listener_filter_config_destroy +func envoy_dynamic_module_on_listener_filter_config_destroy( + configPtr C.envoy_dynamic_module_type_listener_filter_config_module_ptr, +) { + wrapper := listenerConfigManager.unwrap(unsafe.Pointer(configPtr)) + if wrapper == nil { + return + } + if wrapper.configHandle.scheduler != nil { + // See bootstrap config destroy for why we close synchronously. + wrapper.configHandle.scheduler.close() + wrapper.configHandle.scheduler = nil + } + wrapper.factory.OnDestroy() + listenerConfigManager.remove(unsafe.Pointer(configPtr)) +} + +//export envoy_dynamic_module_on_listener_filter_new +func envoy_dynamic_module_on_listener_filter_new( + configPtr C.envoy_dynamic_module_type_listener_filter_config_module_ptr, + hostFilterPtr C.envoy_dynamic_module_type_listener_filter_envoy_ptr, +) C.envoy_dynamic_module_type_listener_filter_module_ptr { + cfg := listenerConfigManager.unwrap(unsafe.Pointer(configPtr)) + if cfg == nil { + return nil + } + handle := &dymListenerFilterHandle{hostFilterPtr: hostFilterPtr} + handle.plugin = cfg.factory.Create(handle) + if handle.plugin == nil { + return nil + } + filterPtr := listenerFilterManager.record(handle) + return C.envoy_dynamic_module_type_listener_filter_module_ptr(filterPtr) +} + +//export envoy_dynamic_module_on_listener_filter_on_accept +func envoy_dynamic_module_on_listener_filter_on_accept( + _ C.envoy_dynamic_module_type_listener_filter_envoy_ptr, + filterPtr C.envoy_dynamic_module_type_listener_filter_module_ptr, +) C.envoy_dynamic_module_type_on_listener_filter_status { + h := listenerFilterManager.unwrap(unsafe.Pointer(filterPtr)) + if h == nil || h.plugin == nil || h.destroyed { + return 0 + } + return C.envoy_dynamic_module_type_on_listener_filter_status(h.plugin.OnAccept(h)) +} + +//export envoy_dynamic_module_on_listener_filter_on_data +func envoy_dynamic_module_on_listener_filter_on_data( + _ C.envoy_dynamic_module_type_listener_filter_envoy_ptr, + filterPtr C.envoy_dynamic_module_type_listener_filter_module_ptr, + _ignoredDataLen C.size_t, +) C.envoy_dynamic_module_type_on_listener_filter_status { + h := listenerFilterManager.unwrap(unsafe.Pointer(filterPtr)) + if h == nil || h.plugin == nil || h.destroyed { + return 0 + } + return C.envoy_dynamic_module_type_on_listener_filter_status(h.plugin.OnData(h)) +} + +//export envoy_dynamic_module_on_listener_filter_on_close +func envoy_dynamic_module_on_listener_filter_on_close( + _ C.envoy_dynamic_module_type_listener_filter_envoy_ptr, + filterPtr C.envoy_dynamic_module_type_listener_filter_module_ptr, +) { + h := listenerFilterManager.unwrap(unsafe.Pointer(filterPtr)) + if h == nil || h.plugin == nil || h.destroyed { + return + } + h.plugin.OnClose(h) +} + +//export envoy_dynamic_module_on_listener_filter_get_max_read_bytes +func envoy_dynamic_module_on_listener_filter_get_max_read_bytes( + _ C.envoy_dynamic_module_type_listener_filter_envoy_ptr, + filterPtr C.envoy_dynamic_module_type_listener_filter_module_ptr, +) C.size_t { + h := listenerFilterManager.unwrap(unsafe.Pointer(filterPtr)) + if h == nil || h.plugin == nil || h.destroyed { + return 0 + } + return C.size_t(h.plugin.GetMaxReadBytes()) +} + +//export envoy_dynamic_module_on_listener_filter_destroy +func envoy_dynamic_module_on_listener_filter_destroy( + filterPtr C.envoy_dynamic_module_type_listener_filter_module_ptr, +) { + h := listenerFilterManager.unwrap(unsafe.Pointer(filterPtr)) + if h == nil || h.destroyed { + return + } + h.destroyed = true + if h.plugin != nil { + h.plugin.OnDestroy() + } + if h.scheduler != nil { + // See bootstrap config destroy for why we close synchronously. + h.scheduler.close() + h.scheduler = nil + } + listenerFilterManager.remove(unsafe.Pointer(filterPtr)) +} + +//export envoy_dynamic_module_on_listener_filter_scheduled +func envoy_dynamic_module_on_listener_filter_scheduled( + _ C.envoy_dynamic_module_type_listener_filter_envoy_ptr, + filterPtr C.envoy_dynamic_module_type_listener_filter_module_ptr, + eventID C.uint64_t, +) { + h := listenerFilterManager.unwrap(unsafe.Pointer(filterPtr)) + if h == nil || h.destroyed || h.scheduler == nil { + return + } + h.scheduler.onScheduled(uint64(eventID)) +} + +//export envoy_dynamic_module_on_listener_filter_config_scheduled +func envoy_dynamic_module_on_listener_filter_config_scheduled( + _ C.envoy_dynamic_module_type_listener_filter_config_envoy_ptr, + configPtr C.envoy_dynamic_module_type_listener_filter_config_module_ptr, + eventID C.uint64_t, +) { + cfg := listenerConfigManager.unwrap(unsafe.Pointer(configPtr)) + if cfg == nil || cfg.configHandle == nil || cfg.configHandle.scheduler == nil { + return + } + cfg.configHandle.scheduler.onScheduled(uint64(eventID)) +} + +//export envoy_dynamic_module_on_listener_filter_http_callout_done +func envoy_dynamic_module_on_listener_filter_http_callout_done( + _ C.envoy_dynamic_module_type_listener_filter_envoy_ptr, + filterPtr C.envoy_dynamic_module_type_listener_filter_module_ptr, + calloutID C.uint64_t, + result C.envoy_dynamic_module_type_http_callout_result, + headers *C.envoy_dynamic_module_type_envoy_http_header, + headersSize C.size_t, + chunks *C.envoy_dynamic_module_type_envoy_buffer, + chunksSize C.size_t, +) { + h := listenerFilterManager.unwrap(unsafe.Pointer(filterPtr)) + if h == nil || h.destroyed { + return + } + resultHeaders := envoyHttpHeaderSliceToUnsafeHeaderSlice(unsafe.Slice(headers, int(headersSize))) + resultChunks := envoyBufferSliceToUnsafeEnvoyBufferSlice(unsafe.Slice(chunks, int(chunksSize))) + + h.calloutMu.Lock() + cb := h.calloutCallbacks[uint64(calloutID)] + delete(h.calloutCallbacks, uint64(calloutID)) + h.calloutMu.Unlock() + if cb != nil { + cb.OnHttpCalloutDone(uint64(calloutID), shared.HttpCalloutResult(result), resultHeaders, resultChunks) + } +} diff --git a/source/extensions/dynamic_modules/sdk/go/abi/load_balancer.go b/source/extensions/dynamic_modules/sdk/go/abi/load_balancer.go new file mode 100644 index 0000000000000..b6d315ff8cf63 --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/abi/load_balancer.go @@ -0,0 +1,511 @@ +package abi + +/* +#cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup +#cgo linux LDFLAGS: -Wl,--unresolved-symbols=ignore-all +#include +#include +#include +#include +#include "../../../abi/abi.h" +*/ +import "C" +import ( + "runtime" + "unsafe" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +type lbConfigWrapper struct { + factory shared.LoadBalancerFactory + configHandle *dymLbConfigHandle +} + +type lbWrapper = dymLoadBalancerHandle + +var lbConfigManager = newManager[lbConfigWrapper]() +var lbManager = newManager[lbWrapper]() + +// dymLbConfigHandle implements shared.LoadBalancerConfigHandle using the labeled-metric ABI. +type dymLbConfigHandle struct { + hostConfigPtr C.envoy_dynamic_module_type_lb_config_envoy_ptr +} + +func (h *dymLbConfigHandle) DefineCounter(name string, labelNames []string) (shared.MetricID, shared.MetricsResult) { + labels := stringSlicesToModuleBuffers(labelNames) + var id C.size_t + ret := C.envoy_dynamic_module_callback_lb_config_define_counter( + h.hostConfigPtr, stringToModuleBuffer(name), + bufferSlicePtr(labels), C.size_t(len(labels)), &id, + ) + runtime.KeepAlive(name) + runtime.KeepAlive(labelNames) + runtime.KeepAlive(labels) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymLbConfigHandle) IncrementCounter(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_lb_config_increment_counter( + h.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +func (h *dymLbConfigHandle) DefineGauge(name string, labelNames []string) (shared.MetricID, shared.MetricsResult) { + labels := stringSlicesToModuleBuffers(labelNames) + var id C.size_t + ret := C.envoy_dynamic_module_callback_lb_config_define_gauge( + h.hostConfigPtr, stringToModuleBuffer(name), + bufferSlicePtr(labels), C.size_t(len(labels)), &id, + ) + runtime.KeepAlive(name) + runtime.KeepAlive(labelNames) + runtime.KeepAlive(labels) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymLbConfigHandle) SetGauge(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_lb_config_set_gauge( + h.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +func (h *dymLbConfigHandle) IncrementGauge(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_lb_config_increment_gauge( + h.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +func (h *dymLbConfigHandle) DecrementGauge(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_lb_config_decrement_gauge( + h.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +func (h *dymLbConfigHandle) DefineHistogram(name string, labelNames []string) (shared.MetricID, shared.MetricsResult) { + labels := stringSlicesToModuleBuffers(labelNames) + var id C.size_t + ret := C.envoy_dynamic_module_callback_lb_config_define_histogram( + h.hostConfigPtr, stringToModuleBuffer(name), + bufferSlicePtr(labels), C.size_t(len(labels)), &id, + ) + runtime.KeepAlive(name) + runtime.KeepAlive(labelNames) + runtime.KeepAlive(labels) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymLbConfigHandle) RecordHistogramValue(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_lb_config_record_histogram_value( + h.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +// dymLoadBalancerHandle implements shared.LoadBalancerHandle. +type dymLoadBalancerHandle struct { + hostLbPtr C.envoy_dynamic_module_type_lb_envoy_ptr + + plugin shared.LoadBalancer + destroyed bool +} + +func (h *dymLoadBalancerHandle) GetClusterName() shared.UnsafeEnvoyBuffer { + var buf C.envoy_dynamic_module_type_envoy_buffer + C.envoy_dynamic_module_callback_lb_get_cluster_name(h.hostLbPtr, &buf) + if buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{} + } + return envoyBufferToUnsafeEnvoyBuffer(buf) +} + +func (h *dymLoadBalancerHandle) GetHostsCount(priority uint32) uint64 { + return uint64(C.envoy_dynamic_module_callback_lb_get_hosts_count(h.hostLbPtr, C.uint32_t(priority))) +} + +func (h *dymLoadBalancerHandle) GetHealthyHostsCount(priority uint32) uint64 { + return uint64(C.envoy_dynamic_module_callback_lb_get_healthy_hosts_count(h.hostLbPtr, C.uint32_t(priority))) +} + +func (h *dymLoadBalancerHandle) GetDegradedHostsCount(priority uint32) uint64 { + return uint64(C.envoy_dynamic_module_callback_lb_get_degraded_hosts_count(h.hostLbPtr, C.uint32_t(priority))) +} + +func (h *dymLoadBalancerHandle) GetPrioritySetSize() uint64 { + return uint64(C.envoy_dynamic_module_callback_lb_get_priority_set_size(h.hostLbPtr)) +} + +func (h *dymLoadBalancerHandle) GetHealthyHostAddress(priority uint32, index uint64) (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_lb_get_healthy_host_address( + h.hostLbPtr, C.uint32_t(priority), C.size_t(index), &buf) + if !bool(ok) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ok) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func (h *dymLoadBalancerHandle) GetHealthyHostWeight(priority uint32, index uint64) uint32 { + return uint32(C.envoy_dynamic_module_callback_lb_get_healthy_host_weight( + h.hostLbPtr, C.uint32_t(priority), C.size_t(index))) +} + +func (h *dymLoadBalancerHandle) GetHostHealth(priority uint32, index uint64) shared.HostHealth { + return shared.HostHealth(C.envoy_dynamic_module_callback_lb_get_host_health( + h.hostLbPtr, C.uint32_t(priority), C.size_t(index))) +} + +func (h *dymLoadBalancerHandle) GetHostHealthByAddress(address string) (shared.HostHealth, bool) { + var v C.envoy_dynamic_module_type_host_health + ok := C.envoy_dynamic_module_callback_lb_get_host_health_by_address( + h.hostLbPtr, stringToModuleBuffer(address), &v) + runtime.KeepAlive(address) + if !bool(ok) { + return 0, false + } + return shared.HostHealth(v), true +} + +func (h *dymLoadBalancerHandle) GetHostAddress(priority uint32, index uint64) (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_lb_get_host_address( + h.hostLbPtr, C.uint32_t(priority), C.size_t(index), &buf) + if !bool(ok) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ok) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func (h *dymLoadBalancerHandle) GetHostWeight(priority uint32, index uint64) uint32 { + return uint32(C.envoy_dynamic_module_callback_lb_get_host_weight( + h.hostLbPtr, C.uint32_t(priority), C.size_t(index))) +} + +func (h *dymLoadBalancerHandle) GetHostLocality(priority uint32, index uint64) (shared.UnsafeEnvoyBuffer, shared.UnsafeEnvoyBuffer, shared.UnsafeEnvoyBuffer, bool) { + var region, zone, subZone C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_lb_get_host_locality( + h.hostLbPtr, C.uint32_t(priority), C.size_t(index), ®ion, &zone, &subZone) + if !bool(ok) { + return shared.UnsafeEnvoyBuffer{}, shared.UnsafeEnvoyBuffer{}, shared.UnsafeEnvoyBuffer{}, false + } + return envoyBufferToUnsafeEnvoyBuffer(region), + envoyBufferToUnsafeEnvoyBuffer(zone), + envoyBufferToUnsafeEnvoyBuffer(subZone), + true +} + +func (h *dymLoadBalancerHandle) SetHostData(priority uint32, index uint64, data uintptr) bool { + return bool(C.envoy_dynamic_module_callback_lb_set_host_data( + h.hostLbPtr, C.uint32_t(priority), C.size_t(index), C.uintptr_t(data))) +} + +func (h *dymLoadBalancerHandle) GetHostData(priority uint32, index uint64) (uintptr, bool) { + var data C.uintptr_t + ok := C.envoy_dynamic_module_callback_lb_get_host_data( + h.hostLbPtr, C.uint32_t(priority), C.size_t(index), &data) + if !bool(ok) { + return 0, false + } + return uintptr(data), true +} + +func (h *dymLoadBalancerHandle) GetHostMetadataString(priority uint32, index uint64, filterName, key string) (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_lb_get_host_metadata_string( + h.hostLbPtr, C.uint32_t(priority), C.size_t(index), + stringToModuleBuffer(filterName), stringToModuleBuffer(key), &buf) + runtime.KeepAlive(filterName) + runtime.KeepAlive(key) + if !bool(ok) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ok) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func (h *dymLoadBalancerHandle) GetHostMetadataNumber(priority uint32, index uint64, filterName, key string) (float64, bool) { + var v C.double + ok := C.envoy_dynamic_module_callback_lb_get_host_metadata_number( + h.hostLbPtr, C.uint32_t(priority), C.size_t(index), + stringToModuleBuffer(filterName), stringToModuleBuffer(key), &v) + runtime.KeepAlive(filterName) + runtime.KeepAlive(key) + if !bool(ok) { + return 0, false + } + return float64(v), true +} + +func (h *dymLoadBalancerHandle) GetHostMetadataBool(priority uint32, index uint64, filterName, key string) (bool, bool) { + var v C.bool + ok := C.envoy_dynamic_module_callback_lb_get_host_metadata_bool( + h.hostLbPtr, C.uint32_t(priority), C.size_t(index), + stringToModuleBuffer(filterName), stringToModuleBuffer(key), &v) + runtime.KeepAlive(filterName) + runtime.KeepAlive(key) + if !bool(ok) { + return false, false + } + return bool(v), true +} + +func (h *dymLoadBalancerHandle) GetHostStat(priority uint32, index uint64, stat shared.HostStat) uint64 { + return uint64(C.envoy_dynamic_module_callback_lb_get_host_stat( + h.hostLbPtr, C.uint32_t(priority), C.size_t(index), + C.envoy_dynamic_module_type_host_stat(stat))) +} + +func (h *dymLoadBalancerHandle) GetLocalityCount(priority uint32) uint64 { + return uint64(C.envoy_dynamic_module_callback_lb_get_locality_count(h.hostLbPtr, C.uint32_t(priority))) +} + +func (h *dymLoadBalancerHandle) GetLocalityHostCount(priority uint32, localityIndex uint64) uint64 { + return uint64(C.envoy_dynamic_module_callback_lb_get_locality_host_count( + h.hostLbPtr, C.uint32_t(priority), C.size_t(localityIndex))) +} + +func (h *dymLoadBalancerHandle) GetLocalityHostAddress(priority uint32, localityIndex, hostIndex uint64) (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_lb_get_locality_host_address( + h.hostLbPtr, C.uint32_t(priority), C.size_t(localityIndex), C.size_t(hostIndex), &buf) + if !bool(ok) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ok) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func (h *dymLoadBalancerHandle) GetLocalityWeight(priority uint32, localityIndex uint64) uint32 { + return uint32(C.envoy_dynamic_module_callback_lb_get_locality_weight( + h.hostLbPtr, C.uint32_t(priority), C.size_t(localityIndex))) +} + +func (h *dymLoadBalancerHandle) GetMemberUpdateHostAddress(index uint64, isAdded bool) (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ok := C.envoy_dynamic_module_callback_lb_get_member_update_host_address( + h.hostLbPtr, C.size_t(index), C.bool(isAdded), &buf) + if !bool(ok) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ok) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +// dymLoadBalancerContext implements shared.LoadBalancerContext, bound to a single ChooseHost +// call. +type dymLoadBalancerContext struct { + hostCtxPtr C.envoy_dynamic_module_type_lb_context_envoy_ptr + lbHandle *dymLoadBalancerHandle // for ShouldSelectAnotherHost +} + +func (c *dymLoadBalancerContext) ComputeHashKey() (uint64, bool) { + var v C.uint64_t + ok := C.envoy_dynamic_module_callback_lb_context_compute_hash_key(c.hostCtxPtr, &v) + if !bool(ok) { + return 0, false + } + return uint64(v), true +} + +func (c *dymLoadBalancerContext) GetDownstreamHeadersSize() uint64 { + return uint64(C.envoy_dynamic_module_callback_lb_context_get_downstream_headers_size(c.hostCtxPtr)) +} + +func (c *dymLoadBalancerContext) GetDownstreamHeaders() [][2]shared.UnsafeEnvoyBuffer { + size := C.envoy_dynamic_module_callback_lb_context_get_downstream_headers_size(c.hostCtxPtr) + if size == 0 { + return nil + } + hdrs := make([]C.envoy_dynamic_module_type_envoy_http_header, int(size)) + if !bool(C.envoy_dynamic_module_callback_lb_context_get_downstream_headers( + c.hostCtxPtr, unsafe.SliceData(hdrs))) { + return nil + } + out := envoyHttpHeaderSliceToUnsafeHeaderSlice(hdrs) + runtime.KeepAlive(hdrs) + return out +} + +func (c *dymLoadBalancerContext) GetDownstreamHeader(key string, index uint64) (shared.UnsafeEnvoyBuffer, uint64, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var total C.size_t + ok := C.envoy_dynamic_module_callback_lb_context_get_downstream_header( + c.hostCtxPtr, stringToModuleBuffer(key), &buf, C.size_t(index), &total) + runtime.KeepAlive(key) + if !bool(ok) { + return shared.UnsafeEnvoyBuffer{}, uint64(total), false + } + if buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, uint64(total), true + } + return envoyBufferToUnsafeEnvoyBuffer(buf), uint64(total), true +} + +func (c *dymLoadBalancerContext) GetHostSelectionRetryCount() uint32 { + return uint32(C.envoy_dynamic_module_callback_lb_context_get_host_selection_retry_count(c.hostCtxPtr)) +} + +func (c *dymLoadBalancerContext) ShouldSelectAnotherHost(_ shared.LoadBalancerHandle, priority uint32, index uint64) bool { + if c.lbHandle == nil { + return false + } + return bool(C.envoy_dynamic_module_callback_lb_context_should_select_another_host( + c.lbHandle.hostLbPtr, c.hostCtxPtr, C.uint32_t(priority), C.size_t(index))) +} + +func (c *dymLoadBalancerContext) GetOverrideHost() (shared.UnsafeEnvoyBuffer, bool, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var strict C.bool + ok := C.envoy_dynamic_module_callback_lb_context_get_override_host(c.hostCtxPtr, &buf, &strict) + if !bool(ok) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(strict), bool(ok) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), bool(strict), true +} + +// ============================================================================= +// Event hooks +// ============================================================================= + +//export envoy_dynamic_module_on_lb_config_new +func envoy_dynamic_module_on_lb_config_new( + hostConfigPtr C.envoy_dynamic_module_type_lb_config_envoy_ptr, + name C.envoy_dynamic_module_type_envoy_buffer, + config C.envoy_dynamic_module_type_envoy_buffer, +) C.envoy_dynamic_module_type_lb_config_module_ptr { + nameStr := envoyBufferToStringUnsafe(name) + configBytes := envoyBufferToBytesCopy(config) + + configHandle := &dymLbConfigHandle{hostConfigPtr: hostConfigPtr} + configFactory := sdk.GetLoadBalancerConfigFactory(nameStr) + if configFactory == nil { + hostLog(shared.LogLevelWarn, "Failed to load load balancer configuration for %q: no factory registered", []any{nameStr}) + return nil + } + factory, err := configFactory.Create(configHandle, configBytes) + if err != nil { + hostLog(shared.LogLevelWarn, "Failed to load load balancer configuration for %q: %v", []any{nameStr, err}) + return nil + } + if factory == nil { + hostLog(shared.LogLevelWarn, "Failed to load load balancer configuration for %q: factory returned nil", []any{nameStr}) + return nil + } + wrapper := &lbConfigWrapper{factory: factory, configHandle: configHandle} + configPtr := lbConfigManager.record(wrapper) + return C.envoy_dynamic_module_type_lb_config_module_ptr(configPtr) +} + +//export envoy_dynamic_module_on_lb_config_destroy +func envoy_dynamic_module_on_lb_config_destroy( + configPtr C.envoy_dynamic_module_type_lb_config_module_ptr, +) { + w := lbConfigManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil { + return + } + w.factory.OnDestroy() + lbConfigManager.remove(unsafe.Pointer(configPtr)) +} + +//export envoy_dynamic_module_on_lb_new +func envoy_dynamic_module_on_lb_new( + configPtr C.envoy_dynamic_module_type_lb_config_module_ptr, + hostLbPtr C.envoy_dynamic_module_type_lb_envoy_ptr, +) C.envoy_dynamic_module_type_lb_module_ptr { + cfg := lbConfigManager.unwrap(unsafe.Pointer(configPtr)) + if cfg == nil { + return nil + } + handle := &dymLoadBalancerHandle{hostLbPtr: hostLbPtr} + handle.plugin = cfg.factory.Create(handle) + if handle.plugin == nil { + return nil + } + lbPtr := lbManager.record(handle) + return C.envoy_dynamic_module_type_lb_module_ptr(lbPtr) +} + +//export envoy_dynamic_module_on_lb_choose_host +func envoy_dynamic_module_on_lb_choose_host( + hostLbPtr C.envoy_dynamic_module_type_lb_envoy_ptr, + lbPtr C.envoy_dynamic_module_type_lb_module_ptr, + hostCtxPtr C.envoy_dynamic_module_type_lb_context_envoy_ptr, + resultPriority *C.uint32_t, + resultIndex *C.uint32_t, +) C.bool { + w := lbManager.unwrap(unsafe.Pointer(lbPtr)) + if w == nil || w.plugin == nil || w.destroyed { + return false + } + // Refresh the lb pointer in case Envoy gives us a fresh one per call. + w.hostLbPtr = hostLbPtr + var ctx shared.LoadBalancerContext + if hostCtxPtr != nil { + ctx = &dymLoadBalancerContext{hostCtxPtr: hostCtxPtr, lbHandle: w} + } + sel, ok := w.plugin.ChooseHost(w, ctx) + if !ok { + return false + } + *resultPriority = C.uint32_t(sel.Priority) + *resultIndex = C.uint32_t(sel.Index) + return true +} + +//export envoy_dynamic_module_on_lb_on_host_membership_update +func envoy_dynamic_module_on_lb_on_host_membership_update( + hostLbPtr C.envoy_dynamic_module_type_lb_envoy_ptr, + lbPtr C.envoy_dynamic_module_type_lb_module_ptr, + numHostsAdded C.size_t, + numHostsRemoved C.size_t, +) { + w := lbManager.unwrap(unsafe.Pointer(lbPtr)) + if w == nil || w.plugin == nil || w.destroyed { + return + } + w.hostLbPtr = hostLbPtr + w.plugin.OnHostMembershipUpdate(w, uint64(numHostsAdded), uint64(numHostsRemoved)) +} + +//export envoy_dynamic_module_on_lb_destroy +func envoy_dynamic_module_on_lb_destroy( + lbPtr C.envoy_dynamic_module_type_lb_module_ptr, +) { + w := lbManager.unwrap(unsafe.Pointer(lbPtr)) + if w == nil || w.destroyed { + return + } + w.destroyed = true + if w.plugin != nil { + w.plugin.OnDestroy() + } + lbManager.remove(unsafe.Pointer(lbPtr)) +} diff --git a/source/extensions/dynamic_modules/sdk/go/abi/matcher.go b/source/extensions/dynamic_modules/sdk/go/abi/matcher.go new file mode 100644 index 0000000000000..d2551474ae381 --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/abi/matcher.go @@ -0,0 +1,137 @@ +package abi + +/* +#cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup +#cgo linux LDFLAGS: -Wl,--unresolved-symbols=ignore-all +#include +#include +#include +#include +#include "../../../abi/abi.h" +*/ +import "C" +import ( + "runtime" + "unsafe" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +type matcherConfigWrapper struct { + matcher shared.Matcher + + // destroyed is set during config_destroy so a late on_match becomes a no-op + // returning false instead of re-entering user code. + destroyed bool +} + +var matcherConfigManager = newManager[matcherConfigWrapper]() + +// dymMatchInputContext implements shared.MatchInputContext. +type dymMatchInputContext struct { + hostInputPtr C.envoy_dynamic_module_type_matcher_input_envoy_ptr +} + +func (c *dymMatchInputContext) GetHeadersSize(headerType shared.HttpHeaderType) uint64 { + return uint64(C.envoy_dynamic_module_callback_matcher_get_headers_size( + c.hostInputPtr, C.envoy_dynamic_module_type_http_header_type(headerType))) +} + +func (c *dymMatchInputContext) GetHeaders(headerType shared.HttpHeaderType) [][2]shared.UnsafeEnvoyBuffer { + size := C.envoy_dynamic_module_callback_matcher_get_headers_size( + c.hostInputPtr, C.envoy_dynamic_module_type_http_header_type(headerType)) + if size == 0 { + return nil + } + hdrs := make([]C.envoy_dynamic_module_type_envoy_http_header, int(size)) + if !bool(C.envoy_dynamic_module_callback_matcher_get_headers( + c.hostInputPtr, C.envoy_dynamic_module_type_http_header_type(headerType), + unsafe.SliceData(hdrs))) { + return nil + } + out := envoyHttpHeaderSliceToUnsafeHeaderSlice(hdrs) + runtime.KeepAlive(hdrs) + return out +} + +func (c *dymMatchInputContext) GetHeaderValue(headerType shared.HttpHeaderType, key string, index uint64) (shared.UnsafeEnvoyBuffer, uint64, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var total C.size_t + ret := C.envoy_dynamic_module_callback_matcher_get_header_value( + c.hostInputPtr, + C.envoy_dynamic_module_type_http_header_type(headerType), + stringToModuleBuffer(key), + &buf, + C.size_t(index), + &total, + ) + runtime.KeepAlive(key) + if !bool(ret) { + return shared.UnsafeEnvoyBuffer{}, uint64(total), false + } + if buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, uint64(total), true + } + return envoyBufferToUnsafeEnvoyBuffer(buf), uint64(total), true +} + +// ============================================================================= +// Event hooks +// ============================================================================= + +//export envoy_dynamic_module_on_matcher_config_new +func envoy_dynamic_module_on_matcher_config_new( + _ C.envoy_dynamic_module_type_matcher_config_envoy_ptr, + name C.envoy_dynamic_module_type_envoy_buffer, + config C.envoy_dynamic_module_type_envoy_buffer, +) C.envoy_dynamic_module_type_matcher_config_module_ptr { + nameStr := envoyBufferToStringUnsafe(name) + configBytes := envoyBufferToBytesCopy(config) + + configFactory := sdk.GetMatcherConfigFactory(nameStr) + if configFactory == nil { + hostLog(shared.LogLevelWarn, "Failed to load matcher configuration for %q: no factory registered", []any{nameStr}) + return nil + } + matcher, err := configFactory.Create(nameStr, configBytes) + if err != nil { + hostLog(shared.LogLevelWarn, "Failed to load matcher configuration for %q: %v", []any{nameStr, err}) + return nil + } + if matcher == nil { + hostLog(shared.LogLevelWarn, "Failed to load matcher configuration for %q: factory returned nil", []any{nameStr}) + return nil + } + wrapper := &matcherConfigWrapper{matcher: matcher} + configPtr := matcherConfigManager.record(wrapper) + return C.envoy_dynamic_module_type_matcher_config_module_ptr(configPtr) +} + +//export envoy_dynamic_module_on_matcher_config_destroy +func envoy_dynamic_module_on_matcher_config_destroy( + configPtr C.envoy_dynamic_module_type_matcher_config_module_ptr, +) { + w := matcherConfigManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil || w.destroyed { + return + } + w.destroyed = true + if w.matcher != nil { + w.matcher.OnDestroy() + } + matcherConfigManager.remove(unsafe.Pointer(configPtr)) +} + +//export envoy_dynamic_module_on_matcher_match +func envoy_dynamic_module_on_matcher_match( + configPtr C.envoy_dynamic_module_type_matcher_config_module_ptr, + inputPtr C.envoy_dynamic_module_type_matcher_input_envoy_ptr, +) C.bool { + w := matcherConfigManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil || w.matcher == nil || w.destroyed { + return false + } + ctx := &dymMatchInputContext{hostInputPtr: inputPtr} + return C.bool(w.matcher.OnMatch(ctx)) +} diff --git a/source/extensions/dynamic_modules/sdk/go/abi/network.go b/source/extensions/dynamic_modules/sdk/go/abi/network.go index 7cc1e9e4cd35b..94b7e6826ea0f 100644 --- a/source/extensions/dynamic_modules/sdk/go/abi/network.go +++ b/source/extensions/dynamic_modules/sdk/go/abi/network.go @@ -9,6 +9,7 @@ import "C" import ( "runtime" + "sync" "unsafe" sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" @@ -20,10 +21,8 @@ type networkFilterConfigWrapper struct { configHandle *dymNetworkConfigHandle } -type networkFilterWrapper = dymNetworkFilterHandle - var networkConfigManager = newManager[networkFilterConfigWrapper]() -var networkPluginManager = newManager[networkFilterWrapper]() +var networkPluginManager = newManager[dymNetworkFilterHandle]() type dymNetworkBuffer struct { hostPluginPtr C.envoy_dynamic_module_type_network_filter_envoy_ptr @@ -130,10 +129,15 @@ func (b *dymNetworkBuffer) Append(data []byte) bool { } type dymNetworkFilterHandle struct { - hostPluginPtr C.envoy_dynamic_module_type_network_filter_envoy_ptr - plugin shared.NetworkFilter - readBuffer dymNetworkBuffer - writeBuffer dymNetworkBuffer + hostPluginPtr C.envoy_dynamic_module_type_network_filter_envoy_ptr + plugin shared.NetworkFilter + readBuffer dymNetworkBuffer + writeBuffer dymNetworkBuffer + // calloutMu guards calloutCallbacks. Per-connection network filter processing is + // single-threaded today, so this lock is uncontended in normal use; it's held for + // consistency with cluster/bootstrap callout maps and to keep the SDK safe if a future + // change introduces cross-thread callout dispatch. + calloutMu sync.Mutex calloutCallbacks map[uint64]shared.HttpCalloutCallback scheduler *dymScheduler filterDestroyed bool @@ -669,10 +673,12 @@ func (h *dymNetworkFilterHandle) HttpCallout( return goResult, 0 } + h.calloutMu.Lock() if h.calloutCallbacks == nil { h.calloutCallbacks = make(map[uint64]shared.HttpCalloutCallback) } h.calloutCallbacks[uint64(calloutID)] = cb + h.calloutMu.Unlock() return goResult, uint64(calloutID) } @@ -879,13 +885,15 @@ func (h *dymNetworkFilterHandle) GetScheduler() shared.Scheduler { taskID, ) }, + func(p unsafe.Pointer) { + C.envoy_dynamic_module_callback_network_filter_scheduler_delete( + C.envoy_dynamic_module_type_network_filter_scheduler_module_ptr(p), + ) + }, ) - runtime.SetFinalizer(h.scheduler, func(s *dymScheduler) { - C.envoy_dynamic_module_callback_network_filter_scheduler_delete( - C.envoy_dynamic_module_type_network_filter_scheduler_module_ptr(s.schedulerPtr), - ) - }) + // Finalizer is a fallback; the destroy hook should call close() synchronously. + runtime.SetFinalizer(h.scheduler, func(s *dymScheduler) { s.close() }) } return h.scheduler } @@ -951,15 +959,15 @@ func (h *dymNetworkConfigHandle) GetScheduler() shared.Scheduler { taskID, ) }, + func(p unsafe.Pointer) { + C.envoy_dynamic_module_callback_network_filter_config_scheduler_delete( + C.envoy_dynamic_module_type_network_filter_config_scheduler_module_ptr(p), + ) + }, ) - runtime.SetFinalizer(h.scheduler, func(s *dymScheduler) { - C.envoy_dynamic_module_callback_network_filter_config_scheduler_delete( - C.envoy_dynamic_module_type_network_filter_config_scheduler_module_ptr( - s.schedulerPtr, - ), - ) - }) + // Finalizer is a fallback; the destroy hook should call close() synchronously. + runtime.SetFinalizer(h.scheduler, func(s *dymScheduler) { s.close() }) } return h.scheduler } @@ -971,7 +979,7 @@ func envoy_dynamic_module_on_network_filter_config_new( config C.envoy_dynamic_module_type_envoy_buffer, ) C.envoy_dynamic_module_type_network_filter_config_module_ptr { nameString := envoyBufferToStringUnsafe(name) - configBytes := envoyBufferToBytesUnsafe(config) + configBytes := envoyBufferToBytesCopy(config) configHandle := &dymNetworkConfigHandle{hostConfigPtr: hostConfigPtr} factory, err := sdk.NewNetworkFilterFactory(configHandle, nameString, configBytes) @@ -1001,7 +1009,11 @@ func envoy_dynamic_module_on_network_filter_config_destroy( if configWrapper == nil { return } - configWrapper.configHandle.scheduler = nil + if configWrapper.configHandle.scheduler != nil { + // See bootstrap config destroy for why we close synchronously. + configWrapper.configHandle.scheduler.close() + configWrapper.configHandle.scheduler = nil + } configWrapper.pluginFactory.OnDestroy() networkConfigManager.remove(unsafe.Pointer(configPtr)) } @@ -1106,7 +1118,11 @@ func envoy_dynamic_module_on_network_filter_destroy( return } filterWrapper.filterDestroyed = true - filterWrapper.scheduler = nil + if filterWrapper.scheduler != nil { + // See bootstrap config destroy for why we close synchronously. + filterWrapper.scheduler.close() + filterWrapper.scheduler = nil + } if filterWrapper.plugin != nil { filterWrapper.plugin.OnDestroy() } @@ -1133,9 +1149,13 @@ func envoy_dynamic_module_on_network_filter_http_callout_done( resultHeaders := envoyHttpHeaderSliceToUnsafeHeaderSlice(unsafe.Slice(headers, int(headersSize))) resultChunks := envoyBufferSliceToUnsafeEnvoyBufferSlice(unsafe.Slice(chunks, int(chunksSize))) + filterWrapper.calloutMu.Lock() cb := filterWrapper.calloutCallbacks[uint64(calloutID)] if cb != nil { delete(filterWrapper.calloutCallbacks, uint64(calloutID)) + } + filterWrapper.calloutMu.Unlock() + if cb != nil { cb.OnHttpCalloutDone(uint64(calloutID), shared.HttpCalloutResult(result), resultHeaders, resultChunks) } } diff --git a/source/extensions/dynamic_modules/sdk/go/abi/program.go b/source/extensions/dynamic_modules/sdk/go/abi/program.go new file mode 100644 index 0000000000000..25ec068886b4b --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/abi/program.go @@ -0,0 +1,83 @@ +package abi + +/* +#cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup +#cgo linux LDFLAGS: -Wl,--unresolved-symbols=ignore-all +#include +#include +#include +#include +#include "../../../abi/abi.h" +*/ +import "C" +import ( + "runtime" + "unsafe" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +// init wires the live ABI-backed program handle into the sdk package so that +// sdk.GetConcurrency / sdk.IsValidationMode / sdk.Register*/Get* are usable from any module +// code that imports the abi package (which all modules do transitively). +func init() { + sdk.SetProgramHandle(DefaultProgramHandle) +} + +// dymProgramHandle implements shared.ProgramHandle by calling into Envoy via the +// dynamic-modules ABI. +type dymProgramHandle struct{} + +// DefaultProgramHandle is the singleton ProgramHandle backed by the live Envoy ABI. The sdk +// package exposes its methods via top-level convenience functions; modules under test can +// substitute their own shared.ProgramHandle implementation. +var DefaultProgramHandle shared.ProgramHandle = &dymProgramHandle{} + +func (dymProgramHandle) GetConcurrency() uint32 { + return uint32(C.envoy_dynamic_module_callback_get_concurrency()) +} + +func (dymProgramHandle) IsValidationMode() bool { + return bool(C.envoy_dynamic_module_callback_is_validation_mode()) +} + +func (dymProgramHandle) RegisterFunction(key string, fnPtr unsafe.Pointer) bool { + ret := C.envoy_dynamic_module_callback_register_function( + stringToModuleBuffer(key), fnPtr) + runtime.KeepAlive(key) + return bool(ret) +} + +func (dymProgramHandle) GetFunction(key string) (unsafe.Pointer, bool) { + var p unsafe.Pointer + ret := C.envoy_dynamic_module_callback_get_function( + stringToModuleBuffer(key), &p) + runtime.KeepAlive(key) + if !bool(ret) { + return nil, false + } + return p, true +} + +func (dymProgramHandle) RegisterSharedData(key string, dataPtr unsafe.Pointer) bool { + ret := C.envoy_dynamic_module_callback_register_shared_data( + stringToModuleBuffer(key), dataPtr) + runtime.KeepAlive(key) + return bool(ret) +} + +func (dymProgramHandle) GetSharedData(key string) (unsafe.Pointer, bool) { + var p unsafe.Pointer + ret := C.envoy_dynamic_module_callback_get_shared_data( + stringToModuleBuffer(key), &p) + runtime.KeepAlive(key) + if !bool(ret) { + return nil, false + } + return p, true +} + +func (dymProgramHandle) Log(level shared.LogLevel, format string, args ...any) { + hostLog(level, format, args) +} diff --git a/source/extensions/dynamic_modules/sdk/go/abi/scheduler.go b/source/extensions/dynamic_modules/sdk/go/abi/scheduler.go new file mode 100644 index 0000000000000..336f67a71bdc1 --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/abi/scheduler.go @@ -0,0 +1,74 @@ +package abi + +/* +#cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup +#cgo linux LDFLAGS: -Wl,--unresolved-symbols=ignore-all +#include +*/ +import "C" +import ( + "sync" + "unsafe" +) + +// dymScheduler is the SDK-side implementation of shared.Scheduler used by every extension +// surface that exposes a NewScheduler method (HTTP filter, network filter, cluster, bootstrap, +// etc.). It is defined here rather than in http.go so that the type's home file reflects its +// cross-surface role. +type dymScheduler struct { + schedulerPtr unsafe.Pointer + schedulerLock sync.Mutex + nextTaskID uint64 + tasks map[uint64]func() + commitFunc func(unsafe.Pointer, C.uint64_t) + // deleteFunc invokes the host's *_scheduler_delete callback for this scheduler. + // Stored here so close() can free the host-side allocation synchronously from the + // destroy hook (the finalizer-only path leaks under LeakSanitizer because Go GC + // finalizers don't run on process exit). + deleteFunc func(unsafe.Pointer) +} + +func newDymScheduler( + schedulerPtr unsafe.Pointer, + commitFunc func(unsafe.Pointer, C.uint64_t), + deleteFunc func(unsafe.Pointer), +) *dymScheduler { + return &dymScheduler{ + schedulerPtr: schedulerPtr, + tasks: make(map[uint64]func()), + commitFunc: commitFunc, + deleteFunc: deleteFunc, + } +} + +// close synchronously frees the host-side scheduler. Idempotent: subsequent calls and +// the runtime finalizer both no-op once schedulerPtr is nil. Must be called from the +// extension's destroy hook so the host allocation is reclaimed before the .so unloads. +func (s *dymScheduler) close() { + s.schedulerLock.Lock() + ptr := s.schedulerPtr + s.schedulerPtr = nil + s.schedulerLock.Unlock() + if ptr != nil && s.deleteFunc != nil { + s.deleteFunc(ptr) + } +} + +func (s *dymScheduler) Schedule(task func()) { + s.schedulerLock.Lock() + taskID := s.nextTaskID + s.nextTaskID++ + s.tasks[taskID] = task + s.schedulerLock.Unlock() + s.commitFunc(s.schedulerPtr, C.uint64_t(taskID)) +} + +func (s *dymScheduler) onScheduled(taskID uint64) { + s.schedulerLock.Lock() + task := s.tasks[taskID] + delete(s.tasks, taskID) + s.schedulerLock.Unlock() + if task != nil { + task() + } +} diff --git a/source/extensions/dynamic_modules/sdk/go/abi/tracer.go b/source/extensions/dynamic_modules/sdk/go/abi/tracer.go new file mode 100644 index 0000000000000..ebf991f12b8d1 --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/abi/tracer.go @@ -0,0 +1,474 @@ +package abi + +/* +#cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup +#cgo linux LDFLAGS: -Wl,--unresolved-symbols=ignore-all +#include +#include +#include +#include +#include "../../../abi/abi.h" +*/ +import "C" +import ( + "runtime" + "unsafe" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +type tracerConfigWrapper struct { + tracer shared.Tracer + configHandle *dymTracerConfigHandle + + // destroyed is set during config_destroy so any late StartSpan / span callbacks + // become no-ops instead of re-entering user code. + destroyed bool +} + +type tracerSpanWrapper struct { + span shared.TracerSpan + // keep digest-style returns alive across ABI calls that take pointers into Go memory + traceIDCache []byte + spanIDCache []byte + baggageCache []byte + + // destroyed is set during span destroy so any late span operations (set_tag, + // set_baggage, etc. that arrive concurrent with destroy) become no-ops. + destroyed bool +} + +var tracerConfigManager = newManager[tracerConfigWrapper]() +var tracerSpanManager = newManager[tracerSpanWrapper]() + +// dymTracerConfigHandle implements shared.TracerConfigHandle. +type dymTracerConfigHandle struct { + hostConfigPtr C.envoy_dynamic_module_type_tracer_config_envoy_ptr +} + +func (h *dymTracerConfigHandle) DefineCounter(name string, labelNames []string) (shared.MetricID, shared.MetricsResult) { + labels := stringSlicesToModuleBuffers(labelNames) + var id C.size_t + ret := C.envoy_dynamic_module_callback_tracer_define_counter( + h.hostConfigPtr, stringToModuleBuffer(name), + bufferSlicePtr(labels), C.size_t(len(labels)), &id, + ) + runtime.KeepAlive(name) + runtime.KeepAlive(labelNames) + runtime.KeepAlive(labels) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymTracerConfigHandle) IncrementCounter(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_tracer_increment_counter( + h.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +func (h *dymTracerConfigHandle) DefineGauge(name string, labelNames []string) (shared.MetricID, shared.MetricsResult) { + labels := stringSlicesToModuleBuffers(labelNames) + var id C.size_t + ret := C.envoy_dynamic_module_callback_tracer_define_gauge( + h.hostConfigPtr, stringToModuleBuffer(name), + bufferSlicePtr(labels), C.size_t(len(labels)), &id, + ) + runtime.KeepAlive(name) + runtime.KeepAlive(labelNames) + runtime.KeepAlive(labels) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymTracerConfigHandle) SetGauge(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_tracer_set_gauge( + h.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +func (h *dymTracerConfigHandle) DefineHistogram(name string, labelNames []string) (shared.MetricID, shared.MetricsResult) { + labels := stringSlicesToModuleBuffers(labelNames) + var id C.size_t + ret := C.envoy_dynamic_module_callback_tracer_define_histogram( + h.hostConfigPtr, stringToModuleBuffer(name), + bufferSlicePtr(labels), C.size_t(len(labels)), &id, + ) + runtime.KeepAlive(name) + runtime.KeepAlive(labelNames) + runtime.KeepAlive(labels) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymTracerConfigHandle) RecordHistogramValue(id shared.MetricID, labelValues []string, value uint64) shared.MetricsResult { + labels := stringSlicesToModuleBuffers(labelValues) + ret := C.envoy_dynamic_module_callback_tracer_record_histogram_value( + h.hostConfigPtr, C.size_t(uint64(id)), + bufferSlicePtr(labels), C.size_t(len(labels)), + C.uint64_t(value), + ) + runtime.KeepAlive(labelValues) + runtime.KeepAlive(labels) + return shared.MetricsResult(ret) +} + +// dymTracerSpanContext implements shared.TracerSpanContext. It is bound to a single StartSpan +// or InjectContext call. +type dymTracerSpanContext struct { + hostSpanPtr C.envoy_dynamic_module_type_tracer_span_envoy_ptr +} + +func (c *dymTracerSpanContext) GetTraceContextValue(key string) (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ret := C.envoy_dynamic_module_callback_tracer_get_trace_context_value( + c.hostSpanPtr, stringToModuleBuffer(key), &buf) + runtime.KeepAlive(key) + if !bool(ret) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ret) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func (c *dymTracerSpanContext) SetTraceContextValue(key, value string) { + C.envoy_dynamic_module_callback_tracer_set_trace_context_value( + c.hostSpanPtr, stringToModuleBuffer(key), stringToModuleBuffer(value)) + runtime.KeepAlive(key) + runtime.KeepAlive(value) +} + +func (c *dymTracerSpanContext) RemoveTraceContextValue(key string) { + C.envoy_dynamic_module_callback_tracer_remove_trace_context_value( + c.hostSpanPtr, stringToModuleBuffer(key)) + runtime.KeepAlive(key) +} + +func (c *dymTracerSpanContext) GetTraceContextProtocol() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ret := C.envoy_dynamic_module_callback_tracer_get_trace_context_protocol(c.hostSpanPtr, &buf) + if !bool(ret) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ret) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func (c *dymTracerSpanContext) GetTraceContextHost() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ret := C.envoy_dynamic_module_callback_tracer_get_trace_context_host(c.hostSpanPtr, &buf) + if !bool(ret) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ret) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func (c *dymTracerSpanContext) GetTraceContextPath() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ret := C.envoy_dynamic_module_callback_tracer_get_trace_context_path(c.hostSpanPtr, &buf) + if !bool(ret) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ret) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +func (c *dymTracerSpanContext) GetTraceContextMethod() (shared.UnsafeEnvoyBuffer, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + ret := C.envoy_dynamic_module_callback_tracer_get_trace_context_method(c.hostSpanPtr, &buf) + if !bool(ret) || buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, bool(ret) + } + return envoyBufferToUnsafeEnvoyBuffer(buf), true +} + +// ============================================================================= +// Event hooks - tracer config +// ============================================================================= + +//export envoy_dynamic_module_on_tracer_config_new +func envoy_dynamic_module_on_tracer_config_new( + hostConfigPtr C.envoy_dynamic_module_type_tracer_config_envoy_ptr, + name C.envoy_dynamic_module_type_envoy_buffer, + config C.envoy_dynamic_module_type_envoy_buffer, +) C.envoy_dynamic_module_type_tracer_config_module_ptr { + nameStr := envoyBufferToStringUnsafe(name) + configBytes := envoyBufferToBytesCopy(config) + + configHandle := &dymTracerConfigHandle{hostConfigPtr: hostConfigPtr} + configFactory := sdk.GetTracerConfigFactory(nameStr) + if configFactory == nil { + hostLog(shared.LogLevelWarn, "Failed to load tracer configuration for %q: no factory registered", []any{nameStr}) + return nil + } + t, err := configFactory.Create(configHandle, configBytes) + if err != nil { + hostLog(shared.LogLevelWarn, "Failed to load tracer configuration for %q: %v", []any{nameStr, err}) + return nil + } + if t == nil { + hostLog(shared.LogLevelWarn, "Failed to load tracer configuration for %q: factory returned nil", []any{nameStr}) + return nil + } + wrapper := &tracerConfigWrapper{tracer: t, configHandle: configHandle} + configPtr := tracerConfigManager.record(wrapper) + return C.envoy_dynamic_module_type_tracer_config_module_ptr(configPtr) +} + +//export envoy_dynamic_module_on_tracer_config_destroy +func envoy_dynamic_module_on_tracer_config_destroy( + configPtr C.envoy_dynamic_module_type_tracer_config_module_ptr, +) { + w := tracerConfigManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil || w.destroyed { + return + } + w.destroyed = true + if w.tracer != nil { + w.tracer.OnDestroy() + } + tracerConfigManager.remove(unsafe.Pointer(configPtr)) +} + +//export envoy_dynamic_module_on_tracer_start_span +func envoy_dynamic_module_on_tracer_start_span( + configPtr C.envoy_dynamic_module_type_tracer_config_module_ptr, + hostSpanPtr C.envoy_dynamic_module_type_tracer_span_envoy_ptr, + operationName C.envoy_dynamic_module_type_envoy_buffer, + traced C.bool, + reason C.envoy_dynamic_module_type_trace_reason, +) C.envoy_dynamic_module_type_tracer_span_module_ptr { + cfg := tracerConfigManager.unwrap(unsafe.Pointer(configPtr)) + if cfg == nil || cfg.tracer == nil || cfg.destroyed { + return nil + } + ctx := &dymTracerSpanContext{hostSpanPtr: hostSpanPtr} + span := cfg.tracer.StartSpan(ctx, envoyBufferToStringUnsafe(operationName), bool(traced), shared.TraceReason(reason)) + if span == nil { + return nil + } + wrapper := &tracerSpanWrapper{span: span} + spanPtr := tracerSpanManager.record(wrapper) + return C.envoy_dynamic_module_type_tracer_span_module_ptr(spanPtr) +} + +// ============================================================================= +// Event hooks - span +// ============================================================================= + +//export envoy_dynamic_module_on_tracer_span_set_operation +func envoy_dynamic_module_on_tracer_span_set_operation( + spanPtr C.envoy_dynamic_module_type_tracer_span_module_ptr, + operation C.envoy_dynamic_module_type_envoy_buffer, +) { + w := tracerSpanManager.unwrap(unsafe.Pointer(spanPtr)) + if w == nil || w.span == nil || w.destroyed { + return + } + w.span.SetOperation(envoyBufferToStringUnsafe(operation)) +} + +//export envoy_dynamic_module_on_tracer_span_set_tag +func envoy_dynamic_module_on_tracer_span_set_tag( + spanPtr C.envoy_dynamic_module_type_tracer_span_module_ptr, + key C.envoy_dynamic_module_type_envoy_buffer, + value C.envoy_dynamic_module_type_envoy_buffer, +) { + w := tracerSpanManager.unwrap(unsafe.Pointer(spanPtr)) + if w == nil || w.span == nil || w.destroyed { + return + } + w.span.SetTag(envoyBufferToStringUnsafe(key), envoyBufferToStringUnsafe(value)) +} + +//export envoy_dynamic_module_on_tracer_span_log +func envoy_dynamic_module_on_tracer_span_log( + spanPtr C.envoy_dynamic_module_type_tracer_span_module_ptr, + timestampNs C.int64_t, + event C.envoy_dynamic_module_type_envoy_buffer, +) { + w := tracerSpanManager.unwrap(unsafe.Pointer(spanPtr)) + if w == nil || w.span == nil || w.destroyed { + return + } + w.span.Log(int64(timestampNs), envoyBufferToStringUnsafe(event)) +} + +//export envoy_dynamic_module_on_tracer_span_finish +func envoy_dynamic_module_on_tracer_span_finish( + spanPtr C.envoy_dynamic_module_type_tracer_span_module_ptr, +) { + w := tracerSpanManager.unwrap(unsafe.Pointer(spanPtr)) + if w == nil || w.span == nil || w.destroyed { + return + } + w.span.Finish() +} + +//export envoy_dynamic_module_on_tracer_span_inject_context +func envoy_dynamic_module_on_tracer_span_inject_context( + spanPtr C.envoy_dynamic_module_type_tracer_span_module_ptr, + hostSpanPtr C.envoy_dynamic_module_type_tracer_span_envoy_ptr, +) { + w := tracerSpanManager.unwrap(unsafe.Pointer(spanPtr)) + if w == nil || w.span == nil || w.destroyed { + return + } + ctx := &dymTracerSpanContext{hostSpanPtr: hostSpanPtr} + w.span.InjectContext(ctx) +} + +//export envoy_dynamic_module_on_tracer_span_spawn_child +func envoy_dynamic_module_on_tracer_span_spawn_child( + spanPtr C.envoy_dynamic_module_type_tracer_span_module_ptr, + name C.envoy_dynamic_module_type_envoy_buffer, + startTimeNs C.int64_t, +) C.envoy_dynamic_module_type_tracer_span_module_ptr { + w := tracerSpanManager.unwrap(unsafe.Pointer(spanPtr)) + if w == nil || w.span == nil || w.destroyed { + return nil + } + child := w.span.SpawnChild(envoyBufferToStringUnsafe(name), int64(startTimeNs)) + if child == nil { + return nil + } + wrapper := &tracerSpanWrapper{span: child} + childPtr := tracerSpanManager.record(wrapper) + return C.envoy_dynamic_module_type_tracer_span_module_ptr(childPtr) +} + +//export envoy_dynamic_module_on_tracer_span_set_sampled +func envoy_dynamic_module_on_tracer_span_set_sampled( + spanPtr C.envoy_dynamic_module_type_tracer_span_module_ptr, + sampled C.bool, +) { + w := tracerSpanManager.unwrap(unsafe.Pointer(spanPtr)) + if w == nil || w.span == nil || w.destroyed { + return + } + w.span.SetSampled(bool(sampled)) +} + +//export envoy_dynamic_module_on_tracer_span_use_local_decision +func envoy_dynamic_module_on_tracer_span_use_local_decision( + spanPtr C.envoy_dynamic_module_type_tracer_span_module_ptr, +) C.bool { + w := tracerSpanManager.unwrap(unsafe.Pointer(spanPtr)) + if w == nil || w.span == nil || w.destroyed { + return true + } + return C.bool(w.span.UseLocalDecision()) +} + +//export envoy_dynamic_module_on_tracer_span_get_baggage +func envoy_dynamic_module_on_tracer_span_get_baggage( + spanPtr C.envoy_dynamic_module_type_tracer_span_module_ptr, + key C.envoy_dynamic_module_type_envoy_buffer, + valueOut *C.envoy_dynamic_module_type_module_buffer, +) C.bool { + w := tracerSpanManager.unwrap(unsafe.Pointer(spanPtr)) + if w == nil || w.span == nil || w.destroyed { + return false + } + value, ok := w.span.GetBaggage(envoyBufferToStringUnsafe(key)) + if !ok { + valueOut.ptr = nil + valueOut.length = 0 + return false + } + w.baggageCache = value + if len(value) == 0 { + valueOut.ptr = nil + valueOut.length = 0 + } else { + valueOut.ptr = (*C.char)(unsafe.Pointer(unsafe.SliceData(value))) + valueOut.length = C.size_t(len(value)) + } + return true +} + +//export envoy_dynamic_module_on_tracer_span_set_baggage +func envoy_dynamic_module_on_tracer_span_set_baggage( + spanPtr C.envoy_dynamic_module_type_tracer_span_module_ptr, + key C.envoy_dynamic_module_type_envoy_buffer, + value C.envoy_dynamic_module_type_envoy_buffer, +) { + w := tracerSpanManager.unwrap(unsafe.Pointer(spanPtr)) + if w == nil || w.span == nil || w.destroyed { + return + } + w.span.SetBaggage(envoyBufferToStringUnsafe(key), envoyBufferToStringUnsafe(value)) +} + +//export envoy_dynamic_module_on_tracer_span_get_trace_id +func envoy_dynamic_module_on_tracer_span_get_trace_id( + spanPtr C.envoy_dynamic_module_type_tracer_span_module_ptr, + valueOut *C.envoy_dynamic_module_type_module_buffer, +) C.bool { + w := tracerSpanManager.unwrap(unsafe.Pointer(spanPtr)) + if w == nil || w.span == nil || w.destroyed { + return false + } + value, ok := w.span.GetTraceID() + if !ok { + valueOut.ptr = nil + valueOut.length = 0 + return false + } + w.traceIDCache = value + if len(value) == 0 { + valueOut.ptr = nil + valueOut.length = 0 + } else { + valueOut.ptr = (*C.char)(unsafe.Pointer(unsafe.SliceData(value))) + valueOut.length = C.size_t(len(value)) + } + return true +} + +//export envoy_dynamic_module_on_tracer_span_get_span_id +func envoy_dynamic_module_on_tracer_span_get_span_id( + spanPtr C.envoy_dynamic_module_type_tracer_span_module_ptr, + valueOut *C.envoy_dynamic_module_type_module_buffer, +) C.bool { + w := tracerSpanManager.unwrap(unsafe.Pointer(spanPtr)) + if w == nil || w.span == nil || w.destroyed { + return false + } + value, ok := w.span.GetSpanID() + if !ok { + valueOut.ptr = nil + valueOut.length = 0 + return false + } + w.spanIDCache = value + if len(value) == 0 { + valueOut.ptr = nil + valueOut.length = 0 + } else { + valueOut.ptr = (*C.char)(unsafe.Pointer(unsafe.SliceData(value))) + valueOut.length = C.size_t(len(value)) + } + return true +} + +//export envoy_dynamic_module_on_tracer_span_destroy +func envoy_dynamic_module_on_tracer_span_destroy( + spanPtr C.envoy_dynamic_module_type_tracer_span_module_ptr, +) { + w := tracerSpanManager.unwrap(unsafe.Pointer(spanPtr)) + if w == nil || w.destroyed { + return + } + w.destroyed = true + if w.span != nil { + w.span.OnDestroy() + } + tracerSpanManager.remove(unsafe.Pointer(spanPtr)) +} diff --git a/source/extensions/dynamic_modules/sdk/go/abi/transport_socket.go b/source/extensions/dynamic_modules/sdk/go/abi/transport_socket.go new file mode 100644 index 0000000000000..4810e364e5eaa --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/abi/transport_socket.go @@ -0,0 +1,372 @@ +package abi + +/* +#cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup +#cgo linux LDFLAGS: -Wl,--unresolved-symbols=ignore-all +#include +#include +#include +#include +#include "../../../abi/abi.h" +*/ +import "C" +import ( + "runtime" + "unsafe" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +type tsFactoryConfigWrapper struct { + factory shared.TransportSocketFactory + + // destroyed is set during factory_config_destroy so any late TransportSocket creation + // becomes a no-op instead of re-entering user code. + destroyed bool +} + +type tsSocketWrapper struct { + socket shared.TransportSocket + + // Memory caches that keep Go slices alive while their pointers cross the ABI for the + // duration of GetProtocol / GetFailureReason callbacks. + protocolCache []byte + failureCache []byte + + // destroyed is set during transport_socket_destroy so any late socket callbacks + // (do_handshake, do_read, etc.) become no-ops. + destroyed bool +} + +var tsFactoryConfigManager = newManager[tsFactoryConfigWrapper]() +var tsSocketManager = newManager[tsSocketWrapper]() + +// dymTransportSocketHandle implements shared.TransportSocketHandle. +type dymTransportSocketHandle struct { + hostSocketPtr C.envoy_dynamic_module_type_transport_socket_envoy_ptr +} + +func (h *dymTransportSocketHandle) IoHandleRead(buffer []byte) (int64, int64) { + if len(buffer) == 0 { + return 0, 0 + } + io := C.envoy_dynamic_module_callback_transport_socket_get_io_handle(h.hostSocketPtr) + if io == nil { + return 0, -1 + } + var bytesRead C.size_t + rc := C.envoy_dynamic_module_callback_transport_socket_io_handle_read( + io, + (*C.char)(unsafe.Pointer(unsafe.SliceData(buffer))), + C.size_t(len(buffer)), + &bytesRead, + ) + runtime.KeepAlive(buffer) + return int64(bytesRead), int64(rc) +} + +func (h *dymTransportSocketHandle) IoHandleWrite(buffer []byte) (int64, int64) { + if len(buffer) == 0 { + return 0, 0 + } + io := C.envoy_dynamic_module_callback_transport_socket_get_io_handle(h.hostSocketPtr) + if io == nil { + return 0, -1 + } + var bytesWritten C.size_t + rc := C.envoy_dynamic_module_callback_transport_socket_io_handle_write( + io, + (*C.char)(unsafe.Pointer(unsafe.SliceData(buffer))), + C.size_t(len(buffer)), + &bytesWritten, + ) + runtime.KeepAlive(buffer) + return int64(bytesWritten), int64(rc) +} + +func (h *dymTransportSocketHandle) IoHandleFD() int32 { + io := C.envoy_dynamic_module_callback_transport_socket_get_io_handle(h.hostSocketPtr) + if io == nil { + return -1 + } + return int32(C.envoy_dynamic_module_callback_transport_socket_io_handle_fd(io)) +} + +func (h *dymTransportSocketHandle) DrainReadBuffer(length uint64) { + C.envoy_dynamic_module_callback_transport_socket_read_buffer_drain(h.hostSocketPtr, C.size_t(length)) +} + +func (h *dymTransportSocketHandle) AddToReadBuffer(data []byte) { + if len(data) == 0 { + return + } + C.envoy_dynamic_module_callback_transport_socket_read_buffer_add( + h.hostSocketPtr, + (*C.char)(unsafe.Pointer(unsafe.SliceData(data))), + C.size_t(len(data)), + ) + runtime.KeepAlive(data) +} + +func (h *dymTransportSocketHandle) ReadBufferLength() uint64 { + return uint64(C.envoy_dynamic_module_callback_transport_socket_read_buffer_length(h.hostSocketPtr)) +} + +func (h *dymTransportSocketHandle) DrainWriteBuffer(length uint64) { + C.envoy_dynamic_module_callback_transport_socket_write_buffer_drain(h.hostSocketPtr, C.size_t(length)) +} + +func (h *dymTransportSocketHandle) GetWriteBufferSlices() []shared.UnsafeEnvoyBuffer { + // Query mode: pass slices=NULL to get the count. + var count C.size_t + C.envoy_dynamic_module_callback_transport_socket_write_buffer_get_slices(h.hostSocketPtr, nil, &count) + if count == 0 { + return nil + } + bufs := make([]C.envoy_dynamic_module_type_envoy_buffer, int(count)) + C.envoy_dynamic_module_callback_transport_socket_write_buffer_get_slices( + h.hostSocketPtr, unsafe.SliceData(bufs), &count) + out := envoyBufferSliceToUnsafeEnvoyBufferSlice(bufs[:int(count)]) + runtime.KeepAlive(bufs) + return out +} + +func (h *dymTransportSocketHandle) WriteBufferLength() uint64 { + return uint64(C.envoy_dynamic_module_callback_transport_socket_write_buffer_length(h.hostSocketPtr)) +} + +func (h *dymTransportSocketHandle) RaiseEvent(event shared.NetworkConnectionEvent) { + C.envoy_dynamic_module_callback_transport_socket_raise_event( + h.hostSocketPtr, C.envoy_dynamic_module_type_network_connection_event(event)) +} + +func (h *dymTransportSocketHandle) ShouldDrainReadBuffer() bool { + return bool(C.envoy_dynamic_module_callback_transport_socket_should_drain_read_buffer(h.hostSocketPtr)) +} + +func (h *dymTransportSocketHandle) SetIsReadable() { + C.envoy_dynamic_module_callback_transport_socket_set_is_readable(h.hostSocketPtr) +} + +func (h *dymTransportSocketHandle) FlushWriteBuffer() { + C.envoy_dynamic_module_callback_transport_socket_flush_write_buffer(h.hostSocketPtr) +} + +// ============================================================================= +// Event hooks +// ============================================================================= + +//export envoy_dynamic_module_on_transport_socket_factory_config_new +func envoy_dynamic_module_on_transport_socket_factory_config_new( + _ C.envoy_dynamic_module_type_transport_socket_factory_config_envoy_ptr, + socketName C.envoy_dynamic_module_type_envoy_buffer, + socketConfig C.envoy_dynamic_module_type_envoy_buffer, + isUpstream C.bool, +) C.envoy_dynamic_module_type_transport_socket_factory_config_module_ptr { + nameStr := envoyBufferToStringUnsafe(socketName) + configBytes := envoyBufferToBytesCopy(socketConfig) + + configFactory := sdk.GetTransportSocketFactoryConfigFactory(nameStr) + if configFactory == nil { + hostLog(shared.LogLevelWarn, "Failed to load transport socket configuration for %q: no factory registered", []any{nameStr}) + return nil + } + factory, err := configFactory.Create(nameStr, configBytes, bool(isUpstream)) + if err != nil { + hostLog(shared.LogLevelWarn, "Failed to load transport socket configuration for %q: %v", []any{nameStr, err}) + return nil + } + if factory == nil { + hostLog(shared.LogLevelWarn, "Failed to load transport socket configuration for %q: factory returned nil", []any{nameStr}) + return nil + } + wrapper := &tsFactoryConfigWrapper{factory: factory} + configPtr := tsFactoryConfigManager.record(wrapper) + return C.envoy_dynamic_module_type_transport_socket_factory_config_module_ptr(configPtr) +} + +//export envoy_dynamic_module_on_transport_socket_factory_config_destroy +func envoy_dynamic_module_on_transport_socket_factory_config_destroy( + configPtr C.envoy_dynamic_module_type_transport_socket_factory_config_module_ptr, +) { + w := tsFactoryConfigManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil || w.destroyed { + return + } + w.destroyed = true + w.factory.OnDestroy() + tsFactoryConfigManager.remove(unsafe.Pointer(configPtr)) +} + +//export envoy_dynamic_module_on_transport_socket_new +func envoy_dynamic_module_on_transport_socket_new( + configPtr C.envoy_dynamic_module_type_transport_socket_factory_config_module_ptr, + hostSocketPtr C.envoy_dynamic_module_type_transport_socket_envoy_ptr, +) C.envoy_dynamic_module_type_transport_socket_module_ptr { + cfg := tsFactoryConfigManager.unwrap(unsafe.Pointer(configPtr)) + if cfg == nil || cfg.destroyed { + return nil + } + handle := &dymTransportSocketHandle{hostSocketPtr: hostSocketPtr} + socket := cfg.factory.Create(handle) + if socket == nil { + return nil + } + wrapper := &tsSocketWrapper{socket: socket} + socketPtr := tsSocketManager.record(wrapper) + return C.envoy_dynamic_module_type_transport_socket_module_ptr(socketPtr) +} + +//export envoy_dynamic_module_on_transport_socket_destroy +func envoy_dynamic_module_on_transport_socket_destroy( + socketPtr C.envoy_dynamic_module_type_transport_socket_module_ptr, +) { + w := tsSocketManager.unwrap(unsafe.Pointer(socketPtr)) + if w == nil || w.destroyed { + return + } + w.destroyed = true + if w.socket != nil { + w.socket.OnDestroy() + } + tsSocketManager.remove(unsafe.Pointer(socketPtr)) +} + +//export envoy_dynamic_module_on_transport_socket_set_callbacks +func envoy_dynamic_module_on_transport_socket_set_callbacks( + hostSocketPtr C.envoy_dynamic_module_type_transport_socket_envoy_ptr, + socketPtr C.envoy_dynamic_module_type_transport_socket_module_ptr, +) { + w := tsSocketManager.unwrap(unsafe.Pointer(socketPtr)) + if w == nil || w.socket == nil || w.destroyed { + return + } + handle := &dymTransportSocketHandle{hostSocketPtr: hostSocketPtr} + w.socket.SetCallbacks(handle) +} + +//export envoy_dynamic_module_on_transport_socket_on_connected +func envoy_dynamic_module_on_transport_socket_on_connected( + hostSocketPtr C.envoy_dynamic_module_type_transport_socket_envoy_ptr, + socketPtr C.envoy_dynamic_module_type_transport_socket_module_ptr, +) { + w := tsSocketManager.unwrap(unsafe.Pointer(socketPtr)) + if w == nil || w.socket == nil || w.destroyed { + return + } + handle := &dymTransportSocketHandle{hostSocketPtr: hostSocketPtr} + w.socket.OnConnected(handle) +} + +//export envoy_dynamic_module_on_transport_socket_do_read +func envoy_dynamic_module_on_transport_socket_do_read( + hostSocketPtr C.envoy_dynamic_module_type_transport_socket_envoy_ptr, + socketPtr C.envoy_dynamic_module_type_transport_socket_module_ptr, +) C.envoy_dynamic_module_type_transport_socket_io_result { + w := tsSocketManager.unwrap(unsafe.Pointer(socketPtr)) + if w == nil || w.socket == nil || w.destroyed { + return C.envoy_dynamic_module_type_transport_socket_io_result{ + action: C.envoy_dynamic_module_type_transport_socket_post_io_action_Close, + } + } + handle := &dymTransportSocketHandle{hostSocketPtr: hostSocketPtr} + r := w.socket.DoRead(handle) + return C.envoy_dynamic_module_type_transport_socket_io_result{ + action: C.envoy_dynamic_module_type_transport_socket_post_io_action(r.Action), + bytes_processed: C.uint64_t(r.BytesProcessed), + end_stream_read: C.bool(r.EndStreamRead), + } +} + +//export envoy_dynamic_module_on_transport_socket_do_write +func envoy_dynamic_module_on_transport_socket_do_write( + hostSocketPtr C.envoy_dynamic_module_type_transport_socket_envoy_ptr, + socketPtr C.envoy_dynamic_module_type_transport_socket_module_ptr, + writeBufferLength C.size_t, + endStream C.bool, +) C.envoy_dynamic_module_type_transport_socket_io_result { + w := tsSocketManager.unwrap(unsafe.Pointer(socketPtr)) + if w == nil || w.socket == nil || w.destroyed { + return C.envoy_dynamic_module_type_transport_socket_io_result{ + action: C.envoy_dynamic_module_type_transport_socket_post_io_action_Close, + } + } + handle := &dymTransportSocketHandle{hostSocketPtr: hostSocketPtr} + r := w.socket.DoWrite(handle, uint64(writeBufferLength), bool(endStream)) + return C.envoy_dynamic_module_type_transport_socket_io_result{ + action: C.envoy_dynamic_module_type_transport_socket_post_io_action(r.Action), + bytes_processed: C.uint64_t(r.BytesProcessed), + end_stream_read: C.bool(r.EndStreamRead), + } +} + +//export envoy_dynamic_module_on_transport_socket_close +func envoy_dynamic_module_on_transport_socket_close( + hostSocketPtr C.envoy_dynamic_module_type_transport_socket_envoy_ptr, + socketPtr C.envoy_dynamic_module_type_transport_socket_module_ptr, + event C.envoy_dynamic_module_type_network_connection_event, +) { + w := tsSocketManager.unwrap(unsafe.Pointer(socketPtr)) + if w == nil || w.socket == nil || w.destroyed { + return + } + handle := &dymTransportSocketHandle{hostSocketPtr: hostSocketPtr} + w.socket.OnClose(handle, shared.NetworkConnectionEvent(event)) +} + +//export envoy_dynamic_module_on_transport_socket_get_protocol +func envoy_dynamic_module_on_transport_socket_get_protocol( + _ C.envoy_dynamic_module_type_transport_socket_envoy_ptr, + socketPtr C.envoy_dynamic_module_type_transport_socket_module_ptr, + result *C.envoy_dynamic_module_type_module_buffer, +) { + w := tsSocketManager.unwrap(unsafe.Pointer(socketPtr)) + if w == nil || w.socket == nil || w.destroyed { + result.ptr = nil + result.length = 0 + return + } + w.protocolCache = w.socket.GetProtocol() + if len(w.protocolCache) == 0 { + result.ptr = nil + result.length = 0 + return + } + result.ptr = (*C.char)(unsafe.Pointer(unsafe.SliceData(w.protocolCache))) + result.length = C.size_t(len(w.protocolCache)) +} + +//export envoy_dynamic_module_on_transport_socket_get_failure_reason +func envoy_dynamic_module_on_transport_socket_get_failure_reason( + _ C.envoy_dynamic_module_type_transport_socket_envoy_ptr, + socketPtr C.envoy_dynamic_module_type_transport_socket_module_ptr, + result *C.envoy_dynamic_module_type_module_buffer, +) { + w := tsSocketManager.unwrap(unsafe.Pointer(socketPtr)) + if w == nil || w.socket == nil || w.destroyed { + result.ptr = nil + result.length = 0 + return + } + w.failureCache = w.socket.GetFailureReason() + if len(w.failureCache) == 0 { + result.ptr = nil + result.length = 0 + return + } + result.ptr = (*C.char)(unsafe.Pointer(unsafe.SliceData(w.failureCache))) + result.length = C.size_t(len(w.failureCache)) +} + +//export envoy_dynamic_module_on_transport_socket_can_flush_close +func envoy_dynamic_module_on_transport_socket_can_flush_close( + _ C.envoy_dynamic_module_type_transport_socket_envoy_ptr, + socketPtr C.envoy_dynamic_module_type_transport_socket_module_ptr, +) C.bool { + w := tsSocketManager.unwrap(unsafe.Pointer(socketPtr)) + if w == nil || w.socket == nil || w.destroyed { + return true + } + return C.bool(w.socket.CanFlushClose()) +} diff --git a/source/extensions/dynamic_modules/sdk/go/abi/udp_listener.go b/source/extensions/dynamic_modules/sdk/go/abi/udp_listener.go new file mode 100644 index 0000000000000..b2fd433a7c381 --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/abi/udp_listener.go @@ -0,0 +1,241 @@ +package abi + +/* +#cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup +#cgo linux LDFLAGS: -Wl,--unresolved-symbols=ignore-all +#include +#include +#include +#include +#include "../../../abi/abi.h" +*/ +import "C" +import ( + "runtime" + "unsafe" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +type udpListenerFilterConfigWrapper struct { + factory shared.UdpListenerFilterFactory + configHandle *dymUdpListenerConfigHandle +} + +var udpListenerConfigManager = newManager[udpListenerFilterConfigWrapper]() +var udpListenerFilterManager = newManager[dymUdpListenerFilterHandle]() + +// dymUdpListenerConfigHandle implements shared.UdpListenerFilterConfigHandle. +type dymUdpListenerConfigHandle struct { + hostConfigPtr C.envoy_dynamic_module_type_udp_listener_filter_config_envoy_ptr +} + +func (h *dymUdpListenerConfigHandle) DefineCounter(name string) (shared.MetricID, shared.MetricsResult) { + var id C.size_t + ret := C.envoy_dynamic_module_callback_udp_listener_filter_config_define_counter( + h.hostConfigPtr, stringToModuleBuffer(name), &id) + runtime.KeepAlive(name) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymUdpListenerConfigHandle) DefineGauge(name string) (shared.MetricID, shared.MetricsResult) { + var id C.size_t + ret := C.envoy_dynamic_module_callback_udp_listener_filter_config_define_gauge( + h.hostConfigPtr, stringToModuleBuffer(name), &id) + runtime.KeepAlive(name) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +func (h *dymUdpListenerConfigHandle) DefineHistogram(name string) (shared.MetricID, shared.MetricsResult) { + var id C.size_t + ret := C.envoy_dynamic_module_callback_udp_listener_filter_config_define_histogram( + h.hostConfigPtr, stringToModuleBuffer(name), &id) + runtime.KeepAlive(name) + return shared.MetricID(id), shared.MetricsResult(ret) +} + +// dymUdpListenerFilterHandle implements shared.UdpListenerFilterHandle. +type dymUdpListenerFilterHandle struct { + hostFilterPtr C.envoy_dynamic_module_type_udp_listener_filter_envoy_ptr + + plugin shared.UdpListenerFilter + destroyed bool +} + +func (h *dymUdpListenerFilterHandle) GetDatagramData() []shared.UnsafeEnvoyBuffer { + size := C.envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks_size(h.hostFilterPtr) + if size == 0 { + return nil + } + bufs := make([]C.envoy_dynamic_module_type_envoy_buffer, int(size)) + if !bool(C.envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_chunks( + h.hostFilterPtr, unsafe.SliceData(bufs))) { + return nil + } + out := envoyBufferSliceToUnsafeEnvoyBufferSlice(bufs) + runtime.KeepAlive(bufs) + return out +} + +func (h *dymUdpListenerFilterHandle) GetDatagramSize() uint64 { + return uint64(C.envoy_dynamic_module_callback_udp_listener_filter_get_datagram_data_size(h.hostFilterPtr)) +} + +func (h *dymUdpListenerFilterHandle) SetDatagramData(data []byte) bool { + ret := C.envoy_dynamic_module_callback_udp_listener_filter_set_datagram_data( + h.hostFilterPtr, bytesToModuleBuffer(data)) + runtime.KeepAlive(data) + return bool(ret) +} + +func (h *dymUdpListenerFilterHandle) GetPeerAddress() (shared.UnsafeEnvoyBuffer, uint32, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var port C.uint32_t + ok := C.envoy_dynamic_module_callback_udp_listener_filter_get_peer_address(h.hostFilterPtr, &buf, &port) + if !bool(ok) { + return shared.UnsafeEnvoyBuffer{}, 0, false + } + return envoyBufferToUnsafeEnvoyBuffer(buf), uint32(port), true +} + +func (h *dymUdpListenerFilterHandle) GetLocalAddress() (shared.UnsafeEnvoyBuffer, uint32, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var port C.uint32_t + ok := C.envoy_dynamic_module_callback_udp_listener_filter_get_local_address(h.hostFilterPtr, &buf, &port) + if !bool(ok) { + return shared.UnsafeEnvoyBuffer{}, 0, false + } + return envoyBufferToUnsafeEnvoyBuffer(buf), uint32(port), true +} + +func (h *dymUdpListenerFilterHandle) SendDatagram(data []byte, peerAddress string, peerPort uint32) bool { + ret := C.envoy_dynamic_module_callback_udp_listener_filter_send_datagram( + h.hostFilterPtr, + bytesToModuleBuffer(data), + stringToModuleBuffer(peerAddress), + C.uint32_t(peerPort), + ) + runtime.KeepAlive(data) + runtime.KeepAlive(peerAddress) + return bool(ret) +} + +func (h *dymUdpListenerFilterHandle) IncrementCounter(id shared.MetricID, value uint64) shared.MetricsResult { + return shared.MetricsResult(C.envoy_dynamic_module_callback_udp_listener_filter_increment_counter( + h.hostFilterPtr, C.size_t(uint64(id)), C.uint64_t(value))) +} + +func (h *dymUdpListenerFilterHandle) SetGauge(id shared.MetricID, value uint64) shared.MetricsResult { + return shared.MetricsResult(C.envoy_dynamic_module_callback_udp_listener_filter_set_gauge( + h.hostFilterPtr, C.size_t(uint64(id)), C.uint64_t(value))) +} + +func (h *dymUdpListenerFilterHandle) IncrementGauge(id shared.MetricID, value uint64) shared.MetricsResult { + return shared.MetricsResult(C.envoy_dynamic_module_callback_udp_listener_filter_increment_gauge( + h.hostFilterPtr, C.size_t(uint64(id)), C.uint64_t(value))) +} + +func (h *dymUdpListenerFilterHandle) DecrementGauge(id shared.MetricID, value uint64) shared.MetricsResult { + return shared.MetricsResult(C.envoy_dynamic_module_callback_udp_listener_filter_decrement_gauge( + h.hostFilterPtr, C.size_t(uint64(id)), C.uint64_t(value))) +} + +func (h *dymUdpListenerFilterHandle) RecordHistogramValue(id shared.MetricID, value uint64) shared.MetricsResult { + return shared.MetricsResult(C.envoy_dynamic_module_callback_udp_listener_filter_record_histogram_value( + h.hostFilterPtr, C.size_t(uint64(id)), C.uint64_t(value))) +} + +func (h *dymUdpListenerFilterHandle) GetWorkerIndex() uint32 { + return uint32(C.envoy_dynamic_module_callback_udp_listener_filter_get_worker_index(h.hostFilterPtr)) +} + +// ============================================================================= +// Event hooks +// ============================================================================= + +//export envoy_dynamic_module_on_udp_listener_filter_config_new +func envoy_dynamic_module_on_udp_listener_filter_config_new( + hostConfigPtr C.envoy_dynamic_module_type_udp_listener_filter_config_envoy_ptr, + name C.envoy_dynamic_module_type_envoy_buffer, + config C.envoy_dynamic_module_type_envoy_buffer, +) C.envoy_dynamic_module_type_udp_listener_filter_config_module_ptr { + nameStr := envoyBufferToStringUnsafe(name) + configBytes := envoyBufferToBytesCopy(config) + + configHandle := &dymUdpListenerConfigHandle{hostConfigPtr: hostConfigPtr} + configFactory := sdk.GetUdpListenerFilterConfigFactory(nameStr) + if configFactory == nil { + hostLog(shared.LogLevelWarn, "Failed to load UDP listener filter configuration for %q: no factory registered", []any{nameStr}) + return nil + } + factory, err := configFactory.Create(configHandle, configBytes) + if err != nil { + hostLog(shared.LogLevelWarn, "Failed to load UDP listener filter configuration for %q: %v", []any{nameStr, err}) + return nil + } + if factory == nil { + hostLog(shared.LogLevelWarn, "Failed to load UDP listener filter configuration for %q: factory returned nil", []any{nameStr}) + return nil + } + wrapper := &udpListenerFilterConfigWrapper{factory: factory, configHandle: configHandle} + configPtr := udpListenerConfigManager.record(wrapper) + return C.envoy_dynamic_module_type_udp_listener_filter_config_module_ptr(configPtr) +} + +//export envoy_dynamic_module_on_udp_listener_filter_config_destroy +func envoy_dynamic_module_on_udp_listener_filter_config_destroy( + configPtr C.envoy_dynamic_module_type_udp_listener_filter_config_module_ptr, +) { + wrapper := udpListenerConfigManager.unwrap(unsafe.Pointer(configPtr)) + if wrapper == nil { + return + } + wrapper.factory.OnDestroy() + udpListenerConfigManager.remove(unsafe.Pointer(configPtr)) +} + +//export envoy_dynamic_module_on_udp_listener_filter_new +func envoy_dynamic_module_on_udp_listener_filter_new( + configPtr C.envoy_dynamic_module_type_udp_listener_filter_config_module_ptr, + hostFilterPtr C.envoy_dynamic_module_type_udp_listener_filter_envoy_ptr, +) C.envoy_dynamic_module_type_udp_listener_filter_module_ptr { + cfg := udpListenerConfigManager.unwrap(unsafe.Pointer(configPtr)) + if cfg == nil { + return nil + } + handle := &dymUdpListenerFilterHandle{hostFilterPtr: hostFilterPtr} + handle.plugin = cfg.factory.Create(handle) + if handle.plugin == nil { + return nil + } + filterPtr := udpListenerFilterManager.record(handle) + return C.envoy_dynamic_module_type_udp_listener_filter_module_ptr(filterPtr) +} + +//export envoy_dynamic_module_on_udp_listener_filter_on_data +func envoy_dynamic_module_on_udp_listener_filter_on_data( + _ C.envoy_dynamic_module_type_udp_listener_filter_envoy_ptr, + filterPtr C.envoy_dynamic_module_type_udp_listener_filter_module_ptr, +) C.envoy_dynamic_module_type_on_udp_listener_filter_status { + h := udpListenerFilterManager.unwrap(unsafe.Pointer(filterPtr)) + if h == nil || h.plugin == nil || h.destroyed { + return 0 + } + return C.envoy_dynamic_module_type_on_udp_listener_filter_status(h.plugin.OnData(h)) +} + +//export envoy_dynamic_module_on_udp_listener_filter_destroy +func envoy_dynamic_module_on_udp_listener_filter_destroy( + filterPtr C.envoy_dynamic_module_type_udp_listener_filter_module_ptr, +) { + h := udpListenerFilterManager.unwrap(unsafe.Pointer(filterPtr)) + if h == nil || h.destroyed { + return + } + h.destroyed = true + if h.plugin != nil { + h.plugin.OnDestroy() + } + udpListenerFilterManager.remove(unsafe.Pointer(filterPtr)) +} diff --git a/source/extensions/dynamic_modules/sdk/go/abi/upstream_http_tcp_bridge.go b/source/extensions/dynamic_modules/sdk/go/abi/upstream_http_tcp_bridge.go new file mode 100644 index 0000000000000..97cfaf409efa3 --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/abi/upstream_http_tcp_bridge.go @@ -0,0 +1,293 @@ +package abi + +/* +#cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup +#cgo linux LDFLAGS: -Wl,--unresolved-symbols=ignore-all +#include +#include +#include +#include +#include "../../../abi/abi.h" +*/ +import "C" +import ( + "runtime" + "unsafe" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +type uhtbConfigWrapper struct { + factory shared.UpstreamHttpTcpBridgeFactory +} + +type uhtbBridgeWrapper = dymUpstreamHttpTcpBridgeHandle + +var uhtbConfigManager = newManager[uhtbConfigWrapper]() +var uhtbBridgeManager = newManager[uhtbBridgeWrapper]() + +// dymUpstreamHttpTcpBridgeHandle implements shared.UpstreamHttpTcpBridgeHandle. +type dymUpstreamHttpTcpBridgeHandle struct { + hostBridgePtr C.envoy_dynamic_module_type_upstream_http_tcp_bridge_envoy_ptr + + bridge shared.UpstreamHttpTcpBridge + destroyed bool +} + +func (h *dymUpstreamHttpTcpBridgeHandle) GetRequestHeader(key string, index uint64) (shared.UnsafeEnvoyBuffer, uint64, bool) { + var buf C.envoy_dynamic_module_type_envoy_buffer + var total C.size_t + ret := C.envoy_dynamic_module_callback_upstream_http_tcp_bridge_get_request_header( + h.hostBridgePtr, stringToModuleBuffer(key), &buf, C.size_t(index), &total, + ) + runtime.KeepAlive(key) + if !bool(ret) { + return shared.UnsafeEnvoyBuffer{}, uint64(total), false + } + if buf.ptr == nil || buf.length == 0 { + return shared.UnsafeEnvoyBuffer{}, uint64(total), true + } + return envoyBufferToUnsafeEnvoyBuffer(buf), uint64(total), true +} + +func (h *dymUpstreamHttpTcpBridgeHandle) GetRequestHeadersSize() uint64 { + return uint64(C.envoy_dynamic_module_callback_upstream_http_tcp_bridge_get_request_headers_size(h.hostBridgePtr)) +} + +func (h *dymUpstreamHttpTcpBridgeHandle) GetRequestHeaders() [][2]shared.UnsafeEnvoyBuffer { + size := C.envoy_dynamic_module_callback_upstream_http_tcp_bridge_get_request_headers_size(h.hostBridgePtr) + if size == 0 { + return nil + } + hdrs := make([]C.envoy_dynamic_module_type_envoy_http_header, int(size)) + if !bool(C.envoy_dynamic_module_callback_upstream_http_tcp_bridge_get_request_headers( + h.hostBridgePtr, unsafe.SliceData(hdrs))) { + return nil + } + out := envoyHttpHeaderSliceToUnsafeHeaderSlice(hdrs) + runtime.KeepAlive(hdrs) + return out +} + +func (h *dymUpstreamHttpTcpBridgeHandle) getBufferChunks( + getFn func(C.envoy_dynamic_module_type_upstream_http_tcp_bridge_envoy_ptr, *C.envoy_dynamic_module_type_envoy_buffer, *C.size_t), +) []shared.UnsafeEnvoyBuffer { + // The ABI here returns chunks via an output buffer pointer + count. We call once to learn + // the count by passing a null buffer — Envoy fills only the count when buffer is null. + // Then allocate and call again. To avoid a second call, allocate a sized array based on + // a worst-case heuristic (16 chunks) and grow if needed. + buf := make([]C.envoy_dynamic_module_type_envoy_buffer, 16) + var count C.size_t = C.size_t(len(buf)) + getFn(h.hostBridgePtr, unsafe.SliceData(buf), &count) + if int(count) > len(buf) { + buf = make([]C.envoy_dynamic_module_type_envoy_buffer, int(count)) + count = C.size_t(len(buf)) + getFn(h.hostBridgePtr, unsafe.SliceData(buf), &count) + } + if count == 0 { + return nil + } + out := envoyBufferSliceToUnsafeEnvoyBufferSlice(buf[:int(count)]) + runtime.KeepAlive(buf) + return out +} + +func (h *dymUpstreamHttpTcpBridgeHandle) GetRequestBuffer() []shared.UnsafeEnvoyBuffer { + return h.getBufferChunks(func(p C.envoy_dynamic_module_type_upstream_http_tcp_bridge_envoy_ptr, buf *C.envoy_dynamic_module_type_envoy_buffer, n *C.size_t) { + C.envoy_dynamic_module_callback_upstream_http_tcp_bridge_get_request_buffer(p, buf, n) + }) +} + +func (h *dymUpstreamHttpTcpBridgeHandle) GetResponseBuffer() []shared.UnsafeEnvoyBuffer { + return h.getBufferChunks(func(p C.envoy_dynamic_module_type_upstream_http_tcp_bridge_envoy_ptr, buf *C.envoy_dynamic_module_type_envoy_buffer, n *C.size_t) { + C.envoy_dynamic_module_callback_upstream_http_tcp_bridge_get_response_buffer(p, buf, n) + }) +} + +func (h *dymUpstreamHttpTcpBridgeHandle) SendUpstreamData(data []byte, endOfStream bool) { + C.envoy_dynamic_module_callback_upstream_http_tcp_bridge_send_upstream_data( + h.hostBridgePtr, bytesToModuleBuffer(data), C.bool(endOfStream)) + runtime.KeepAlive(data) +} + +func (h *dymUpstreamHttpTcpBridgeHandle) SendResponse(statusCode uint32, headers [][2]string, body []byte) { + headerViews := headersToModuleHttpHeaderSlice(headers) + var headerPtr *C.envoy_dynamic_module_type_module_http_header + if len(headerViews) > 0 { + headerPtr = unsafe.SliceData(headerViews) + } + C.envoy_dynamic_module_callback_upstream_http_tcp_bridge_send_response( + h.hostBridgePtr, + C.uint32_t(statusCode), + headerPtr, + C.size_t(len(headerViews)), + bytesToModuleBuffer(body), + ) + runtime.KeepAlive(headers) + runtime.KeepAlive(headerViews) + runtime.KeepAlive(body) +} + +func (h *dymUpstreamHttpTcpBridgeHandle) SendResponseHeaders(statusCode uint32, headers [][2]string, endOfStream bool) { + headerViews := headersToModuleHttpHeaderSlice(headers) + var headerPtr *C.envoy_dynamic_module_type_module_http_header + if len(headerViews) > 0 { + headerPtr = unsafe.SliceData(headerViews) + } + C.envoy_dynamic_module_callback_upstream_http_tcp_bridge_send_response_headers( + h.hostBridgePtr, + C.uint32_t(statusCode), + headerPtr, + C.size_t(len(headerViews)), + C.bool(endOfStream), + ) + runtime.KeepAlive(headers) + runtime.KeepAlive(headerViews) +} + +func (h *dymUpstreamHttpTcpBridgeHandle) SendResponseData(data []byte, endOfStream bool) { + C.envoy_dynamic_module_callback_upstream_http_tcp_bridge_send_response_data( + h.hostBridgePtr, bytesToModuleBuffer(data), C.bool(endOfStream)) + runtime.KeepAlive(data) +} + +func (h *dymUpstreamHttpTcpBridgeHandle) SendResponseTrailers(trailers [][2]string) { + trailerViews := headersToModuleHttpHeaderSlice(trailers) + var trailerPtr *C.envoy_dynamic_module_type_module_http_header + if len(trailerViews) > 0 { + trailerPtr = unsafe.SliceData(trailerViews) + } + C.envoy_dynamic_module_callback_upstream_http_tcp_bridge_send_response_trailers( + h.hostBridgePtr, trailerPtr, C.size_t(len(trailerViews))) + runtime.KeepAlive(trailers) + runtime.KeepAlive(trailerViews) +} + +// ============================================================================= +// Event hooks +// ============================================================================= + +//export envoy_dynamic_module_on_upstream_http_tcp_bridge_config_new +func envoy_dynamic_module_on_upstream_http_tcp_bridge_config_new( + _ C.envoy_dynamic_module_type_upstream_http_tcp_bridge_config_envoy_ptr, + name C.envoy_dynamic_module_type_envoy_buffer, + config C.envoy_dynamic_module_type_envoy_buffer, +) C.envoy_dynamic_module_type_upstream_http_tcp_bridge_config_module_ptr { + nameStr := envoyBufferToStringUnsafe(name) + configBytes := envoyBufferToBytesCopy(config) + + configFactory := sdk.GetUpstreamHttpTcpBridgeConfigFactory(nameStr) + if configFactory == nil { + hostLog(shared.LogLevelWarn, "Failed to load upstream HTTP/TCP bridge configuration for %q: no factory registered", []any{nameStr}) + return nil + } + factory, err := configFactory.Create(nameStr, configBytes) + if err != nil { + hostLog(shared.LogLevelWarn, "Failed to load upstream HTTP/TCP bridge configuration for %q: %v", []any{nameStr, err}) + return nil + } + if factory == nil { + hostLog(shared.LogLevelWarn, "Failed to load upstream HTTP/TCP bridge configuration for %q: factory returned nil", []any{nameStr}) + return nil + } + wrapper := &uhtbConfigWrapper{factory: factory} + configPtr := uhtbConfigManager.record(wrapper) + return C.envoy_dynamic_module_type_upstream_http_tcp_bridge_config_module_ptr(configPtr) +} + +//export envoy_dynamic_module_on_upstream_http_tcp_bridge_config_destroy +func envoy_dynamic_module_on_upstream_http_tcp_bridge_config_destroy( + configPtr C.envoy_dynamic_module_type_upstream_http_tcp_bridge_config_module_ptr, +) { + w := uhtbConfigManager.unwrap(unsafe.Pointer(configPtr)) + if w == nil { + return + } + w.factory.OnDestroy() + uhtbConfigManager.remove(unsafe.Pointer(configPtr)) +} + +//export envoy_dynamic_module_on_upstream_http_tcp_bridge_new +func envoy_dynamic_module_on_upstream_http_tcp_bridge_new( + configPtr C.envoy_dynamic_module_type_upstream_http_tcp_bridge_config_module_ptr, + hostBridgePtr C.envoy_dynamic_module_type_upstream_http_tcp_bridge_envoy_ptr, +) C.envoy_dynamic_module_type_upstream_http_tcp_bridge_module_ptr { + cfg := uhtbConfigManager.unwrap(unsafe.Pointer(configPtr)) + if cfg == nil { + return nil + } + handle := &dymUpstreamHttpTcpBridgeHandle{hostBridgePtr: hostBridgePtr} + handle.bridge = cfg.factory.Create(handle) + if handle.bridge == nil { + return nil + } + bridgePtr := uhtbBridgeManager.record(handle) + return C.envoy_dynamic_module_type_upstream_http_tcp_bridge_module_ptr(bridgePtr) +} + +//export envoy_dynamic_module_on_upstream_http_tcp_bridge_encode_headers +func envoy_dynamic_module_on_upstream_http_tcp_bridge_encode_headers( + _ C.envoy_dynamic_module_type_upstream_http_tcp_bridge_envoy_ptr, + bridgePtr C.envoy_dynamic_module_type_upstream_http_tcp_bridge_module_ptr, + endOfStream C.bool, +) { + h := uhtbBridgeManager.unwrap(unsafe.Pointer(bridgePtr)) + if h == nil || h.bridge == nil || h.destroyed { + return + } + h.bridge.EncodeHeaders(h, bool(endOfStream)) +} + +//export envoy_dynamic_module_on_upstream_http_tcp_bridge_encode_data +func envoy_dynamic_module_on_upstream_http_tcp_bridge_encode_data( + _ C.envoy_dynamic_module_type_upstream_http_tcp_bridge_envoy_ptr, + bridgePtr C.envoy_dynamic_module_type_upstream_http_tcp_bridge_module_ptr, + endOfStream C.bool, +) { + h := uhtbBridgeManager.unwrap(unsafe.Pointer(bridgePtr)) + if h == nil || h.bridge == nil || h.destroyed { + return + } + h.bridge.EncodeData(h, bool(endOfStream)) +} + +//export envoy_dynamic_module_on_upstream_http_tcp_bridge_encode_trailers +func envoy_dynamic_module_on_upstream_http_tcp_bridge_encode_trailers( + _ C.envoy_dynamic_module_type_upstream_http_tcp_bridge_envoy_ptr, + bridgePtr C.envoy_dynamic_module_type_upstream_http_tcp_bridge_module_ptr, +) { + h := uhtbBridgeManager.unwrap(unsafe.Pointer(bridgePtr)) + if h == nil || h.bridge == nil || h.destroyed { + return + } + h.bridge.EncodeTrailers(h) +} + +//export envoy_dynamic_module_on_upstream_http_tcp_bridge_on_upstream_data +func envoy_dynamic_module_on_upstream_http_tcp_bridge_on_upstream_data( + _ C.envoy_dynamic_module_type_upstream_http_tcp_bridge_envoy_ptr, + bridgePtr C.envoy_dynamic_module_type_upstream_http_tcp_bridge_module_ptr, + endOfStream C.bool, +) { + h := uhtbBridgeManager.unwrap(unsafe.Pointer(bridgePtr)) + if h == nil || h.bridge == nil || h.destroyed { + return + } + h.bridge.OnUpstreamData(h, bool(endOfStream)) +} + +//export envoy_dynamic_module_on_upstream_http_tcp_bridge_destroy +func envoy_dynamic_module_on_upstream_http_tcp_bridge_destroy( + bridgePtr C.envoy_dynamic_module_type_upstream_http_tcp_bridge_module_ptr, +) { + h := uhtbBridgeManager.unwrap(unsafe.Pointer(bridgePtr)) + if h == nil || h.destroyed { + return + } + h.destroyed = true + if h.bridge != nil { + h.bridge.OnDestroy() + } + uhtbBridgeManager.remove(unsafe.Pointer(bridgePtr)) +} diff --git a/source/extensions/dynamic_modules/sdk/go/sdk.go b/source/extensions/dynamic_modules/sdk/go/sdk.go index 766fa4ed1d6b2..af1d8e50bcad2 100644 --- a/source/extensions/dynamic_modules/sdk/go/sdk.go +++ b/source/extensions/dynamic_modules/sdk/go/sdk.go @@ -2,6 +2,8 @@ package sdk import ( "fmt" + "sync/atomic" + "unsafe" "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" ) @@ -38,8 +40,12 @@ func RegisterHttpFilterConfigFactories(factories map[string]shared.HttpFilterCon } } +// --------------------------------------------------------------------------- +// Network filter registry +// --------------------------------------------------------------------------- + // NewNetworkFilterFactory creates a new network filter factory for the given plugin name and -// unparsed config. +// unparsed config. Returns an error if no factory is registered for name. func NewNetworkFilterFactory(handle shared.NetworkFilterConfigHandle, name string, unparsedConfig []byte) (shared.NetworkFilterFactory, error) { configFactory := networkFilterConfigFactoryRegistry[name] @@ -49,13 +55,14 @@ func NewNetworkFilterFactory(handle shared.NetworkFilterConfigHandle, name strin return configFactory.Create(handle, unparsedConfig) } -// GetNetworkFilterConfigFactory gets the network filter config factory for the given plugin name. +// GetNetworkFilterConfigFactory returns the registered network filter config factory for name, +// or nil if no factory is registered. func GetNetworkFilterConfigFactory(name string) shared.NetworkFilterConfigFactory { return networkFilterConfigFactoryRegistry[name] } -// RegisterNetworkFilterConfigFactories registers network filter config factories for plugins in the -// composer binary itself. This function MUST only be called from init() functions. +// RegisterNetworkFilterConfigFactories registers network filter config factories. MUST only be +// called from init() functions. func RegisterNetworkFilterConfigFactories(factories map[string]shared.NetworkFilterConfigFactory) { for name, factory := range factories { if _, ok := networkFilterConfigFactoryRegistry[name]; ok { @@ -64,3 +71,355 @@ func RegisterNetworkFilterConfigFactories(factories map[string]shared.NetworkFil networkFilterConfigFactoryRegistry[name] = factory } } + +// --------------------------------------------------------------------------- +// Listener filter registry +// --------------------------------------------------------------------------- + +var listenerFilterConfigFactoryRegistry = make(map[string]shared.ListenerFilterConfigFactory) + +// GetListenerFilterConfigFactory returns the registered listener filter config factory for +// name, or nil if no factory is registered. +func GetListenerFilterConfigFactory(name string) shared.ListenerFilterConfigFactory { + return listenerFilterConfigFactoryRegistry[name] +} + +// RegisterListenerFilterConfigFactories registers listener filter config factories. MUST only be +// called from init() functions. +func RegisterListenerFilterConfigFactories(factories map[string]shared.ListenerFilterConfigFactory) { + for name, factory := range factories { + if _, ok := listenerFilterConfigFactoryRegistry[name]; ok { + panic("listener filter config factory already registered: " + name) + } + listenerFilterConfigFactoryRegistry[name] = factory + } +} + +// --------------------------------------------------------------------------- +// UDP listener filter registry +// --------------------------------------------------------------------------- + +var udpListenerFilterConfigFactoryRegistry = make(map[string]shared.UdpListenerFilterConfigFactory) + +// GetUdpListenerFilterConfigFactory returns the registered UDP listener filter config factory +// for name, or nil if no factory is registered. +func GetUdpListenerFilterConfigFactory(name string) shared.UdpListenerFilterConfigFactory { + return udpListenerFilterConfigFactoryRegistry[name] +} + +// RegisterUdpListenerFilterConfigFactories registers UDP listener filter config factories. MUST +// only be called from init() functions. +func RegisterUdpListenerFilterConfigFactories(factories map[string]shared.UdpListenerFilterConfigFactory) { + for name, factory := range factories { + if _, ok := udpListenerFilterConfigFactoryRegistry[name]; ok { + panic("UDP listener filter config factory already registered: " + name) + } + udpListenerFilterConfigFactoryRegistry[name] = factory + } +} + +// --------------------------------------------------------------------------- +// Access logger registry +// --------------------------------------------------------------------------- + +var accessLoggerConfigFactoryRegistry = make(map[string]shared.AccessLoggerConfigFactory) + +// GetAccessLoggerConfigFactory returns the registered access logger config factory for name, +// or nil if no factory is registered. +func GetAccessLoggerConfigFactory(name string) shared.AccessLoggerConfigFactory { + return accessLoggerConfigFactoryRegistry[name] +} + +// RegisterAccessLoggerConfigFactories registers access logger config factories. MUST only be +// called from init() functions. +func RegisterAccessLoggerConfigFactories(factories map[string]shared.AccessLoggerConfigFactory) { + for name, factory := range factories { + if _, ok := accessLoggerConfigFactoryRegistry[name]; ok { + panic("access logger config factory already registered: " + name) + } + accessLoggerConfigFactoryRegistry[name] = factory + } +} + +// --------------------------------------------------------------------------- +// Matcher registry +// --------------------------------------------------------------------------- + +var matcherConfigFactoryRegistry = make(map[string]shared.MatcherConfigFactory) + +// GetMatcherConfigFactory returns the registered matcher config factory for name, or nil if no +// factory is registered. +func GetMatcherConfigFactory(name string) shared.MatcherConfigFactory { + return matcherConfigFactoryRegistry[name] +} + +// RegisterMatcherConfigFactories registers matcher config factories. MUST only be called from +// init() functions. +func RegisterMatcherConfigFactories(factories map[string]shared.MatcherConfigFactory) { + for name, factory := range factories { + if _, ok := matcherConfigFactoryRegistry[name]; ok { + panic("matcher config factory already registered: " + name) + } + matcherConfigFactoryRegistry[name] = factory + } +} + +// --------------------------------------------------------------------------- +// Cert validator registry +// --------------------------------------------------------------------------- + +var certValidatorConfigFactoryRegistry = make(map[string]shared.CertValidatorConfigFactory) + +// GetCertValidatorConfigFactory returns the registered cert validator config factory for name, +// or nil if no factory is registered. +func GetCertValidatorConfigFactory(name string) shared.CertValidatorConfigFactory { + return certValidatorConfigFactoryRegistry[name] +} + +// RegisterCertValidatorConfigFactories registers cert validator config factories. MUST only be +// called from init() functions. +func RegisterCertValidatorConfigFactories(factories map[string]shared.CertValidatorConfigFactory) { + for name, factory := range factories { + if _, ok := certValidatorConfigFactoryRegistry[name]; ok { + panic("cert validator config factory already registered: " + name) + } + certValidatorConfigFactoryRegistry[name] = factory + } +} + +// --------------------------------------------------------------------------- +// DNS resolver registry +// --------------------------------------------------------------------------- + +var dnsResolverConfigFactoryRegistry = make(map[string]shared.DnsResolverConfigFactory) + +// GetDnsResolverConfigFactory returns the registered DNS resolver config factory for name, or +// nil if no factory is registered. +func GetDnsResolverConfigFactory(name string) shared.DnsResolverConfigFactory { + return dnsResolverConfigFactoryRegistry[name] +} + +// RegisterDnsResolverConfigFactories registers DNS resolver config factories. MUST only be +// called from init() functions. +func RegisterDnsResolverConfigFactories(factories map[string]shared.DnsResolverConfigFactory) { + for name, factory := range factories { + if _, ok := dnsResolverConfigFactoryRegistry[name]; ok { + panic("DNS resolver config factory already registered: " + name) + } + dnsResolverConfigFactoryRegistry[name] = factory + } +} + +// --------------------------------------------------------------------------- +// Upstream HTTP/TCP bridge registry +// --------------------------------------------------------------------------- + +var upstreamHttpTcpBridgeConfigFactoryRegistry = make(map[string]shared.UpstreamHttpTcpBridgeConfigFactory) + +// GetUpstreamHttpTcpBridgeConfigFactory returns the registered upstream HTTP/TCP bridge config +// factory for name, or nil if no factory is registered. +func GetUpstreamHttpTcpBridgeConfigFactory(name string) shared.UpstreamHttpTcpBridgeConfigFactory { + return upstreamHttpTcpBridgeConfigFactoryRegistry[name] +} + +// RegisterUpstreamHttpTcpBridgeConfigFactories registers upstream HTTP/TCP bridge config +// factories. MUST only be called from init() functions. +func RegisterUpstreamHttpTcpBridgeConfigFactories(factories map[string]shared.UpstreamHttpTcpBridgeConfigFactory) { + for name, factory := range factories { + if _, ok := upstreamHttpTcpBridgeConfigFactoryRegistry[name]; ok { + panic("upstream HTTP/TCP bridge config factory already registered: " + name) + } + upstreamHttpTcpBridgeConfigFactoryRegistry[name] = factory + } +} + +// --------------------------------------------------------------------------- +// Tracer registry +// --------------------------------------------------------------------------- + +var tracerConfigFactoryRegistry = make(map[string]shared.TracerConfigFactory) + +// GetTracerConfigFactory returns the registered tracer config factory for name, or nil if no +// factory is registered. +func GetTracerConfigFactory(name string) shared.TracerConfigFactory { + return tracerConfigFactoryRegistry[name] +} + +// RegisterTracerConfigFactories registers tracer config factories. MUST only be called from +// init() functions. +func RegisterTracerConfigFactories(factories map[string]shared.TracerConfigFactory) { + for name, factory := range factories { + if _, ok := tracerConfigFactoryRegistry[name]; ok { + panic("tracer config factory already registered: " + name) + } + tracerConfigFactoryRegistry[name] = factory + } +} + +// --------------------------------------------------------------------------- +// Transport socket factory registry +// --------------------------------------------------------------------------- + +var transportSocketFactoryConfigFactoryRegistry = make(map[string]shared.TransportSocketFactoryConfigFactory) + +// GetTransportSocketFactoryConfigFactory returns the registered transport socket factory config +// factory for name, or nil if no factory is registered. +func GetTransportSocketFactoryConfigFactory(name string) shared.TransportSocketFactoryConfigFactory { + return transportSocketFactoryConfigFactoryRegistry[name] +} + +// RegisterTransportSocketFactoryConfigFactories registers transport socket factory config +// factories. MUST only be called from init() functions. +func RegisterTransportSocketFactoryConfigFactories(factories map[string]shared.TransportSocketFactoryConfigFactory) { + for name, factory := range factories { + if _, ok := transportSocketFactoryConfigFactoryRegistry[name]; ok { + panic("transport socket factory config factory already registered: " + name) + } + transportSocketFactoryConfigFactoryRegistry[name] = factory + } +} + +// --------------------------------------------------------------------------- +// Load balancer registry +// --------------------------------------------------------------------------- + +var loadBalancerConfigFactoryRegistry = make(map[string]shared.LoadBalancerConfigFactory) + +// GetLoadBalancerConfigFactory returns the registered load balancer config factory for name, +// or nil if no factory is registered. +func GetLoadBalancerConfigFactory(name string) shared.LoadBalancerConfigFactory { + return loadBalancerConfigFactoryRegistry[name] +} + +// RegisterLoadBalancerConfigFactories registers load balancer config factories. MUST only be +// called from init() functions. +func RegisterLoadBalancerConfigFactories(factories map[string]shared.LoadBalancerConfigFactory) { + for name, factory := range factories { + if _, ok := loadBalancerConfigFactoryRegistry[name]; ok { + panic("load balancer config factory already registered: " + name) + } + loadBalancerConfigFactoryRegistry[name] = factory + } +} + +// --------------------------------------------------------------------------- +// Bootstrap extension registry +// --------------------------------------------------------------------------- + +var bootstrapExtensionConfigFactoryRegistry = make(map[string]shared.BootstrapExtensionConfigFactory) + +// GetBootstrapExtensionConfigFactory returns the registered bootstrap extension config +// factory for name, or nil if no factory is registered. +func GetBootstrapExtensionConfigFactory(name string) shared.BootstrapExtensionConfigFactory { + return bootstrapExtensionConfigFactoryRegistry[name] +} + +// RegisterBootstrapExtensionConfigFactories registers bootstrap extension config factories. +// MUST only be called from init() functions. +func RegisterBootstrapExtensionConfigFactories(factories map[string]shared.BootstrapExtensionConfigFactory) { + for name, factory := range factories { + if _, ok := bootstrapExtensionConfigFactoryRegistry[name]; ok { + panic("bootstrap extension config factory already registered: " + name) + } + bootstrapExtensionConfigFactoryRegistry[name] = factory + } +} + +// --------------------------------------------------------------------------- +// Cluster registry +// --------------------------------------------------------------------------- + +var clusterConfigFactoryRegistry = make(map[string]shared.ClusterConfigFactory) + +// GetClusterConfigFactory returns the registered cluster config factory for name, or nil if no +// factory is registered. +func GetClusterConfigFactory(name string) shared.ClusterConfigFactory { + return clusterConfigFactoryRegistry[name] +} + +// RegisterClusterConfigFactories registers cluster config factories. MUST only be called from +// init() functions. +func RegisterClusterConfigFactories(factories map[string]shared.ClusterConfigFactory) { + for name, factory := range factories { + if _, ok := clusterConfigFactoryRegistry[name]; ok { + panic("cluster config factory already registered: " + name) + } + clusterConfigFactoryRegistry[name] = factory + } +} + +// --------------------------------------------------------------------------- +// Program-wide utilities +// --------------------------------------------------------------------------- + +// programHandle is the live shared.ProgramHandle wired up by the abi package via +// SetProgramHandle. It defaults to a no-op so module test code that doesn't link the abi +// package still gets sensible zero values. atomic.Pointer is used so SetProgramHandle from +// a test goroutine doesn't race with module code reading the handle. +var programHandle atomic.Pointer[shared.ProgramHandle] + +func init() { + var noop shared.ProgramHandle = &noopProgramHandle{} + programHandle.Store(&noop) +} + +func loadProgramHandle() shared.ProgramHandle { return *programHandle.Load() } + +// SetProgramHandle replaces the program-wide handle used by GetConcurrency / IsValidationMode +// / Register*/Get* below. The abi package calls this once during init to wire in the live +// Envoy callbacks; tests may override it to inject a fake. +func SetProgramHandle(h shared.ProgramHandle) { programHandle.Store(&h) } + +// GetConcurrency returns the number of worker threads the server is configured to use. MUST +// be called on the main thread (typically inside an on-program-init or on-server-initialized +// hook). +func GetConcurrency() uint32 { return loadProgramHandle().GetConcurrency() } + +// IsValidationMode reports whether the server is running in config-validation mode +// (`--mode validate`). Modules can use this to skip expensive operations during validation. +// MUST be called on the main thread. +func IsValidationMode() bool { return loadProgramHandle().IsValidationMode() } + +// RegisterFunction registers a function pointer in Envoy's process-wide function registry — +// used for zero-copy cross-module calls. See shared.ProgramHandle.RegisterFunction for full +// semantics. Thread-safe. +func RegisterFunction(key string, fnPtr unsafe.Pointer) bool { + return loadProgramHandle().RegisterFunction(key, fnPtr) +} + +// GetFunction retrieves a previously registered function pointer by key. See +// shared.ProgramHandle.GetFunction. Thread-safe. +func GetFunction(key string) (unsafe.Pointer, bool) { + return loadProgramHandle().GetFunction(key) +} + +// RegisterSharedData registers an opaque data pointer in Envoy's process-wide shared-data +// registry. See shared.ProgramHandle.RegisterSharedData. Thread-safe. +func RegisterSharedData(key string, dataPtr unsafe.Pointer) bool { + return loadProgramHandle().RegisterSharedData(key, dataPtr) +} + +// GetSharedData retrieves a previously registered data pointer by key. See +// shared.ProgramHandle.GetSharedData. Thread-safe. +func GetSharedData(key string) (unsafe.Pointer, bool) { + return loadProgramHandle().GetSharedData(key) +} + +// Log writes a formatted message through Envoy's logging subsystem. This is the +// process-global logging entry point — it works for any extension type, including +// surfaces (bootstrap, cluster, tracer, etc.) whose handle does not expose its own Log +// method. Thread-safe. +func Log(level shared.LogLevel, format string, args ...any) { + loadProgramHandle().Log(level, format, args...) +} + +// noopProgramHandle is the default until abi.init() replaces it with the live one. +type noopProgramHandle struct{} + +func (noopProgramHandle) GetConcurrency() uint32 { return 0 } +func (noopProgramHandle) IsValidationMode() bool { return false } +func (noopProgramHandle) RegisterFunction(_ string, _ unsafe.Pointer) bool { return false } +func (noopProgramHandle) GetFunction(_ string) (unsafe.Pointer, bool) { return nil, false } +func (noopProgramHandle) RegisterSharedData(_ string, _ unsafe.Pointer) bool { return false } +func (noopProgramHandle) GetSharedData(_ string) (unsafe.Pointer, bool) { return nil, false } +func (noopProgramHandle) Log(_ shared.LogLevel, _ string, _ ...any) {} diff --git a/source/extensions/dynamic_modules/sdk/go/sdk_test.go b/source/extensions/dynamic_modules/sdk/go/sdk_test.go new file mode 100644 index 0000000000000..1d3b4f3bf5b57 --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/sdk_test.go @@ -0,0 +1,567 @@ +package sdk + +import ( + "sync/atomic" + "testing" + "unsafe" + + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +// The factory registries in sdk.go are package-level globals. Tests in this file pick +// per-test unique factory names so concurrent registration in other tests can't collide. +// +// Each registry is exercised through three behaviours: +// 1. Register-then-Get round-trips correctly. +// 2. Get returns nil when the name is unknown. +// 3. Re-registering the same name panics. +// +// We also cover the New* constructors that go through the registry plus an error path, +// the program-handle indirection (default noop, replacement, Log), and the package-level +// program utilities (RegisterFunction/GetFunction, RegisterSharedData/GetSharedData). + +// ----------------------------------------------------------------------------- +// Test factory implementations — minimal, name them explicitly per registry so +// tests don't accidentally cross-register. +// ----------------------------------------------------------------------------- + +type fakeHttpFilterConfigFactory struct { + shared.EmptyHttpFilterConfigFactory +} +type fakeNetworkFilterConfigFactory struct { + shared.EmptyNetworkFilterConfigFactory +} +type fakeListenerFilterConfigFactory struct { + shared.EmptyListenerFilterConfigFactory +} +type fakeUdpListenerFilterConfigFactory struct { + shared.EmptyUdpListenerFilterConfigFactory +} +type fakeAccessLoggerConfigFactory struct { + shared.EmptyAccessLoggerConfigFactory +} +type fakeMatcherConfigFactory struct { + shared.EmptyMatcherConfigFactory +} +type fakeCertValidatorConfigFactory struct { + shared.EmptyCertValidatorConfigFactory +} +type fakeDnsResolverConfigFactory struct { + shared.EmptyDnsResolverConfigFactory +} +type fakeUpstreamHttpTcpBridgeConfigFactory struct { + shared.EmptyUpstreamHttpTcpBridgeConfigFactory +} +type fakeTracerConfigFactory struct { + shared.EmptyTracerConfigFactory +} +type fakeTransportSocketFactoryConfigFactory struct { + shared.EmptyTransportSocketFactoryConfigFactory +} +type fakeLoadBalancerConfigFactory struct { + shared.EmptyLoadBalancerConfigFactory +} +type fakeBootstrapExtensionConfigFactory struct { + shared.EmptyBootstrapExtensionConfigFactory +} +type fakeClusterConfigFactory struct { + shared.EmptyClusterConfigFactory +} + +// ----------------------------------------------------------------------------- +// HTTP filter registry +// ----------------------------------------------------------------------------- + +func TestHttpFilterRegistry(t *testing.T) { + const name = "test_http_filter_registry_filter" + f := &fakeHttpFilterConfigFactory{} + RegisterHttpFilterConfigFactories(map[string]shared.HttpFilterConfigFactory{name: f}) + + if got := GetHttpFilterConfigFactory(name); got != f { + t.Errorf("Get returned %v, want %v", got, f) + } + if got := GetHttpFilterConfigFactory("does_not_exist"); got != nil { + t.Errorf("Get for unknown name returned %v, want nil", got) + } + + mustPanic(t, "duplicate http filter registration", func() { + RegisterHttpFilterConfigFactories(map[string]shared.HttpFilterConfigFactory{name: f}) + }) +} + +func TestNewHttpFilterFactory(t *testing.T) { + const name = "test_new_http_filter_factory" + f := &fakeHttpFilterConfigFactory{} + RegisterHttpFilterConfigFactories(map[string]shared.HttpFilterConfigFactory{name: f}) + + out, err := NewHttpFilterFactory(nil, name, nil) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if out == nil { + t.Fatal("NewHttpFilterFactory returned nil factory") + } + + if _, err := NewHttpFilterFactory(nil, "does_not_exist", nil); err == nil { + t.Error("NewHttpFilterFactory for unknown name should error") + } +} + +// ----------------------------------------------------------------------------- +// Network filter registry +// ----------------------------------------------------------------------------- + +func TestNetworkFilterRegistry(t *testing.T) { + const name = "test_network_filter_registry_filter" + f := &fakeNetworkFilterConfigFactory{} + RegisterNetworkFilterConfigFactories(map[string]shared.NetworkFilterConfigFactory{name: f}) + + if got := GetNetworkFilterConfigFactory(name); got != f { + t.Errorf("Get returned %v, want %v", got, f) + } + if got := GetNetworkFilterConfigFactory("does_not_exist"); got != nil { + t.Errorf("Get for unknown name returned %v, want nil", got) + } + + mustPanic(t, "duplicate network filter registration", func() { + RegisterNetworkFilterConfigFactories(map[string]shared.NetworkFilterConfigFactory{name: f}) + }) +} + +func TestNewNetworkFilterFactory(t *testing.T) { + const name = "test_new_network_filter_factory" + f := &fakeNetworkFilterConfigFactory{} + RegisterNetworkFilterConfigFactories(map[string]shared.NetworkFilterConfigFactory{name: f}) + + out, err := NewNetworkFilterFactory(nil, name, nil) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if out == nil { + t.Fatal("NewNetworkFilterFactory returned nil factory") + } + + if _, err := NewNetworkFilterFactory(nil, "does_not_exist", nil); err == nil { + t.Error("NewNetworkFilterFactory for unknown name should error") + } +} + +// ----------------------------------------------------------------------------- +// Remaining 12 registries: same shape — register, get, panic on duplicate. +// ----------------------------------------------------------------------------- + +func TestListenerFilterRegistry(t *testing.T) { + const name = "test_listener_filter_registry" + f := &fakeListenerFilterConfigFactory{} + RegisterListenerFilterConfigFactories(map[string]shared.ListenerFilterConfigFactory{name: f}) + if got := GetListenerFilterConfigFactory(name); got != f { + t.Errorf("Get returned %v, want %v", got, f) + } + if got := GetListenerFilterConfigFactory("does_not_exist"); got != nil { + t.Errorf("Get for unknown name returned %v, want nil", got) + } + mustPanic(t, "duplicate listener filter registration", func() { + RegisterListenerFilterConfigFactories(map[string]shared.ListenerFilterConfigFactory{name: f}) + }) +} + +func TestUdpListenerFilterRegistry(t *testing.T) { + const name = "test_udp_listener_filter_registry" + f := &fakeUdpListenerFilterConfigFactory{} + RegisterUdpListenerFilterConfigFactories(map[string]shared.UdpListenerFilterConfigFactory{name: f}) + if got := GetUdpListenerFilterConfigFactory(name); got != f { + t.Errorf("Get returned %v, want %v", got, f) + } + if got := GetUdpListenerFilterConfigFactory("does_not_exist"); got != nil { + t.Errorf("Get for unknown name returned %v, want nil", got) + } + mustPanic(t, "duplicate UDP listener filter registration", func() { + RegisterUdpListenerFilterConfigFactories(map[string]shared.UdpListenerFilterConfigFactory{name: f}) + }) +} + +func TestAccessLoggerRegistry(t *testing.T) { + const name = "test_access_logger_registry" + f := &fakeAccessLoggerConfigFactory{} + RegisterAccessLoggerConfigFactories(map[string]shared.AccessLoggerConfigFactory{name: f}) + if got := GetAccessLoggerConfigFactory(name); got != f { + t.Errorf("Get returned %v, want %v", got, f) + } + if got := GetAccessLoggerConfigFactory("does_not_exist"); got != nil { + t.Errorf("Get for unknown name returned %v, want nil", got) + } + mustPanic(t, "duplicate access logger registration", func() { + RegisterAccessLoggerConfigFactories(map[string]shared.AccessLoggerConfigFactory{name: f}) + }) +} + +func TestMatcherRegistry(t *testing.T) { + const name = "test_matcher_registry" + f := &fakeMatcherConfigFactory{} + RegisterMatcherConfigFactories(map[string]shared.MatcherConfigFactory{name: f}) + if got := GetMatcherConfigFactory(name); got != f { + t.Errorf("Get returned %v, want %v", got, f) + } + if got := GetMatcherConfigFactory("does_not_exist"); got != nil { + t.Errorf("Get for unknown name returned %v, want nil", got) + } + mustPanic(t, "duplicate matcher registration", func() { + RegisterMatcherConfigFactories(map[string]shared.MatcherConfigFactory{name: f}) + }) +} + +func TestCertValidatorRegistry(t *testing.T) { + const name = "test_cert_validator_registry" + f := &fakeCertValidatorConfigFactory{} + RegisterCertValidatorConfigFactories(map[string]shared.CertValidatorConfigFactory{name: f}) + if got := GetCertValidatorConfigFactory(name); got != f { + t.Errorf("Get returned %v, want %v", got, f) + } + if got := GetCertValidatorConfigFactory("does_not_exist"); got != nil { + t.Errorf("Get for unknown name returned %v, want nil", got) + } + mustPanic(t, "duplicate cert validator registration", func() { + RegisterCertValidatorConfigFactories(map[string]shared.CertValidatorConfigFactory{name: f}) + }) +} + +func TestDnsResolverRegistry(t *testing.T) { + const name = "test_dns_resolver_registry" + f := &fakeDnsResolverConfigFactory{} + RegisterDnsResolverConfigFactories(map[string]shared.DnsResolverConfigFactory{name: f}) + if got := GetDnsResolverConfigFactory(name); got != f { + t.Errorf("Get returned %v, want %v", got, f) + } + if got := GetDnsResolverConfigFactory("does_not_exist"); got != nil { + t.Errorf("Get for unknown name returned %v, want nil", got) + } + mustPanic(t, "duplicate DNS resolver registration", func() { + RegisterDnsResolverConfigFactories(map[string]shared.DnsResolverConfigFactory{name: f}) + }) +} + +func TestUpstreamHttpTcpBridgeRegistry(t *testing.T) { + const name = "test_upstream_http_tcp_bridge_registry" + f := &fakeUpstreamHttpTcpBridgeConfigFactory{} + RegisterUpstreamHttpTcpBridgeConfigFactories(map[string]shared.UpstreamHttpTcpBridgeConfigFactory{name: f}) + if got := GetUpstreamHttpTcpBridgeConfigFactory(name); got != f { + t.Errorf("Get returned %v, want %v", got, f) + } + if got := GetUpstreamHttpTcpBridgeConfigFactory("does_not_exist"); got != nil { + t.Errorf("Get for unknown name returned %v, want nil", got) + } + mustPanic(t, "duplicate upstream bridge registration", func() { + RegisterUpstreamHttpTcpBridgeConfigFactories(map[string]shared.UpstreamHttpTcpBridgeConfigFactory{name: f}) + }) +} + +func TestTracerRegistry(t *testing.T) { + const name = "test_tracer_registry" + f := &fakeTracerConfigFactory{} + RegisterTracerConfigFactories(map[string]shared.TracerConfigFactory{name: f}) + if got := GetTracerConfigFactory(name); got != f { + t.Errorf("Get returned %v, want %v", got, f) + } + if got := GetTracerConfigFactory("does_not_exist"); got != nil { + t.Errorf("Get for unknown name returned %v, want nil", got) + } + mustPanic(t, "duplicate tracer registration", func() { + RegisterTracerConfigFactories(map[string]shared.TracerConfigFactory{name: f}) + }) +} + +func TestTransportSocketFactoryRegistry(t *testing.T) { + const name = "test_transport_socket_registry" + f := &fakeTransportSocketFactoryConfigFactory{} + RegisterTransportSocketFactoryConfigFactories(map[string]shared.TransportSocketFactoryConfigFactory{name: f}) + if got := GetTransportSocketFactoryConfigFactory(name); got != f { + t.Errorf("Get returned %v, want %v", got, f) + } + if got := GetTransportSocketFactoryConfigFactory("does_not_exist"); got != nil { + t.Errorf("Get for unknown name returned %v, want nil", got) + } + mustPanic(t, "duplicate transport socket registration", func() { + RegisterTransportSocketFactoryConfigFactories(map[string]shared.TransportSocketFactoryConfigFactory{name: f}) + }) +} + +func TestLoadBalancerRegistry(t *testing.T) { + const name = "test_load_balancer_registry" + f := &fakeLoadBalancerConfigFactory{} + RegisterLoadBalancerConfigFactories(map[string]shared.LoadBalancerConfigFactory{name: f}) + if got := GetLoadBalancerConfigFactory(name); got != f { + t.Errorf("Get returned %v, want %v", got, f) + } + if got := GetLoadBalancerConfigFactory("does_not_exist"); got != nil { + t.Errorf("Get for unknown name returned %v, want nil", got) + } + mustPanic(t, "duplicate load balancer registration", func() { + RegisterLoadBalancerConfigFactories(map[string]shared.LoadBalancerConfigFactory{name: f}) + }) +} + +func TestBootstrapExtensionRegistry(t *testing.T) { + const name = "test_bootstrap_extension_registry" + f := &fakeBootstrapExtensionConfigFactory{} + RegisterBootstrapExtensionConfigFactories(map[string]shared.BootstrapExtensionConfigFactory{name: f}) + if got := GetBootstrapExtensionConfigFactory(name); got != f { + t.Errorf("Get returned %v, want %v", got, f) + } + if got := GetBootstrapExtensionConfigFactory("does_not_exist"); got != nil { + t.Errorf("Get for unknown name returned %v, want nil", got) + } + mustPanic(t, "duplicate bootstrap extension registration", func() { + RegisterBootstrapExtensionConfigFactories(map[string]shared.BootstrapExtensionConfigFactory{name: f}) + }) +} + +func TestClusterRegistry(t *testing.T) { + const name = "test_cluster_registry" + f := &fakeClusterConfigFactory{} + RegisterClusterConfigFactories(map[string]shared.ClusterConfigFactory{name: f}) + if got := GetClusterConfigFactory(name); got != f { + t.Errorf("Get returned %v, want %v", got, f) + } + if got := GetClusterConfigFactory("does_not_exist"); got != nil { + t.Errorf("Get for unknown name returned %v, want nil", got) + } + mustPanic(t, "duplicate cluster registration", func() { + RegisterClusterConfigFactories(map[string]shared.ClusterConfigFactory{name: f}) + }) +} + +// ----------------------------------------------------------------------------- +// Program handle indirection +// ----------------------------------------------------------------------------- + +// fakeProgramHandle is a recording shared.ProgramHandle. Each method writes its inputs to +// fields that the test inspects after the call. +type fakeProgramHandle struct { + concurrency uint32 + validation bool + functions map[string]unsafe.Pointer + sharedData map[string]unsafe.Pointer + registerFnFails bool + registerDataFails bool + + logs []logRecord +} + +type logRecord struct { + level shared.LogLevel + format string + args []any +} + +func (f *fakeProgramHandle) GetConcurrency() uint32 { return f.concurrency } +func (f *fakeProgramHandle) IsValidationMode() bool { return f.validation } +func (f *fakeProgramHandle) Log(l shared.LogLevel, format string, args ...any) { + f.logs = append(f.logs, logRecord{l, format, args}) +} +func (f *fakeProgramHandle) RegisterFunction(key string, ptr unsafe.Pointer) bool { + if f.registerFnFails { + return false + } + if f.functions == nil { + f.functions = map[string]unsafe.Pointer{} + } + f.functions[key] = ptr + return true +} +func (f *fakeProgramHandle) GetFunction(key string) (unsafe.Pointer, bool) { + p, ok := f.functions[key] + return p, ok +} +func (f *fakeProgramHandle) RegisterSharedData(key string, ptr unsafe.Pointer) bool { + if f.registerDataFails { + return false + } + if f.sharedData == nil { + f.sharedData = map[string]unsafe.Pointer{} + } + f.sharedData[key] = ptr + return true +} +func (f *fakeProgramHandle) GetSharedData(key string) (unsafe.Pointer, bool) { + p, ok := f.sharedData[key] + return p, ok +} + +// withProgramHandle swaps in a ProgramHandle for the duration of fn and restores the +// previous one on exit. Tests must use this so they don't leak handle state into other +// tests in the package. +func withProgramHandle(t *testing.T, h shared.ProgramHandle, fn func()) { + t.Helper() + prev := *programHandle.Load() + SetProgramHandle(h) + defer SetProgramHandle(prev) + fn() +} + +func TestProgramHandle_DefaultIsNoop(t *testing.T) { + // The package-level init() installs noopProgramHandle. Each accessor must return its + // documented zero value so unconfigured modules don't blow up. + noop := *programHandle.Load() + if _, ok := noop.(*noopProgramHandle); !ok { + t.Fatalf("default handle is %T, want *noopProgramHandle", noop) + } + + if got := GetConcurrency(); got != 0 { + t.Errorf("GetConcurrency() = %d, want 0", got) + } + if IsValidationMode() { + t.Error("IsValidationMode() = true, want false") + } + var sentinel int + ptr := unsafe.Pointer(&sentinel) + if RegisterFunction("k", ptr) { + t.Error("noop RegisterFunction returned true") + } + if p, ok := GetFunction("k"); p != nil || ok { + t.Errorf("noop GetFunction = (%v, %v), want (nil, false)", p, ok) + } + if RegisterSharedData("k", ptr) { + t.Error("noop RegisterSharedData returned true") + } + if p, ok := GetSharedData("k"); p != nil || ok { + t.Errorf("noop GetSharedData = (%v, %v), want (nil, false)", p, ok) + } + // noop Log should not panic. + Log(shared.LogLevelInfo, "noop %s", "ok") +} + +func TestProgramHandle_Replacement(t *testing.T) { + fake := &fakeProgramHandle{concurrency: 7, validation: true} + withProgramHandle(t, fake, func() { + if got := GetConcurrency(); got != 7 { + t.Errorf("GetConcurrency() = %d, want 7", got) + } + if !IsValidationMode() { + t.Error("IsValidationMode() = false, want true") + } + }) + + // After deferred restore the noop is back. + if got := GetConcurrency(); got != 0 { + t.Errorf("after restore GetConcurrency() = %d, want 0", got) + } +} + +func TestProgramHandle_Log(t *testing.T) { + fake := &fakeProgramHandle{} + withProgramHandle(t, fake, func() { + Log(shared.LogLevelWarn, "hello %s", "world") + Log(shared.LogLevelError, "boom") + }) + + if got, want := len(fake.logs), 2; got != want { + t.Fatalf("len(logs) = %d, want %d", got, want) + } + if fake.logs[0].level != shared.LogLevelWarn { + t.Errorf("log[0].level = %v, want Warn", fake.logs[0].level) + } + if fake.logs[0].format != "hello %s" { + t.Errorf("log[0].format = %q, want %q", fake.logs[0].format, "hello %s") + } + if len(fake.logs[0].args) != 1 || fake.logs[0].args[0].(string) != "world" { + t.Errorf("log[0].args = %v, want [world]", fake.logs[0].args) + } + if fake.logs[1].level != shared.LogLevelError { + t.Errorf("log[1].level = %v, want Error", fake.logs[1].level) + } +} + +func TestProgramHandle_FunctionRegistry(t *testing.T) { + fake := &fakeProgramHandle{} + var sentinel int + ptr := unsafe.Pointer(&sentinel) + + withProgramHandle(t, fake, func() { + if !RegisterFunction("fn", ptr) { + t.Fatal("RegisterFunction returned false") + } + got, ok := GetFunction("fn") + if !ok { + t.Fatal("GetFunction returned false for known key") + } + if got != ptr { + t.Errorf("GetFunction returned %v, want %v", got, ptr) + } + if _, ok := GetFunction("missing"); ok { + t.Error("GetFunction returned true for unknown key") + } + }) +} + +func TestProgramHandle_SharedDataRegistry(t *testing.T) { + fake := &fakeProgramHandle{} + var sentinel int + ptr := unsafe.Pointer(&sentinel) + + withProgramHandle(t, fake, func() { + if !RegisterSharedData("data", ptr) { + t.Fatal("RegisterSharedData returned false") + } + got, ok := GetSharedData("data") + if !ok { + t.Fatal("GetSharedData returned false for known key") + } + if got != ptr { + t.Errorf("GetSharedData returned %v, want %v", got, ptr) + } + if _, ok := GetSharedData("missing"); ok { + t.Error("GetSharedData returned true for unknown key") + } + }) +} + +func TestProgramHandle_AtomicSwap(t *testing.T) { + // SetProgramHandle uses atomic.Pointer, so concurrent reads while a swap is in + // progress must return either the old or new handle — never a torn pointer. We can't + // detect tearing in a unit test directly, but we can at least sanity-check that a + // swap is observed by subsequent reads on a different goroutine. + first := &fakeProgramHandle{concurrency: 1} + second := &fakeProgramHandle{concurrency: 2} + + prev := *programHandle.Load() + defer SetProgramHandle(prev) + + SetProgramHandle(first) + if got := GetConcurrency(); got != 1 { + t.Fatalf("after first set: GetConcurrency = %d, want 1", got) + } + + done := make(chan struct{}) + var observed atomic.Uint32 + go func() { + observed.Store(GetConcurrency()) + close(done) + }() + SetProgramHandle(second) + <-done + + // observed must be 1 or 2 — either is fine (race over the swap), but we never want 0 + // (which would indicate the noop default leaked back in). + if v := observed.Load(); v != 1 && v != 2 { + t.Errorf("observed concurrency = %d, want 1 or 2", v) + } + if got := GetConcurrency(); got != 2 { + t.Errorf("after second set: GetConcurrency = %d, want 2", got) + } +} + +// ----------------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------------- + +// mustPanic asserts fn panics. It does not assert on the panic message — re-registration +// panics include the factory name which we don't want to lock down here. +func mustPanic(t *testing.T, label string, fn func()) { + t.Helper() + defer func() { + if r := recover(); r == nil { + t.Errorf("%s: expected panic, got none", label) + } + }() + fn() +} diff --git a/source/extensions/dynamic_modules/sdk/go/shared/access_log.go b/source/extensions/dynamic_modules/sdk/go/shared/access_log.go new file mode 100644 index 0000000000000..19db80e9dbc25 --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/shared/access_log.go @@ -0,0 +1,326 @@ +//go:generate mockgen -source=access_log.go -destination=mocks/mock_access_log.go -package=mocks +package shared + +// Access logger SDK surface for dynamic modules. +// +// This mirrors the Rust SDK's `access_log` module. A module that exposes an access logger +// implements AccessLoggerConfigFactory and registers it from an init() function via +// sdk.RegisterAccessLoggerConfigFactories. +// +// Lifecycle: Envoy calls AccessLoggerConfigFactory.Create exactly once per access-log +// configuration to obtain an AccessLoggerFactory. The factory is then asked to Create per-thread +// AccessLogger instances; each AccessLogger receives Log calls for every log event on the +// thread it owns. On shutdown, Flush is called once before OnDestroy. + +// AccessLogType identifies the kind of access-log event being delivered to AccessLogger.Log. It +// corresponds to envoy_dynamic_module_type_access_log_type / envoy::data::accesslog::v3::AccessLogType. +type AccessLogType uint32 + +const ( + AccessLogTypeNotSet AccessLogType = 0 + AccessLogTypeTcpUpstreamConnected AccessLogType = 1 + AccessLogTypeTcpPeriodic AccessLogType = 2 + AccessLogTypeTcpConnectionEnd AccessLogType = 3 + AccessLogTypeDownstreamStart AccessLogType = 4 + AccessLogTypeDownstreamPeriodic AccessLogType = 5 + AccessLogTypeDownstreamEnd AccessLogType = 6 + AccessLogTypeUpstreamPoolReady AccessLogType = 7 + AccessLogTypeUpstreamPeriodic AccessLogType = 8 + AccessLogTypeUpstreamEnd AccessLogType = 9 + AccessLogTypeDownstreamTunnelSuccessfullyEstablished AccessLogType = 10 + AccessLogTypeUdpTunnelUpstreamConnected AccessLogType = 11 + AccessLogTypeUdpPeriodic AccessLogType = 12 + AccessLogTypeUdpSessionEnd AccessLogType = 13 +) + +// ResponseFlag corresponds to envoy_dynamic_module_type_response_flag and Envoy's +// CoreResponseFlag enum. Use AccessLogContext.HasResponseFlag to test for individual flags or +// GetResponseFlags to retrieve the bitmask. +type ResponseFlag uint32 + +const ( + ResponseFlagFailedLocalHealthCheck ResponseFlag = 0 + ResponseFlagNoHealthyUpstream ResponseFlag = 1 + ResponseFlagUpstreamRequestTimeout ResponseFlag = 2 + ResponseFlagLocalReset ResponseFlag = 3 + ResponseFlagUpstreamRemoteReset ResponseFlag = 4 + ResponseFlagUpstreamConnectionFailure ResponseFlag = 5 + ResponseFlagUpstreamConnectionTermination ResponseFlag = 6 + ResponseFlagUpstreamOverflow ResponseFlag = 7 + ResponseFlagNoRouteFound ResponseFlag = 8 + ResponseFlagDelayInjected ResponseFlag = 9 + ResponseFlagFaultInjected ResponseFlag = 10 + ResponseFlagRateLimited ResponseFlag = 11 + ResponseFlagUnauthorizedExternalService ResponseFlag = 12 + ResponseFlagRateLimitServiceError ResponseFlag = 13 + ResponseFlagDownstreamConnectionTermination ResponseFlag = 14 + ResponseFlagUpstreamRetryLimitExceeded ResponseFlag = 15 + ResponseFlagStreamIdleTimeout ResponseFlag = 16 + ResponseFlagInvalidEnvoyRequestHeaders ResponseFlag = 17 + ResponseFlagDownstreamProtocolError ResponseFlag = 18 + ResponseFlagUpstreamMaxStreamDurationReached ResponseFlag = 19 + ResponseFlagResponseFromCacheFilter ResponseFlag = 20 + ResponseFlagNoFilterConfigFound ResponseFlag = 21 + ResponseFlagDurationTimeout ResponseFlag = 22 + ResponseFlagUpstreamProtocolError ResponseFlag = 23 + ResponseFlagNoClusterFound ResponseFlag = 24 + ResponseFlagOverloadManager ResponseFlag = 25 + ResponseFlagDnsResolutionFailed ResponseFlag = 26 + ResponseFlagDropOverLoad ResponseFlag = 27 + ResponseFlagDownstreamRemoteReset ResponseFlag = 28 + ResponseFlagUnconditionalDropOverload ResponseFlag = 29 +) + +// AccessLogTimingInfo carries per-stream timing data. All durations are in nanoseconds; -1 +// indicates the timing is not available. +type AccessLogTimingInfo struct { + // StartTimeUnixNs is the request start time as a Unix timestamp in nanoseconds. + StartTimeUnixNs int64 + // RequestCompleteDurationNs is the duration from start to request complete. + RequestCompleteDurationNs int64 + FirstUpstreamTxByteSentNs int64 + LastUpstreamTxByteSentNs int64 + FirstUpstreamRxByteReceivedNs int64 + LastUpstreamRxByteReceivedNs int64 + FirstDownstreamTxByteSentNs int64 + LastDownstreamTxByteSentNs int64 +} + +// AccessLogBytesInfo carries per-stream byte-count totals. All values are 0 if not available. +type AccessLogBytesInfo struct { + // BytesReceived is the total bytes received from downstream. + BytesReceived uint64 + // BytesSent is the total bytes sent to downstream. + BytesSent uint64 + // WireBytesReceived is the wire bytes received (including TLS overhead). + WireBytesReceived uint64 + // WireBytesSent is the wire bytes sent (including TLS overhead). + WireBytesSent uint64 +} + +// AccessLogger is the per-thread (or shared) module-side access logger. The runtime calls Log +// for each log event on the thread it owns; Flush is called once during shutdown before +// OnDestroy. +type AccessLogger interface { + // Log is called when a log event occurs. The handle is valid only for the duration of this + // callback; do not retain it. + Log(handle AccessLogContext, logType AccessLogType) + + // Flush is called once during shutdown before OnDestroy, giving the module a chance to + // flush any buffered log entries. Implementations that don't buffer can leave this empty. + Flush() + + // OnDestroy is called when the logger instance is destroyed. + OnDestroy() +} + +// EmptyAccessLogger is a no-op AccessLogger that does nothing. +type EmptyAccessLogger struct{} + +func (*EmptyAccessLogger) Log(_ AccessLogContext, _ AccessLogType) {} +func (*EmptyAccessLogger) Flush() {} +func (*EmptyAccessLogger) OnDestroy() {} + +// AccessLoggerFactory creates per-thread (or shared) AccessLogger instances. Returning the same +// AccessLogger from multiple Create calls is allowed if the implementation is thread-safe. +type AccessLoggerFactory interface { + // Create creates an AccessLogger. + Create() AccessLogger + + // OnDestroy is called when the factory is destroyed. + OnDestroy() +} + +// EmptyAccessLoggerFactory is a no-op AccessLoggerFactory. +type EmptyAccessLoggerFactory struct{} + +func (*EmptyAccessLoggerFactory) Create() AccessLogger { return &EmptyAccessLogger{} } +func (*EmptyAccessLoggerFactory) OnDestroy() {} + +// AccessLoggerConfigFactory is the top-level factory the module registers via +// sdk.RegisterAccessLoggerConfigFactories. +type AccessLoggerConfigFactory interface { + // Create parses unparsedConfig and returns an AccessLoggerFactory. + Create(handle AccessLoggerConfigHandle, unparsedConfig []byte) (AccessLoggerFactory, error) +} + +// EmptyAccessLoggerConfigFactory is a no-op AccessLoggerConfigFactory. +type EmptyAccessLoggerConfigFactory struct{} + +func (*EmptyAccessLoggerConfigFactory) Create(_ AccessLoggerConfigHandle, _ []byte) (AccessLoggerFactory, error) { + return &EmptyAccessLoggerFactory{}, nil +} + +// AccessLoggerConfigHandle is the config-context handle. Note that for access loggers, metrics +// methods (Increment* / Set* / RecordHistogramValue) are also available on the config handle — +// they are scoped to the configuration rather than to a per-stream context, since loggers are +// not per-stream. +type AccessLoggerConfigHandle interface { + // DefineCounter creates a per-config counter. + DefineCounter(name string) (MetricID, MetricsResult) + + // DefineGauge creates a per-config gauge. + DefineGauge(name string) (MetricID, MetricsResult) + + // DefineHistogram creates a per-config histogram. + DefineHistogram(name string) (MetricID, MetricsResult) + + // IncrementCounter increments a counter previously defined via DefineCounter. + IncrementCounter(id MetricID, value uint64) MetricsResult + + // SetGauge sets a gauge previously defined via DefineGauge. + SetGauge(id MetricID, value uint64) MetricsResult + + // IncrementGauge adds value to a gauge previously defined via DefineGauge. + IncrementGauge(id MetricID, value uint64) MetricsResult + + // DecrementGauge subtracts value from a gauge previously defined via DefineGauge. + DecrementGauge(id MetricID, value uint64) MetricsResult + + // RecordHistogramValue records value in a histogram previously defined via DefineHistogram. + RecordHistogramValue(id MetricID, value uint64) MetricsResult +} + +// AccessLogContext is the per-event handle passed to AccessLogger.Log. It is valid only for the +// duration of the Log call; do not retain it. All getters return Envoy-owned memory whose +// validity ends with the callback unless otherwise noted. +// +// The interface is large because access logs need to surface every per-stream attribute. For +// most attributes, prefer GetAttributeString / GetAttributeNumber / GetAttributeBool with the +// AttributeID enum from types.go — those provide a unified accessor that mirrors the HTTP filter +// attribute API. The named getters below are kept for ABI parity but most are deprecated in +// favor of the attribute accessors. +type AccessLogContext interface { + // ---- headers ---- + + // GetHeadersSize returns the number of headers in the specified header map. Supported types + // are RequestHeader, ResponseHeader, and ResponseTrailer. + GetHeadersSize(headerType HttpHeaderType) uint64 + + // GetHeaders returns all headers from the specified header map. + GetHeaders(headerType HttpHeaderType) [][2]UnsafeEnvoyBuffer + + // GetHeaderValue returns a header value by key. index selects among multi-value headers + // (0 = first). The third return value is the total count of values for the key (0 when not + // found). + GetHeaderValue(headerType HttpHeaderType, key string, index uint64) (UnsafeEnvoyBuffer, uint64, bool) + + // ---- attribute accessors (preferred) ---- + + // GetAttributeString returns a string attribute value. See AttributeID in types.go. + GetAttributeString(id AttributeID) (UnsafeEnvoyBuffer, bool) + + // GetAttributeNumber returns an integer attribute value. + GetAttributeNumber(id AttributeID) (uint64, bool) + + // GetAttributeBool returns a boolean attribute value. + GetAttributeBool(id AttributeID) (bool, bool) + + // ---- response flags / timing / bytes ---- + + // HasResponseFlag returns true if the given response flag is set on the stream. + HasResponseFlag(flag ResponseFlag) bool + + // GetResponseFlags returns all response flags as a bitmask (bit i set ⇔ ResponseFlag(i)). + GetResponseFlags() uint64 + + // GetTimingInfo returns the per-stream timing struct. Individual fields are -1 if + // unavailable. + GetTimingInfo() AccessLogTimingInfo + + // GetBytesInfo returns the per-stream byte counts. + GetBytesInfo() AccessLogBytesInfo + + // ---- addresses ---- + + GetDownstreamRemoteAddress() (UnsafeEnvoyBuffer, uint32, bool) + GetDownstreamLocalAddress() (UnsafeEnvoyBuffer, uint32, bool) + GetDownstreamDirectRemoteAddress() (UnsafeEnvoyBuffer, uint32, bool) + GetDownstreamDirectLocalAddress() (UnsafeEnvoyBuffer, uint32, bool) + GetUpstreamRemoteAddress() (UnsafeEnvoyBuffer, uint32, bool) + GetUpstreamLocalAddress() (UnsafeEnvoyBuffer, uint32, bool) + + // ---- upstream info ---- + + GetUpstreamCluster() (UnsafeEnvoyBuffer, bool) + GetUpstreamHost() (UnsafeEnvoyBuffer, bool) + GetUpstreamConnectionID() uint64 + + // GetUpstreamTLSCipher returns the upstream TLS cipher suite. The returned buffer uses + // thread-local storage and is valid until the next call to this function or + // GetDownstreamTLSCipher on the same thread. + GetUpstreamTLSCipher() (UnsafeEnvoyBuffer, bool) + GetUpstreamTLSSessionID() (UnsafeEnvoyBuffer, bool) + GetUpstreamPeerIssuer() (UnsafeEnvoyBuffer, bool) + + // GetUpstreamPeerCertValidityStart returns the validity-start (notBefore) of the upstream + // peer certificate as epoch seconds, or 0 if not available. + GetUpstreamPeerCertValidityStart() int64 + + // GetUpstreamPeerCertValidityEnd returns the validity-end (notAfter) of the upstream peer + // certificate as epoch seconds, or 0 if not available. + GetUpstreamPeerCertValidityEnd() int64 + + GetUpstreamPeerURISans() []UnsafeEnvoyBuffer + GetUpstreamLocalURISans() []UnsafeEnvoyBuffer + GetUpstreamPeerDNSSans() []UnsafeEnvoyBuffer + GetUpstreamLocalDNSSans() []UnsafeEnvoyBuffer + + // ---- downstream connection / TLS info ---- + + // GetDownstreamTLSCipher returns the downstream TLS cipher suite. The returned buffer uses + // thread-local storage and is valid until the next call to this function or + // GetUpstreamTLSCipher on the same thread. + GetDownstreamTLSCipher() (UnsafeEnvoyBuffer, bool) + GetDownstreamTLSSessionID() (UnsafeEnvoyBuffer, bool) + GetDownstreamPeerIssuer() (UnsafeEnvoyBuffer, bool) + GetDownstreamPeerSerial() (UnsafeEnvoyBuffer, bool) + GetDownstreamPeerFingerprint1() (UnsafeEnvoyBuffer, bool) + + // GetDownstreamPeerCertPresented reports whether a peer certificate was presented. + GetDownstreamPeerCertPresented() bool + + // GetDownstreamPeerCertValidated reports whether the peer certificate was validated. + GetDownstreamPeerCertValidated() bool + + GetDownstreamPeerCertValidityStart() int64 + GetDownstreamPeerCertValidityEnd() int64 + + GetDownstreamPeerURISans() []UnsafeEnvoyBuffer + GetDownstreamLocalURISans() []UnsafeEnvoyBuffer + GetDownstreamPeerDNSSans() []UnsafeEnvoyBuffer + GetDownstreamLocalDNSSans() []UnsafeEnvoyBuffer + + // ---- metadata / filter state / tracing ---- + + // GetDynamicMetadata returns a value from dynamic metadata by filter namespace and key + // path. The path may be nested with dots. Complex (non-string) values are returned as JSON. + GetDynamicMetadata(filterName, path string) (UnsafeEnvoyBuffer, bool) + + // GetFilterState returns the serialized representation of a filter-state value by key. + GetFilterState(key string) (UnsafeEnvoyBuffer, bool) + + GetLocalReplyBody() (UnsafeEnvoyBuffer, bool) + + GetTraceID() (UnsafeEnvoyBuffer, bool) + GetSpanID() (UnsafeEnvoyBuffer, bool) + IsTraceSampled() bool + + // ---- additional stream info ---- + + GetJA3Hash() (UnsafeEnvoyBuffer, bool) + GetJA4Hash() (UnsafeEnvoyBuffer, bool) + + GetRequestHeadersBytes() uint64 + GetResponseHeadersBytes() uint64 + GetResponseTrailersBytes() uint64 + GetUpstreamProtocol() (UnsafeEnvoyBuffer, bool) + + // GetUpstreamPoolReadyDurationNs returns the time from when the upstream request was + // created to when the connection pool became ready, in nanoseconds. Returns -1 if not + // available. + GetUpstreamPoolReadyDurationNs() int64 + + // GetWorkerIndex returns the worker thread index. + GetWorkerIndex() uint32 +} diff --git a/source/extensions/dynamic_modules/sdk/go/shared/api.go b/source/extensions/dynamic_modules/sdk/go/shared/api.go deleted file mode 100644 index 209d6ab702f16..0000000000000 --- a/source/extensions/dynamic_modules/sdk/go/shared/api.go +++ /dev/null @@ -1,225 +0,0 @@ -//go:generate mockgen -source=api.go -destination=mocks/mock_api.go -package=mocks -package shared - -type HeadersStatus int32 - -const ( - // '2' is preserved for ContinueAndDontEndStream and is not exposed here. - - // HeadersStatusContinue indicates that the headers can continue to be processed by - // next plugin in the chain and nothing will be stopped. - HeadersStatusContinue HeadersStatus = 0 - // HeadersStatusStop indicates that the headers processing should stop at this plugin. - // And when the body or trailers are received, the onRequestBody or onRequestTrailers - // of this plugin will be called. And the filter chain will continue or still hang - // based on the returned status of onRequestBody or onRequestTrailers. - // Of course the continueRequestStream or continueResponseStream can be called to continue - // the processing manually. - HeadersStatusStop HeadersStatus = 1 - // HeadersStatusStopAndBuffer indicates that the headers processing should stop at this plugin. - // And even if the body or trailers are received, the onRequestBody or onRequestTrailers - // of this plugin will NOT be called and the body will be buffered. The only way to continue - // the processing is to call continueRequestStream or continueResponseStream manually. - // This is useful when you want to wait a certain condition to be met before continuing - // the processing (For example, waiting for the result of an asynchronous operation). - HeadersStatusStopAllAndBuffer HeadersStatus = 3 - // Similar to HeadersStatusStopAllAndBuffer. But when there are too big body data buffered, - // the HeadersStatusStopAllAndBuffer will result in 413 (Payload Too Large) response to the - // client. But with this status, the watermarking will be used to disable reading from client - // or server. - HeadersStatusStopAllAndWatermark HeadersStatus = 4 - HeadersStatusDefault HeadersStatus = HeadersStatusContinue -) - -type BodyStatus int32 - -const ( - // BodyStatusContinue indicates that the body can continue to be processed by next plugin - // in the chain. And if the onRequestHeaders or onResponseHeaders of this plugin returned - // HeadersStatusStop before, the headers processing will continue. - BodyStatusContinue BodyStatus = 0 - // BodyStatusStopAndBuffer indicates that the body processing should stop at this plugin. - // And the body will be buffered. - BodyStatusStopAndBuffer BodyStatus = 1 - // BodyStatusStopAndWatermark indicates that the body processing should stop at this plugin. - // And watermarking will be used to disable reading from client or server if there are too - // big body data buffered. - BodyStatusStopAndWatermark BodyStatus = 2 - // BodyStatusStopNoBuffer indicates that the body processing should stop at this plugin. - // No body data will be buffered. - BodyStatusStopNoBuffer BodyStatus = 3 - BodyStatusDefault BodyStatus = BodyStatusContinue -) - -type TrailersStatus int32 - -const ( - // TrailersStatusContinue indicates that the trailers can continue to be processed by next plugin - // in the chain. And if the onRequestHeaders, onResponseHeaders, onRequestBody or onResponseBody - // of this plugin have returned stop status before, the processing will continue after this. - TrailersStatusContinue TrailersStatus = 0 - // TrailersStatusStop indicates that the trailers processing should stop at this plugin. The - // only way to continue the processing is to call continueRequestStream or continueResponseStream - // manually. - TrailersStatusStop TrailersStatus = 1 - TrailersStatusDefault TrailersStatus = TrailersStatusContinue -) - -type LocalReplyStatus int32 - -const ( - // LocalReplyStatusContinue indicates that the local reply should continue to be sent after all - // filters are informed. - LocalReplyStatusContinue LocalReplyStatus = 0 - // LocalReplyStatusContinueAndResetStream indicates that the local reply notification should - // continue to all filters, but the stream should be reset instead of sending the local reply. - LocalReplyStatusContinueAndResetStream LocalReplyStatus = 1 - LocalReplyStatusDefault LocalReplyStatus = LocalReplyStatusContinue -) - -// HttpFilter is the interface to implement your own plugin logic. This is a simplified version and could -// not implement flexible stream control. But it should be enough for most of the use cases. -type HttpFilter interface { - // OnRequestHeaders will be called when the request headers are received. - // @Param headers the request headers. - // @Param endOfStream whether this is the end of the stream. - // @Return HeadersStatus the status to control the plugin chain processing. - OnRequestHeaders(headers HeaderMap, endOfStream bool) HeadersStatus - - // OnRequestBody will be called when the request body are received. This may be called multiple times. - // @Param body the request body. - // @Param endOfStream whether this is the end of the stream. - // @Return BodyStatus the status to control the plugin chain processing. - OnRequestBody(body BodyBuffer, endOfStream bool) BodyStatus - - // OnRequestTrailers will be called when the request trailers are received. - // @Param trailers the request trailers. - // @Return TrailersStatus the status to control the plugin chain processing. - OnRequestTrailers(trailers HeaderMap) TrailersStatus - - // OnResponseHeaders will be called when the response headers are received. - // @Param headers the response headers. - // @Param endOfStream whether this is the end of the stream. - // @Return HeadersStatus the status to control the plugin chain processing. - OnResponseHeaders(headers HeaderMap, endOfStream bool) HeadersStatus - - // OnResponseBody will be called when the response body is received. This may be called multiple - // times. - // @Param body the response body. - // @Param endOfStream whether this is the end of the stream. - // @Return BodyStatus the status to control the plugin chain processing. - OnResponseBody(body BodyBuffer, endOfStream bool) BodyStatus - - // OnResponseTrailers will be called when the response trailers are received. - // @Param trailers the response trailers. - // @Return TrailersStatus the status to control the plugin chain processing. - OnResponseTrailers(trailers HeaderMap) TrailersStatus - - // OnStreamComplete is called when the stream processing is complete and before access logs - // are flushed. - // This is a good place to do any final processing or cleanup before the request is fully - // completed. - OnStreamComplete() - - // OnDestroy is called when the HTTP filter instance is being destroyed. This is called - // after OnStreamComplete and access logs are flushed. This is a good place to release - // any per-stream resources. - OnDestroy() - - // OnLocalReply is called when a local reply is being sent. This allows the filter to modify - // the local reply or decide to reset the stream instead. This is called before the local reply - // is sent to the client and before the stream is reset. - // @Param responseCode the response code of the local reply. - // @Param details the details of the local reply. This is usually used to indicate the reason - // for sending the local reply, for example, "buffer overflow" or "rate limit exceeded". - // @Param resetImminent whether the stream is going to be reset after this local reply. This allows - // the filter to decide whether to continue with sending the local reply or just reset the stream. - // @Return LocalReplyStatus the status to control whether to continue with sending the local reply - // or reset the stream. - OnLocalReply(responseCode uint32, details UnsafeEnvoyBuffer, resetImminent bool) LocalReplyStatus -} - -type EmptyHttpFilter struct { -} - -func (p *EmptyHttpFilter) OnRequestHeaders(headers HeaderMap, endOfStream bool) HeadersStatus { - return HeadersStatusDefault -} - -func (p *EmptyHttpFilter) OnRequestBody(body BodyBuffer, endOfStream bool) BodyStatus { - return BodyStatusDefault -} - -func (p *EmptyHttpFilter) OnRequestTrailers(trailers HeaderMap) TrailersStatus { - return TrailersStatusDefault -} - -func (p *EmptyHttpFilter) OnResponseHeaders(headers HeaderMap, endOfStream bool) HeadersStatus { - return HeadersStatusDefault -} - -func (p *EmptyHttpFilter) OnResponseBody(body BodyBuffer, endOfStream bool) BodyStatus { - return BodyStatusDefault -} - -func (p *EmptyHttpFilter) OnResponseTrailers(trailers HeaderMap) TrailersStatus { - return TrailersStatusDefault -} - -func (p *EmptyHttpFilter) OnStreamComplete() { -} - -func (p *EmptyHttpFilter) OnDestroy() { -} - -func (p *EmptyHttpFilter) OnLocalReply(responseCode uint32, details UnsafeEnvoyBuffer, resetImminent bool) LocalReplyStatus { - return LocalReplyStatusDefault -} - -// HttpFilterFactory is the factory interface for creating stream plugins. -// This is used to create instances of the stream plugin at runtime when a new request is received. -// The implementation of this interface should be thread-safe and hold the parsed configuration. -type HttpFilterFactory interface { - // Create creates a HttpFilter instance. - Create(handle HttpFilterHandle) HttpFilter - - // OnDestroy is called when the factory is being destroyed. This is a good place to clean up any - // resources. This usually happens when the configuration is updated and all existing streams - // using this factory are closed. - OnDestroy() -} - -type EmptyHttpFilterFactory struct { -} - -func (f *EmptyHttpFilterFactory) Create(handle HttpFilterHandle) HttpFilter { - return &EmptyHttpFilter{} -} - -func (f *EmptyHttpFilterFactory) OnDestroy() { -} - -// HttpFilterConfigFactory is the factory interface for creating stream plugin configurations. -// This is used to create -// PluginConfig based on the unparsed configuration. The HttpFilterConfigFactory should parse the unparsedConfig -// and create a PluginFactory instance. -// The implementation of this interface should be thread-safe and be stateless in most cases. -type HttpFilterConfigFactory interface { - // Create creates a HttpFilterFactory based on the unparsed configuration. - Create(handle HttpFilterConfigHandle, unparsedConfig []byte) (HttpFilterFactory, error) - - // CreatePerRoute creates a per-route configuration based on the unparsed configuration. - CreatePerRoute(unparsedConfig []byte) (any, error) -} - -type EmptyHttpFilterConfigFactory struct { -} - -func (f *EmptyHttpFilterConfigFactory) Create(handle HttpFilterConfigHandle, - unparsedConfig []byte) (HttpFilterFactory, error) { - return &EmptyHttpFilterFactory{}, nil -} - -func (f *EmptyHttpFilterConfigFactory) CreatePerRoute(unparsedConfig []byte) (any, error) { - return nil, nil -} diff --git a/source/extensions/dynamic_modules/sdk/go/shared/bootstrap.go b/source/extensions/dynamic_modules/sdk/go/shared/bootstrap.go new file mode 100644 index 0000000000000..b4bbad987b367 --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/shared/bootstrap.go @@ -0,0 +1,265 @@ +//go:generate mockgen -source=bootstrap.go -destination=mocks/mock_bootstrap.go -package=mocks +package shared + +// Bootstrap extension SDK surface for dynamic modules. +// +// Mirrors the Rust SDK's `bootstrap` module. Bootstrap extensions are top-level extensions +// configured at server startup; they live for the duration of the Envoy process and have access +// to server-level facilities: timers, file watchers, admin handlers, cluster/listener lifecycle +// notifications, init signaling, and HTTP callouts on the main thread. +// +// Lifecycle: +// 1. BootstrapExtensionConfigFactory.Create is invoked once per configuration on the main +// thread, returning a BootstrapExtension. Envoy automatically registers an init target +// that blocks traffic until the module signals readiness via handle.SignalInitComplete. +// 2. OnNew is called on the main thread once Envoy is ready to invoke per-extension hooks. +// 3. OnServerInitialized fires after the ServerFactoryContext is fully ready. From this +// point on, the module may opt in to cluster/listener lifecycle events. +// 4. OnWorkerThreadInitialized fires once per worker thread. +// 5. OnDrainStarted fires when Envoy begins draining. The module may still use timers and +// HTTP callouts during drain. +// 6. OnShutdown fires on the main thread during ShutdownExit. The module MUST call +// completion exactly once when its async cleanup is done; Envoy waits for it. +// 7. OnDestroy fires when the extension is being destroyed. + +// FileWatcherEvent is a bitmask of file-watcher events. Corresponds to +// envoy_dynamic_module_type_file_watcher_event / Envoy's Filesystem::Watcher::Events. +type FileWatcherEvent uint32 + +const ( + // FileWatcherEventMovedTo — the watched path was moved to (e.g., atomic-replace via + // rename(2)). + FileWatcherEventMovedTo FileWatcherEvent = 0x1 + // FileWatcherEventModified — the watched file's contents were modified. + FileWatcherEventModified FileWatcherEvent = 0x2 +) + +// StatsIterationAction is the action returned from a stats iteration callback to control +// whether iteration continues. +type StatsIterationAction uint32 + +const ( + // StatsIterationActionContinue — keep iterating. + StatsIterationActionContinue StatsIterationAction = 0 + // StatsIterationActionStop — stop iterating. + StatsIterationActionStop StatsIterationAction = 1 +) + +// BootstrapTimer is an opaque handle to a main-thread timer created via +// BootstrapExtensionConfigHandle.NewTimer. The module owns the timer and must call Delete when +// it is no longer needed. Enable/Disable/Enabled MUST all be called on the main thread. +type BootstrapTimer interface { + // Enable arms the timer to fire after delayMs milliseconds. If already armed, the timer is + // reset. + Enable(delayMs uint64) + // Disable disarms the timer without destroying it. The timer can be re-enabled later. + Disable() + // Enabled reports whether the timer is currently armed. + Enabled() bool + // Delete destroys the timer. Calling other methods after Delete is undefined behavior. + Delete() +} + +// BootstrapExtension is the module-side bootstrap extension. All hooks fire on the main +// thread except OnWorkerThreadInitialized (which fires per-worker on the corresponding +// worker thread). +type BootstrapExtension interface { + // OnNew is called when the extension instance is created. + OnNew(handle BootstrapExtensionHandle) + + // OnServerInitialized fires after the ServerFactoryContext is fully initialized. From + // this point on, the ClusterManager and ListenerManager are available — the module may + // call handle.EnableClusterLifecycle / EnableListenerLifecycle. + OnServerInitialized(handle BootstrapExtensionHandle) + + // OnWorkerThreadInitialized fires once per worker thread, on that worker's thread. + OnWorkerThreadInitialized(handle BootstrapExtensionHandle) + + // OnDrainStarted fires on the main thread when Envoy begins draining. The module can + // still use timers and HTTP callouts during drain. + OnDrainStarted(handle BootstrapExtensionHandle) + + // OnShutdown fires on the main thread during ShutdownExit. The module MUST call + // completion() exactly once when its async cleanup is finished; Envoy waits for it + // before terminating. + OnShutdown(handle BootstrapExtensionHandle, completion func()) + + // OnDestroy is called when the extension is being destroyed. + OnDestroy() +} + +// EmptyBootstrapExtension is a no-op BootstrapExtension. OnShutdown immediately calls +// completion. +type EmptyBootstrapExtension struct{} + +func (*EmptyBootstrapExtension) OnNew(_ BootstrapExtensionHandle) {} +func (*EmptyBootstrapExtension) OnServerInitialized(_ BootstrapExtensionHandle) {} +func (*EmptyBootstrapExtension) OnWorkerThreadInitialized(_ BootstrapExtensionHandle) {} +func (*EmptyBootstrapExtension) OnDrainStarted(_ BootstrapExtensionHandle) {} +func (*EmptyBootstrapExtension) OnShutdown(_ BootstrapExtensionHandle, completion func()) { + completion() +} +func (*EmptyBootstrapExtension) OnDestroy() {} + +// BootstrapExtensionConfigFactory is the top-level factory the module registers via +// sdk.RegisterBootstrapExtensionConfigFactories. +type BootstrapExtensionConfigFactory interface { + // Create parses unparsedConfig and returns a BootstrapExtension. The handle is the + // config-context handle that exposes timers, file watchers, admin handlers, scheduling, + // metrics, HTTP callouts, init signaling, and cluster/listener-lifecycle event opt-in. + // + // IMPORTANT: every bootstrap extension is implicitly an init target — Envoy blocks + // startup until the module calls handle.SignalInitComplete. If no async init is needed, + // call it before returning from Create. + Create(handle BootstrapExtensionConfigHandle, unparsedConfig []byte) (BootstrapExtension, error) +} + +// EmptyBootstrapExtensionConfigFactory is a no-op BootstrapExtensionConfigFactory. It signals +// init complete immediately. +type EmptyBootstrapExtensionConfigFactory struct{} + +func (*EmptyBootstrapExtensionConfigFactory) Create(handle BootstrapExtensionConfigHandle, _ []byte) (BootstrapExtension, error) { + handle.SignalInitComplete() + return &EmptyBootstrapExtension{}, nil +} + +// BootstrapClusterLifecycleListener can be implemented by the BootstrapExtensionConfigFactory +// or BootstrapExtension if the module opted in to cluster lifecycle events via +// BootstrapExtensionConfigHandle.EnableClusterLifecycle. Hooks fire on the main thread. +type BootstrapClusterLifecycleListener interface { + // OnClusterAddOrUpdate is called when a cluster is added or updated in the ClusterManager. + OnClusterAddOrUpdate(clusterName string) + // OnClusterRemoval is called when a cluster is removed from the ClusterManager. + OnClusterRemoval(clusterName string) +} + +// BootstrapListenerLifecycleListener can be implemented if the module opted in to listener +// lifecycle events via BootstrapExtensionConfigHandle.EnableListenerLifecycle. Hooks fire on +// the main thread. +type BootstrapListenerLifecycleListener interface { + // OnListenerAddOrUpdate is called when a listener is added or updated in the + // ListenerManager. + OnListenerAddOrUpdate(listenerName string) + // OnListenerRemoval is called when a listener is removed from the ListenerManager. + OnListenerRemoval(listenerName string) +} + +// BootstrapAdminHandler is implemented by the module to handle requests for an admin endpoint +// registered via BootstrapExtensionConfigHandle.RegisterAdminHandler. The module may be the +// BootstrapExtensionConfigFactory itself or a separate object stashed via cookies. +type BootstrapAdminHandler interface { + // HandleAdminRequest is called when the admin endpoint is requested. The module sets the + // response body via handle.SetAdminResponse (only valid during this call) and returns the + // HTTP status code. + HandleAdminRequest(handle BootstrapExtensionConfigHandle, method, path string, body []byte) uint32 +} + +// BootstrapExtensionConfigHandle is the config-context handle. It is valid for the lifetime +// of the BootstrapExtension. Most methods MUST be called on the main thread; cross-thread +// calls should be routed via NewScheduler. +type BootstrapExtensionConfigHandle interface { + // SignalInitComplete signals that the bootstrap extension has finished its + // initialization. Envoy blocks listener traffic until this is called. Modules with no + // async init MUST call this synchronously during Create. + // + // MUST be called on the main thread. + SignalInitComplete() + + // ---- scheduler ---- + + // NewScheduler creates a main-thread scheduler bound to this configuration. The returned + // Scheduler is safe to call from any goroutine; functions scheduled on it will run on + // Envoy's main thread. + NewScheduler() Scheduler + + // ---- HTTP callouts ---- + + // HttpCallout sends an asynchronous HTTP request from the main thread. Result is + // delivered via the supplied HttpCalloutCallback. MUST be called on the main thread (use + // NewScheduler to dispatch from other threads). + HttpCallout(clusterName string, headers [][2]string, body []byte, timeoutMs uint64, + cb HttpCalloutCallback) (HttpCalloutInitResult, uint64) + + // ---- timers / file watchers ---- + + // NewTimer creates a new disabled timer on the main-thread dispatcher. Returns nil on + // failure. MUST be called on the main thread. + NewTimer(onFire func(timer BootstrapTimer)) BootstrapTimer + + // AddFileWatch adds a watch for the given file path. When the file changes, + // onChange(path, events) is invoked on the main thread. The watcher's lifetime is tied + // to the configuration (auto-removed on destroy). MUST be called on the main thread. + AddFileWatch(path string, events FileWatcherEvent, onChange func(path string, events FileWatcherEvent)) bool + + // ---- admin handler ---- + + // RegisterAdminHandler registers a custom admin HTTP endpoint. When pathPrefix is + // requested, handler.HandleAdminRequest is called on the main thread. removable allows + // later RemoveAdminHandler. mutatesServerState marks endpoints that change server state + // (e.g., POST endpoints). + // + // Returns false if admin is unavailable or the prefix is already taken. + RegisterAdminHandler(pathPrefix, helpText string, removable, mutatesServerState bool, handler BootstrapAdminHandler) bool + + // RemoveAdminHandler removes a previously-registered (and removable) admin endpoint. + RemoveAdminHandler(pathPrefix string) bool + + // SetAdminResponse sets the response body for the in-flight admin request. ONLY valid + // inside BootstrapAdminHandler.HandleAdminRequest. Envoy copies the buffer immediately. + SetAdminResponse(responseBody []byte) + + // ---- cluster / listener lifecycle opt-in ---- + + // EnableClusterLifecycle opts the module in to receiving cluster-add/update/removal + // events. The configuration's BootstrapExtension or BootstrapExtensionConfigFactory MUST + // implement BootstrapClusterLifecycleListener. Idempotent: returns false if already + // enabled. + // + // MUST be called on the main thread, typically during or after OnServerInitialized. + EnableClusterLifecycle() bool + + // EnableListenerLifecycle opts the module in to receiving listener-add/update/removal + // events. The configuration's BootstrapExtension or BootstrapExtensionConfigFactory MUST + // implement BootstrapListenerLifecycleListener. Idempotent. + EnableListenerLifecycle() bool + + // ---- labeled metrics ---- + + DefineCounter(name string, labelNames []string) (MetricID, MetricsResult) + IncrementCounter(id MetricID, labelValues []string, value uint64) MetricsResult + DefineGauge(name string, labelNames []string) (MetricID, MetricsResult) + SetGauge(id MetricID, labelValues []string, value uint64) MetricsResult + IncrementGauge(id MetricID, labelValues []string, value uint64) MetricsResult + DecrementGauge(id MetricID, labelValues []string, value uint64) MetricsResult + DefineHistogram(name string, labelNames []string) (MetricID, MetricsResult) + RecordHistogramValue(id MetricID, labelValues []string, value uint64) MetricsResult +} + +// BootstrapExtensionHandle is the per-extension handle passed to BootstrapExtension hooks. It +// exposes server-wide stats access in addition to all the BootstrapExtensionConfigHandle +// methods (via composition: methods are not duplicated here; the runtime gives modules the +// config handle alongside). +// +// In practice, modules typically retain BootstrapExtensionConfigHandle from Create and use it +// throughout. BootstrapExtensionHandle adds read-only access to existing stats. +type BootstrapExtensionHandle interface { + // GetCounterValue returns the current value of a counter by name. Returns 0 and false + // if the counter does not exist. + GetCounterValue(name string) (uint64, bool) + + // GetGaugeValue returns the current value of a gauge by name. Returns 0 and false if the + // gauge does not exist. + GetGaugeValue(name string) (uint64, bool) + + // GetHistogramSummary returns the cumulative sample count and sum for a histogram by + // name. Returns (0, 0, false) if the histogram does not exist. + GetHistogramSummary(name string) (sampleCount uint64, sampleSum float64, ok bool) + + // IterateCounters iterates over all counters in the stats store, calling visit for each. + // Returning StatsIterationActionStop from visit halts iteration. + IterateCounters(visit func(name UnsafeEnvoyBuffer, value uint64) StatsIterationAction) + + // IterateGauges iterates over all gauges in the stats store, calling visit for each. + // Returning StatsIterationActionStop from visit halts iteration. + IterateGauges(visit func(name UnsafeEnvoyBuffer, value uint64) StatsIterationAction) +} diff --git a/source/extensions/dynamic_modules/sdk/go/shared/cert_validator.go b/source/extensions/dynamic_modules/sdk/go/shared/cert_validator.go new file mode 100644 index 0000000000000..b3066328690c2 --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/shared/cert_validator.go @@ -0,0 +1,128 @@ +//go:generate mockgen -source=cert_validator.go -destination=mocks/mock_cert_validator.go -package=mocks +package shared + +// Custom TLS certificate validator SDK surface for dynamic modules. +// +// Mirrors the Rust SDK's `cert_validator` module. A module that exposes a custom certificate +// validator implements CertValidatorConfigFactory and registers it from an init() function via +// sdk.RegisterCertValidatorConfigFactories. It integrates with Envoy's `custom_validator_config` +// in CertificateValidationContext, registered under the `envoy.tls.cert_validator` category. +// +// The module receives DER-encoded certificates during validation and returns a result indicating +// success or failure with optional TLS alert and error details. Asynchronous (Pending) validation +// is not supported. + +// CertValidatorValidationStatus is the overall validation status returned by VerifyCertChain. It +// corresponds to envoy_dynamic_module_type_cert_validator_validation_status, mapping to +// ValidationResults::ValidationStatus in cert_validator.h. +type CertValidatorValidationStatus uint32 + +const ( + // CertValidatorValidationStatusSuccessful indicates the chain was validated. + CertValidatorValidationStatusSuccessful CertValidatorValidationStatus = 0 + // CertValidatorValidationStatusFailed indicates the chain failed validation. + CertValidatorValidationStatusFailed CertValidatorValidationStatus = 1 +) + +// CertValidatorClientValidationStatus is the detailed client validation status. It corresponds +// to Ssl::ClientValidationStatus in ssl_socket_extended_info.h. +type CertValidatorClientValidationStatus uint32 + +const ( + CertValidatorClientValidationStatusNotValidated CertValidatorClientValidationStatus = 0 + CertValidatorClientValidationStatusNoClientCertificate CertValidatorClientValidationStatus = 1 + CertValidatorClientValidationStatusValidated CertValidatorClientValidationStatus = 2 + CertValidatorClientValidationStatusFailed CertValidatorClientValidationStatus = 3 +) + +// CertValidatorValidationResult is the value returned by CertValidator.VerifyCertChain. Use +// CertValidatorContext.SetErrorDetails (during VerifyCertChain) to attach a free-form error +// message visible in stream info / logs. +type CertValidatorValidationResult struct { + // Status is the overall validation status (Successful or Failed). + Status CertValidatorValidationStatus + // DetailedStatus is the detailed client validation status. + DetailedStatus CertValidatorClientValidationStatus + // HasTLSAlert indicates whether TLSAlert is set. + HasTLSAlert bool + // TLSAlert is the TLS alert code to send on failure (e.g. SSL_AD_BAD_CERTIFICATE). Only + // honored if HasTLSAlert is true. + TLSAlert uint8 +} + +// CertValidator is the module-side certificate validator. Implementations must be safe for +// concurrent calls because VerifyCertChain runs on worker threads. +type CertValidator interface { + // VerifyCertChain is called to verify a certificate chain during a TLS handshake. + // + // certs are DER-encoded certificate buffers; the leaf certificate is at index 0. The + // buffers are owned by Envoy and valid only for the duration of the call. hostName is the + // SNI host name; isServer is true on the server side (i.e., when validating client certs). + // + // To attach a human-readable error message, call ctx.SetErrorDetails before returning. + VerifyCertChain(ctx CertValidatorContext, certs []UnsafeEnvoyBuffer, hostName string, isServer bool) CertValidatorValidationResult + + // GetSSLVerifyMode is called during SSL context initialization to get the SSL_VERIFY_* + // flags to apply (e.g. SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT). Returning 0 + // means SSL_VERIFY_NONE. handshakerProvidesCertificates is true when the handshaker + // provides certificates itself. + GetSSLVerifyMode(handshakerProvidesCertificates bool) int32 + + // UpdateDigest is called to contribute to the session-context hash. The module should + // return bytes that uniquely identify its validation configuration so that configuration + // changes invalidate existing TLS sessions. + UpdateDigest() []byte + + // OnDestroy is called when the validator configuration is destroyed. + OnDestroy() +} + +// EmptyCertValidator is a no-op CertValidator. VerifyCertChain returns Successful/Validated; +// GetSSLVerifyMode returns 0; UpdateDigest returns nil. +type EmptyCertValidator struct{} + +func (*EmptyCertValidator) VerifyCertChain(_ CertValidatorContext, _ []UnsafeEnvoyBuffer, _ string, _ bool) CertValidatorValidationResult { + return CertValidatorValidationResult{ + Status: CertValidatorValidationStatusSuccessful, + DetailedStatus: CertValidatorClientValidationStatusValidated, + } +} +func (*EmptyCertValidator) GetSSLVerifyMode(_ bool) int32 { return 0 } +func (*EmptyCertValidator) UpdateDigest() []byte { return nil } +func (*EmptyCertValidator) OnDestroy() {} + +// CertValidatorConfigFactory is the top-level factory the module registers via +// sdk.RegisterCertValidatorConfigFactories. +type CertValidatorConfigFactory interface { + // Create parses unparsedConfig and returns a CertValidator. Returning a non-nil error + // rejects the configuration. + Create(name string, unparsedConfig []byte) (CertValidator, error) +} + +// EmptyCertValidatorConfigFactory is a no-op CertValidatorConfigFactory. +type EmptyCertValidatorConfigFactory struct{} + +func (*EmptyCertValidatorConfigFactory) Create(_ string, _ []byte) (CertValidator, error) { + return &EmptyCertValidator{}, nil +} + +// CertValidatorContext is the per-call handle passed to CertValidator.VerifyCertChain. It is +// valid only for the duration of that call; do not retain it. Methods on this handle are valid +// only inside VerifyCertChain. +type CertValidatorContext interface { + // SetErrorDetails attaches an error message to the failed validation. The message is + // recorded in the connection's stream info / logs. Envoy copies the buffer immediately; + // the module does not need to keep it alive after the call returns. + SetErrorDetails(errorDetails string) + + // SetFilterState stores a string value under key in the connection's filter state with + // Connection lifespan. Returns false if no connection context is available or the key + // already exists and is read-only. + SetFilterState(key, value string) bool + + // GetFilterState retrieves a string value previously stored under key. + // + // NOTE: The buffer is owned by Envoy and only valid for the duration of the + // VerifyCertChain callback. Copy if you need to keep it. + GetFilterState(key string) (UnsafeEnvoyBuffer, bool) +} diff --git a/source/extensions/dynamic_modules/sdk/go/shared/cluster.go b/source/extensions/dynamic_modules/sdk/go/shared/cluster.go new file mode 100644 index 0000000000000..9a0a822a29bfa --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/shared/cluster.go @@ -0,0 +1,314 @@ +//go:generate mockgen -source=cluster.go -destination=mocks/mock_cluster.go -package=mocks +package shared + +import "unsafe" + +// Cluster extension SDK surface for dynamic modules. +// +// Mirrors the Rust SDK's `cluster` module. A cluster extension implements a complete cluster +// type: it manages the host set (add/remove/find hosts), runs an initial discovery phase, +// optionally provides custom load balancing per worker thread, and integrates with the rest of +// Envoy's lifecycle (server_initialized, drain_started, shutdown). +// +// This is the most complex extension surface — it composes lifecycle, host management, and +// (optional) per-worker load balancing in one extension type. +// +// Lifecycle: +// 1. ClusterConfigFactory.Create on the main thread → ClusterFactory. +// 2. ClusterFactory.Create on the main thread → Cluster (one per cluster instance). +// 3. Cluster.OnInit fires on the main thread; the module performs initial host discovery +// and signals readiness via handle.PreInitComplete. Envoy blocks routing to this cluster +// until that signal. +// 4. Cluster.NewLoadBalancer is called once per worker thread, returning a per-worker +// ClusterLoadBalancer. +// 5. ClusterLoadBalancer.ChooseHost is called for each upstream selection on that worker. +// It can resolve synchronously (returning a host) or asynchronously (returning a +// ClusterAsyncHostSelection; the module signals completion via the SDK-provided +// ClusterAsyncCompletion handed to ChooseHost). +// 6. ClusterLoadBalancer.OnHostMembershipUpdate notifies the worker's LB of host-set changes. +// 7. Cluster.OnServerInitialized / OnDrainStarted / OnShutdown fire on the main thread at +// their respective lifecycle stages. OnShutdown MUST call completion exactly once. + +// ClusterHost is an opaque, Envoy-owned pointer to a host in the cluster's host set. The +// module receives ClusterHost values from add-hosts and find-host operations and passes them +// back to host-management and LB-selection callbacks. Modules MUST NOT dereference these +// pointers; they remain valid only while the host is part of the cluster's host set. +// +// The zero value (nil) represents "no host" and is returned by lookup functions when the +// host is not found. +type ClusterHost struct { + // p is the opaque Envoy-owned pointer. Module code MUST NOT dereference this; it is + // strictly a round-trip handle. + p unsafe.Pointer +} + +// IsNil reports whether the host handle is the zero/no-host value. +func (h ClusterHost) IsNil() bool { return h.p == nil } + +// UnsafeClusterHost is internal SDK plumbing — do NOT call from module code. It exposes the +// raw pointer so the ABI bridge can pass it back to Envoy. +// +//go:nosplit +func UnsafeClusterHost(p unsafe.Pointer) ClusterHost { return ClusterHost{p: p} } + +// UnsafeClusterHostPtr is internal SDK plumbing — do NOT call from module code. +// +//go:nosplit +func UnsafeClusterHostPtr(h ClusterHost) unsafe.Pointer { return h.p } + +// ClusterHostSpec describes a host to add to the cluster via ClusterHandle.AddHosts. Region/ +// Zone/SubZone may be empty when no locality is set. MetadataPairs is a flat slice of +// (filterName, key, value) triples; each triple consists of three consecutive strings, all +// values stored as strings on the Envoy side. nil/empty MetadataPairs means no metadata. +type ClusterHostSpec struct { + // Address is the host address in "ip:port" form (e.g. "10.0.0.1:8080"). + Address string + // Weight is the LB weight (1–128). + Weight uint32 + // Region/Zone/SubZone form the host's locality. Empty strings indicate no value. + Region, Zone, SubZone string + // MetadataPairs is a flat slice of (filterName, key, value) triples. + MetadataPairs []string +} + +// ClusterAsyncCompletion is the SDK-provided completion handle passed to +// ClusterLoadBalancer.ChooseHost. When ChooseHost returns asynchronously, the module stores +// this handle and calls Complete exactly once when the selection finishes (unless +// OnCancelHostSelection has already fired). Complete is safe to call from any goroutine. +type ClusterAsyncCompletion interface { + // Complete delivers the final host (or the zero ClusterHost for failure), with a + // free-form details string recorded as the resolution outcome. Calling Complete more + // than once is a no-op; calling Complete after OnCancelHostSelection is a no-op. + Complete(host ClusterHost, details string) +} + +// ClusterAsyncHostSelection is a module-owned handle returned from ClusterLoadBalancer.ChooseHost +// when the selection is performed asynchronously. The module typically stores the +// ClusterAsyncCompletion it was handed and signals completion via that handle; this interface +// exists so the SDK can notify the module of cancellation. +type ClusterAsyncHostSelection interface { + // Cancel is called by the SDK when Envoy cancels async host selection (e.g., the stream + // was destroyed before the module produced a result). After this returns, the module MUST + // NOT call Complete on the associated ClusterAsyncCompletion. The module should release + // any resources tied to the selection. + Cancel() +} + +// Cluster is the module-side cluster object — one instance per cluster configuration. All +// hooks except NewLoadBalancer fire on the main thread. +type Cluster interface { + // OnInit is called when cluster initialization begins. The module should perform initial + // host discovery and call handle.PreInitComplete when the initial set is ready (Envoy + // blocks routing until then). + OnInit(handle ClusterHandle) + + // NewLoadBalancer is called once per worker thread. The returned ClusterLoadBalancer is + // owned by Envoy for the lifetime of the worker; its callbacks fire on that worker. + // Returning nil indicates failure. + NewLoadBalancer(handle ClusterLoadBalancerHandle) ClusterLoadBalancer + + // OnServerInitialized fires after server initialization completes — appropriate for + // starting background discovery tasks that depend on server-wide facilities. + OnServerInitialized(handle ClusterHandle) + + // OnDrainStarted fires when Envoy begins draining. Cluster operations are still allowed. + OnDrainStarted(handle ClusterHandle) + + // OnShutdown fires during ShutdownExit. The module MUST call completion() exactly once + // when its async cleanup is done; Envoy waits for it before terminating. + OnShutdown(handle ClusterHandle, completion func()) + + // OnDestroy is called when the cluster instance is destroyed. + OnDestroy() +} + +// EmptyCluster is a no-op Cluster. OnShutdown calls completion immediately. +type EmptyCluster struct{} + +func (*EmptyCluster) OnInit(handle ClusterHandle) { handle.PreInitComplete() } +func (*EmptyCluster) NewLoadBalancer(_ ClusterLoadBalancerHandle) ClusterLoadBalancer { return nil } +func (*EmptyCluster) OnServerInitialized(_ ClusterHandle) {} +func (*EmptyCluster) OnDrainStarted(_ ClusterHandle) {} +func (*EmptyCluster) OnShutdown(_ ClusterHandle, completion func()) { completion() } +func (*EmptyCluster) OnDestroy() {} + +// ClusterFactory creates the per-cluster Cluster instance. +type ClusterFactory interface { + Create(handle ClusterConfigHandle) Cluster + OnDestroy() +} + +// EmptyClusterFactory is a no-op ClusterFactory. +type EmptyClusterFactory struct{} + +func (*EmptyClusterFactory) Create(_ ClusterConfigHandle) Cluster { return &EmptyCluster{} } +func (*EmptyClusterFactory) OnDestroy() {} + +// ClusterConfigFactory is the top-level factory the module registers via +// sdk.RegisterClusterConfigFactories. +type ClusterConfigFactory interface { + Create(handle ClusterConfigHandle, unparsedConfig []byte) (ClusterFactory, error) +} + +// EmptyClusterConfigFactory is a no-op ClusterConfigFactory. +type EmptyClusterConfigFactory struct{} + +func (*EmptyClusterConfigFactory) Create(_ ClusterConfigHandle, _ []byte) (ClusterFactory, error) { + return &EmptyClusterFactory{}, nil +} + +// ClusterConfigHandle is the config-context handle. Supports labeled metrics. +type ClusterConfigHandle interface { + DefineCounter(name string, labelNames []string) (MetricID, MetricsResult) + IncrementCounter(id MetricID, labelValues []string, value uint64) MetricsResult + DefineGauge(name string, labelNames []string) (MetricID, MetricsResult) + SetGauge(id MetricID, labelValues []string, value uint64) MetricsResult + IncrementGauge(id MetricID, labelValues []string, value uint64) MetricsResult + DecrementGauge(id MetricID, labelValues []string, value uint64) MetricsResult + DefineHistogram(name string, labelNames []string) (MetricID, MetricsResult) + RecordHistogramValue(id MetricID, labelValues []string, value uint64) MetricsResult +} + +// ClusterHandle is the per-cluster handle for managing hosts, scheduling main-thread events, +// and issuing HTTP callouts. Methods MUST be called on the main thread (use NewScheduler to +// dispatch from other threads). +type ClusterHandle interface { + // PreInitComplete signals that initial host discovery is done; Envoy starts routing to + // the cluster after this call. The module MUST call this during or after OnInit. + PreInitComplete() + + // AddHosts adds hosts to the cluster at the given priority. Returns the resulting + // ClusterHost handles in the same order as specs (or nil on failure — when any host fails + // to add, no hosts are added). + AddHosts(priority uint32, specs []ClusterHostSpec) ([]ClusterHost, bool) + + // RemoveHosts removes the given hosts from the cluster. Returns the number that were + // successfully removed. + RemoveHosts(hosts []ClusterHost) uint64 + + // UpdateHostHealth updates a host's health status. Useful when the module manages health + // externally (custom health probes, EDS-like signals). + UpdateHostHealth(host ClusterHost, status HostHealth) bool + + // FindHostByAddress looks up a host by its "ip:port" address. Returns 0 if not found. + FindHostByAddress(address string) ClusterHost + + // HttpCallout sends a main-thread HTTP request. Result is delivered via the supplied + // HttpCalloutCallback. MUST be called on the main thread. + HttpCallout(clusterName string, headers [][2]string, body []byte, timeoutMs uint64, + cb HttpCalloutCallback) (HttpCalloutInitResult, uint64) + + // NewScheduler returns a main-thread scheduler bound to this cluster. Safe from any + // goroutine. + NewScheduler() Scheduler +} + +// ClusterLoadBalancer is the per-worker LB associated with a Cluster. ChooseHost is called for +// every upstream selection on this worker. +type ClusterLoadBalancer interface { + // ChooseHost picks a host for the request. completion is the SDK-provided completion + // handle the module uses to signal an asynchronous result. Returns: + // - (host, nil, true) for synchronous success (completion can be ignored) + // - (zero, async, true) when async resolution is in flight; the module MUST eventually + // call completion.Complete (unless async.Cancel fires first) + // - (zero, nil, false) for synchronous failure (no host selected; request fails) + ChooseHost(handle ClusterLoadBalancerHandle, ctx ClusterLoadBalancerContext, + completion ClusterAsyncCompletion) (ClusterHost, ClusterAsyncHostSelection, bool) + + // OnHostMembershipUpdate notifies the per-worker LB of host-set changes. During the + // callback the module can enumerate added/removed hosts via + // handle.GetMemberUpdateHostAddress. + OnHostMembershipUpdate(handle ClusterLoadBalancerHandle, numHostsAdded, numHostsRemoved uint64) + + // OnDestroy is called when the per-worker LB is destroyed. + OnDestroy() +} + +// EmptyClusterLoadBalancer is a no-op ClusterLoadBalancer that always returns sync failure. +type EmptyClusterLoadBalancer struct{} + +func (*EmptyClusterLoadBalancer) ChooseHost(_ ClusterLoadBalancerHandle, _ ClusterLoadBalancerContext, _ ClusterAsyncCompletion) (ClusterHost, ClusterAsyncHostSelection, bool) { + return ClusterHost{}, nil, false +} +func (*EmptyClusterLoadBalancer) OnHostMembershipUpdate(_ ClusterLoadBalancerHandle, _, _ uint64) {} +func (*EmptyClusterLoadBalancer) OnDestroy() {} + +// ClusterLoadBalancerHandle is the per-worker LB handle. All methods MUST be called on the +// owning worker thread (i.e., from inside a ClusterLoadBalancer callback). +// +// This mirrors LoadBalancerHandle but is bound to a cluster's per-worker LB pointer rather +// than a standalone load-balancer pointer. The two cannot be used interchangeably. +type ClusterLoadBalancerHandle interface { + // ---- cluster-level info ---- + + GetClusterName() UnsafeEnvoyBuffer + + // ---- host queries ---- + + GetHostsCount(priority uint32) uint64 + GetHealthyHostCount(priority uint32) uint64 + GetDegradedHostsCount(priority uint32) uint64 + GetPrioritySetSize() uint64 + + // GetHealthyHost returns a healthy-host handle by index (or 0 if out of bounds). + GetHealthyHost(priority uint32, index uint64) ClusterHost + GetHealthyHostAddress(priority uint32, index uint64) (UnsafeEnvoyBuffer, bool) + GetHealthyHostWeight(priority uint32, index uint64) uint32 + + // GetHost returns an all-hosts handle by index (or 0 if out of bounds). + GetHost(priority uint32, index uint64) ClusterHost + GetHostAddress(priority uint32, index uint64) (UnsafeEnvoyBuffer, bool) + GetHostWeight(priority uint32, index uint64) uint32 + GetHostHealth(priority uint32, index uint64) HostHealth + GetHostHealthByAddress(address string) (HostHealth, bool) + GetHostStat(priority uint32, index uint64, stat HostStat) uint64 + GetHostLocality(priority uint32, index uint64) (region, zone, subZone UnsafeEnvoyBuffer, ok bool) + + // FindHostByAddress is the per-worker counterpart to ClusterHandle.FindHostByAddress; + // safe to call during host selection. + FindHostByAddress(address string) ClusterHost + + // SetHostData / GetHostData attach/retrieve a module-defined opaque uintptr per host, + // per worker (NOT shared across workers). + SetHostData(priority uint32, index uint64, data uintptr) bool + GetHostData(priority uint32, index uint64) (uintptr, bool) + + // Host metadata (per-endpoint). + GetHostMetadataString(priority uint32, index uint64, filterName, key string) (UnsafeEnvoyBuffer, bool) + GetHostMetadataNumber(priority uint32, index uint64, filterName, key string) (float64, bool) + GetHostMetadataBool(priority uint32, index uint64, filterName, key string) (bool, bool) + + // ---- locality buckets (healthy hosts) ---- + + GetLocalityCount(priority uint32) uint64 + GetLocalityHostCount(priority uint32, localityIndex uint64) uint64 + GetLocalityHostAddress(priority uint32, localityIndex, hostIndex uint64) (UnsafeEnvoyBuffer, bool) + GetLocalityWeight(priority uint32, localityIndex uint64) uint32 + + // ---- membership update enumeration ---- + + // GetMemberUpdateHostAddress returns the address of an added (isAdded=true) or removed + // (false) host at the given index. Only valid inside OnHostMembershipUpdate. + GetMemberUpdateHostAddress(index uint64, isAdded bool) (UnsafeEnvoyBuffer, bool) +} + +// ClusterLoadBalancerContext is the per-request context passed to ChooseHost. Memory accessed +// through this handle is only valid for the duration of the ChooseHost callback (synchronous +// case) or until the async-handle is completed/cancelled (async case). +type ClusterLoadBalancerContext interface { + ComputeHashKey() (uint64, bool) + GetDownstreamHeadersSize() uint64 + GetDownstreamHeaders() [][2]UnsafeEnvoyBuffer + GetDownstreamHeader(key string, index uint64) (UnsafeEnvoyBuffer, uint64, bool) + GetHostSelectionRetryCount() uint32 + // ShouldSelectAnotherHost asks Envoy whether the candidate host at (priority, index) should + // be skipped. handle is accepted for interface symmetry but implementations use the LB + // pointer captured when the context was created; callers MUST pass the handle they received + // from ChooseHost. + ShouldSelectAnotherHost(handle ClusterLoadBalancerHandle, priority uint32, index uint64) bool + GetOverrideHost() (address UnsafeEnvoyBuffer, strict bool, ok bool) + + // GetDownstreamConnectionSNI returns the SNI from the downstream connection associated + // with this request, if any. + GetDownstreamConnectionSNI() (UnsafeEnvoyBuffer, bool) +} diff --git a/source/extensions/dynamic_modules/sdk/go/shared/dns_resolver.go b/source/extensions/dynamic_modules/sdk/go/shared/dns_resolver.go new file mode 100644 index 0000000000000..11cc5a2f76a24 --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/shared/dns_resolver.go @@ -0,0 +1,169 @@ +//go:generate mockgen -source=dns_resolver.go -destination=mocks/mock_dns_resolver.go -package=mocks +package shared + +// DNS resolver SDK surface for dynamic modules. +// +// Mirrors the Rust SDK's `dns_resolver` module. A module that exposes a custom DNS resolver +// implements DnsResolverConfigFactory and registers it from an init() function via +// sdk.RegisterDnsResolverConfigFactories. +// +// Lifecycle: Envoy calls DnsResolverConfigFactory.Create exactly once per resolver +// configuration. Each Resolve call creates a query that the module executes asynchronously and +// completes by invoking DnsResolverConfigHandle.ResolveComplete on any thread (Envoy posts the +// result to the correct dispatcher thread internally). + +// DnsLookupFamily specifies which address families to look up. Corresponds to +// envoy_dynamic_module_type_dns_lookup_family / Network::DnsLookupFamily in Envoy. +type DnsLookupFamily uint32 + +const ( + // DnsLookupFamilyV4Only — only A records. + DnsLookupFamilyV4Only DnsLookupFamily = iota + // DnsLookupFamilyV6Only — only AAAA records. + DnsLookupFamilyV6Only + // DnsLookupFamilyAuto — prefer the family of the source address; fall back to either. + DnsLookupFamilyAuto + // DnsLookupFamilyV4Preferred — prefer A records; fall back to AAAA. + DnsLookupFamilyV4Preferred + // DnsLookupFamilyAll — return both A and AAAA records. + DnsLookupFamilyAll +) + +// DnsResolutionStatus is the final status of a DNS resolution. Corresponds to +// envoy_dynamic_module_type_dns_resolution_status / Network::DnsResolver::ResolutionStatus. +type DnsResolutionStatus uint32 + +const ( + // DnsResolutionStatusCompleted — the resolution completed (with zero or more addresses). + DnsResolutionStatusCompleted DnsResolutionStatus = iota + // DnsResolutionStatusFailure — the resolution failed. + DnsResolutionStatusFailure +) + +// DnsAddress is a single resolved DNS address with TTL. +type DnsAddress struct { + // Address is an "ip:port" string. The port MUST be 0 because DNS resolution only produces + // IP addresses; the actual port comes from the cluster/endpoint configuration. + Address string + // TTLSeconds is the time-to-live in seconds. + TTLSeconds uint32 +} + +// DnsResolver is the module-side DNS resolver. A single instance is created per Envoy DNS +// resolver and is invoked for every resolution request on that resolver. Implementations must +// be safe for concurrent calls; Envoy may issue multiple Resolve calls in parallel. +// +// Resolution is asynchronous: the module starts the lookup and returns immediately. When the +// result is available, the module MUST call handle.ResolveComplete with the same queryID it was +// given. If the query is cancelled (Cancel) or the resolver is destroyed, the module MUST NOT +// call ResolveComplete for that query. +type DnsResolver interface { + // Resolve initiates an asynchronous DNS resolution. The module SHOULD start the lookup + // (typically on a background thread) and return a handle/query identifier to its in-flight + // state. handle.ResolveComplete MUST be called once for each successful Resolve unless + // Cancel is invoked. Returning nil indicates the resolution could not be started. + // + // The returned query identifier is opaque — it is only used by the runtime to drive Cancel + // and is not the same as queryID (which is Envoy's identifier for the result delivery). + Resolve(handle DnsResolverConfigHandle, dnsName string, family DnsLookupFamily, queryID uint64) any + + // Cancel cancels an in-flight query that was previously returned by Resolve. After this + // call, the module MUST NOT call handle.ResolveComplete for the cancelled query. The + // module should clean up any resources associated with the query. + Cancel(query any) + + // ResetNetworking is called to reset the resolver's networking state, typically in + // response to a network change (e.g., WiFi to cellular). The module may recreate + // connections, re-read system configuration, etc. + ResetNetworking() + + // OnDestroy is called when the resolver instance is destroyed. The module should release + // the resolver and shut down any background threads. + OnDestroy() +} + +// EmptyDnsResolver is a no-op DnsResolver. Resolve returns nil; ResetNetworking and OnDestroy +// do nothing. +type EmptyDnsResolver struct{} + +func (*EmptyDnsResolver) Resolve(_ DnsResolverConfigHandle, _ string, _ DnsLookupFamily, _ uint64) any { + return nil +} +func (*EmptyDnsResolver) Cancel(_ any) {} +func (*EmptyDnsResolver) ResetNetworking() {} +func (*EmptyDnsResolver) OnDestroy() {} + +// DnsResolverFactory creates the per-Envoy-resolver DnsResolver instance. +type DnsResolverFactory interface { + // Create creates the DnsResolver for an Envoy DNS resolver instance. + Create(handle DnsResolverConfigHandle) DnsResolver + + // OnDestroy is called when the factory itself (the configuration) is destroyed. + OnDestroy() +} + +// EmptyDnsResolverFactory is a no-op DnsResolverFactory. +type EmptyDnsResolverFactory struct{} + +func (*EmptyDnsResolverFactory) Create(_ DnsResolverConfigHandle) DnsResolver { + return &EmptyDnsResolver{} +} +func (*EmptyDnsResolverFactory) OnDestroy() {} + +// DnsResolverConfigFactory is the top-level factory the module registers via +// sdk.RegisterDnsResolverConfigFactories. +type DnsResolverConfigFactory interface { + // Create parses unparsedConfig and returns a DnsResolverFactory. + Create(handle DnsResolverConfigHandle, unparsedConfig []byte) (DnsResolverFactory, error) +} + +// EmptyDnsResolverConfigFactory is a no-op DnsResolverConfigFactory. +type EmptyDnsResolverConfigFactory struct{} + +func (*EmptyDnsResolverConfigFactory) Create(_ DnsResolverConfigHandle, _ []byte) (DnsResolverFactory, error) { + return &EmptyDnsResolverFactory{}, nil +} + +// DnsResolverConfigHandle is the handle used by the module to call back into Envoy. It is valid +// from when the configuration is created until the corresponding factory's OnDestroy returns. +// +// ResolveComplete is safe to call from any thread; Envoy posts the result to the correct +// dispatcher. +// +// Metric definitions on this handle support label names; the values passed at increment-time +// MUST match the order and length of the names declared at definition-time. +type DnsResolverConfigHandle interface { + // ResolveComplete delivers DNS resolution results back to Envoy. queryID MUST match the + // queryID supplied to DnsResolver.Resolve. status indicates success/failure; details is a + // human-readable message; addresses is the resolved set with TTLs. + // + // Safe to call from any thread; Envoy will post the result onto the correct dispatcher. + // Buffer data is copied synchronously before the call returns. + ResolveComplete(queryID uint64, status DnsResolutionStatus, details string, addresses []DnsAddress) + + // DefineCounter creates a per-config counter template with the given label names. Labels + // passed at increment-time MUST match this order and length. + DefineCounter(name string, labelNames []string) (MetricID, MetricsResult) + + // IncrementCounter increments a counter by value. labelValues MUST match the labels + // declared in DefineCounter (length and order). + IncrementCounter(id MetricID, labelValues []string, value uint64) MetricsResult + + // DefineGauge creates a per-config gauge template with the given label names. + DefineGauge(name string, labelNames []string) (MetricID, MetricsResult) + + // SetGauge sets a gauge to value. + SetGauge(id MetricID, labelValues []string, value uint64) MetricsResult + + // IncrementGauge adds value to a gauge. + IncrementGauge(id MetricID, labelValues []string, value uint64) MetricsResult + + // DecrementGauge subtracts value from a gauge. + DecrementGauge(id MetricID, labelValues []string, value uint64) MetricsResult + + // DefineHistogram creates a per-config histogram template with the given label names. + DefineHistogram(name string, labelNames []string) (MetricID, MetricsResult) + + // RecordHistogramValue records value in a histogram. + RecordHistogramValue(id MetricID, labelValues []string, value uint64) MetricsResult +} diff --git a/source/extensions/dynamic_modules/sdk/go/shared/empty_test.go b/source/extensions/dynamic_modules/sdk/go/shared/empty_test.go new file mode 100644 index 0000000000000..ef5f201a5f357 --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/shared/empty_test.go @@ -0,0 +1,202 @@ +package shared + +import "testing" + +// Compile-time interface conformance for every EmptyXxx helper. If a future change adds a +// method to one of the interfaces below and the corresponding Empty implementation is +// missed, this file fails to compile — surfaced as a fast feedback loop instead of waiting +// for a downstream module's build to break. +// +// Each line is paired so you can read "this Empty satisfies that interface". + +// Access logger surfaces. +var ( + _ AccessLoggerConfigFactory = (*EmptyAccessLoggerConfigFactory)(nil) + _ AccessLoggerFactory = (*EmptyAccessLoggerFactory)(nil) + _ AccessLogger = (*EmptyAccessLogger)(nil) +) + +// Bootstrap surfaces. +var ( + _ BootstrapExtensionConfigFactory = (*EmptyBootstrapExtensionConfigFactory)(nil) + _ BootstrapExtension = (*EmptyBootstrapExtension)(nil) +) + +// Cert validator surfaces. +var ( + _ CertValidatorConfigFactory = (*EmptyCertValidatorConfigFactory)(nil) + _ CertValidator = (*EmptyCertValidator)(nil) +) + +// Cluster surfaces. +var ( + _ ClusterConfigFactory = (*EmptyClusterConfigFactory)(nil) + _ ClusterFactory = (*EmptyClusterFactory)(nil) + _ Cluster = (*EmptyCluster)(nil) + _ ClusterLoadBalancer = (*EmptyClusterLoadBalancer)(nil) +) + +// DNS resolver surfaces. +var ( + _ DnsResolverConfigFactory = (*EmptyDnsResolverConfigFactory)(nil) + _ DnsResolverFactory = (*EmptyDnsResolverFactory)(nil) + _ DnsResolver = (*EmptyDnsResolver)(nil) +) + +// HTTP filter surfaces. +var ( + _ HttpFilterConfigFactory = (*EmptyHttpFilterConfigFactory)(nil) + _ HttpFilterFactory = (*EmptyHttpFilterFactory)(nil) + _ HttpFilter = (*EmptyHttpFilter)(nil) +) + +// Listener filter surfaces. +var ( + _ ListenerFilterConfigFactory = (*EmptyListenerFilterConfigFactory)(nil) + _ ListenerFilterFactory = (*EmptyListenerFilterFactory)(nil) + _ ListenerFilter = (*EmptyListenerFilter)(nil) +) + +// Load balancer surfaces. +var ( + _ LoadBalancerConfigFactory = (*EmptyLoadBalancerConfigFactory)(nil) + _ LoadBalancerFactory = (*EmptyLoadBalancerFactory)(nil) + _ LoadBalancer = (*EmptyLoadBalancer)(nil) +) + +// Matcher surfaces. +var ( + _ MatcherConfigFactory = (*EmptyMatcherConfigFactory)(nil) + _ Matcher = (*EmptyMatcher)(nil) +) + +// Network filter surfaces. +var ( + _ NetworkFilterConfigFactory = (*EmptyNetworkFilterConfigFactory)(nil) + _ NetworkFilterFactory = (*EmptyNetworkFilterFactory)(nil) + _ NetworkFilter = (*EmptyNetworkFilter)(nil) +) + +// Tracer surfaces. +var ( + _ TracerConfigFactory = (*EmptyTracerConfigFactory)(nil) + _ Tracer = (*EmptyTracer)(nil) + _ TracerSpan = (*EmptyTracerSpan)(nil) +) + +// Transport socket surfaces. +var ( + _ TransportSocketFactoryConfigFactory = (*EmptyTransportSocketFactoryConfigFactory)(nil) + _ TransportSocketFactory = (*EmptyTransportSocketFactory)(nil) + _ TransportSocket = (*EmptyTransportSocket)(nil) +) + +// UDP listener filter surfaces. +var ( + _ UdpListenerFilterConfigFactory = (*EmptyUdpListenerFilterConfigFactory)(nil) + _ UdpListenerFilterFactory = (*EmptyUdpListenerFilterFactory)(nil) + _ UdpListenerFilter = (*EmptyUdpListenerFilter)(nil) +) + +// Upstream HTTP/TCP bridge surfaces. +var ( + _ UpstreamHttpTcpBridgeConfigFactory = (*EmptyUpstreamHttpTcpBridgeConfigFactory)(nil) + _ UpstreamHttpTcpBridgeFactory = (*EmptyUpstreamHttpTcpBridgeFactory)(nil) + _ UpstreamHttpTcpBridge = (*EmptyUpstreamHttpTcpBridge)(nil) +) + +// ----------------------------------------------------------------------------- +// Runtime no-panic checks for the side-effect-free Emptys. +// +// EmptyBootstrapExtensionConfigFactory.Create is intentionally NOT called here: it invokes +// handle.SignalInitComplete on the supplied handle, which is a real side effect, not a +// no-op. Constructing a stub handle that satisfies the 30+ method +// BootstrapExtensionConfigHandle interface just to exercise a side-effecting Create would +// add maintenance cost without locking down anything the compile-time check above doesn't +// already cover. +// ----------------------------------------------------------------------------- + +func TestEmptyConfigFactories_CreateDoesNotPanic(t *testing.T) { + // Each Create returns the documented (factory, nil) pair. The exact return values + // aren't important — just that the call doesn't panic and returns a non-nil factory + // where applicable. + + if f, err := (&EmptyAccessLoggerConfigFactory{}).Create(nil, nil); err != nil || f == nil { + t.Errorf("EmptyAccessLoggerConfigFactory.Create returned (%v, %v)", f, err) + } + if f, err := (&EmptyCertValidatorConfigFactory{}).Create("", nil); err != nil || f == nil { + t.Errorf("EmptyCertValidatorConfigFactory.Create returned (%v, %v)", f, err) + } + if f, err := (&EmptyClusterConfigFactory{}).Create(nil, nil); err != nil || f == nil { + t.Errorf("EmptyClusterConfigFactory.Create returned (%v, %v)", f, err) + } + if f, err := (&EmptyDnsResolverConfigFactory{}).Create(nil, nil); err != nil || f == nil { + t.Errorf("EmptyDnsResolverConfigFactory.Create returned (%v, %v)", f, err) + } + if f, err := (&EmptyHttpFilterConfigFactory{}).Create(nil, nil); err != nil || f == nil { + t.Errorf("EmptyHttpFilterConfigFactory.Create returned (%v, %v)", f, err) + } + if f, err := (&EmptyListenerFilterConfigFactory{}).Create(nil, nil); err != nil || f == nil { + t.Errorf("EmptyListenerFilterConfigFactory.Create returned (%v, %v)", f, err) + } + if f, err := (&EmptyLoadBalancerConfigFactory{}).Create(nil, nil); err != nil || f == nil { + t.Errorf("EmptyLoadBalancerConfigFactory.Create returned (%v, %v)", f, err) + } + if f, err := (&EmptyMatcherConfigFactory{}).Create("", nil); err != nil || f == nil { + t.Errorf("EmptyMatcherConfigFactory.Create returned (%v, %v)", f, err) + } + if f, err := (&EmptyNetworkFilterConfigFactory{}).Create(nil, nil); err != nil || f == nil { + t.Errorf("EmptyNetworkFilterConfigFactory.Create returned (%v, %v)", f, err) + } + if f, err := (&EmptyTracerConfigFactory{}).Create(nil, nil); err != nil || f == nil { + t.Errorf("EmptyTracerConfigFactory.Create returned (%v, %v)", f, err) + } + if f, err := (&EmptyTransportSocketFactoryConfigFactory{}).Create("", nil, false); err != nil || f == nil { + t.Errorf("EmptyTransportSocketFactoryConfigFactory.Create returned (%v, %v)", f, err) + } + if f, err := (&EmptyUdpListenerFilterConfigFactory{}).Create(nil, nil); err != nil || f == nil { + t.Errorf("EmptyUdpListenerFilterConfigFactory.Create returned (%v, %v)", f, err) + } + if f, err := (&EmptyUpstreamHttpTcpBridgeConfigFactory{}).Create("", nil); err != nil || f == nil { + t.Errorf("EmptyUpstreamHttpTcpBridgeConfigFactory.Create returned (%v, %v)", f, err) + } +} + +func TestEmptyOnDestroyDoesNotPanic(t *testing.T) { + // Every Empty type has an OnDestroy(); none should panic on a zero receiver. This is + // the universal teardown contract. + (&EmptyAccessLoggerFactory{}).OnDestroy() + (&EmptyAccessLogger{}).OnDestroy() + (&EmptyBootstrapExtension{}).OnDestroy() + (&EmptyCertValidator{}).OnDestroy() + (&EmptyClusterFactory{}).OnDestroy() + (&EmptyCluster{}).OnDestroy() + (&EmptyClusterLoadBalancer{}).OnDestroy() + (&EmptyDnsResolverFactory{}).OnDestroy() + (&EmptyDnsResolver{}).OnDestroy() + (&EmptyHttpFilterFactory{}).OnDestroy() + (&EmptyHttpFilter{}).OnDestroy() + (&EmptyListenerFilterFactory{}).OnDestroy() + (&EmptyListenerFilter{}).OnDestroy() + (&EmptyLoadBalancerFactory{}).OnDestroy() + (&EmptyLoadBalancer{}).OnDestroy() + (&EmptyMatcher{}).OnDestroy() + (&EmptyNetworkFilterFactory{}).OnDestroy() + (&EmptyNetworkFilter{}).OnDestroy() + (&EmptyTracer{}).OnDestroy() + (&EmptyTracerSpan{}).OnDestroy() + (&EmptyTransportSocketFactory{}).OnDestroy() + (&EmptyTransportSocket{}).OnDestroy() + (&EmptyUdpListenerFilterFactory{}).OnDestroy() + (&EmptyUdpListenerFilter{}).OnDestroy() + (&EmptyUpstreamHttpTcpBridgeFactory{}).OnDestroy() + (&EmptyUpstreamHttpTcpBridge{}).OnDestroy() +} + +// EmptyAccessLogger has Log/Flush in addition to OnDestroy. Verify these are no-ops too. +func TestEmptyAccessLogger_NoopMethods(t *testing.T) { + l := &EmptyAccessLogger{} + l.Log(nil, AccessLogTypeNotSet) + l.Flush() + l.OnDestroy() +} diff --git a/source/extensions/dynamic_modules/sdk/go/shared/fake/fake_access_log.go b/source/extensions/dynamic_modules/sdk/go/shared/fake/fake_access_log.go new file mode 100644 index 0000000000000..dacd171e900de --- /dev/null +++ b/source/extensions/dynamic_modules/sdk/go/shared/fake/fake_access_log.go @@ -0,0 +1,368 @@ +package fake + +import ( + "strings" + "unsafe" + + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +var _ shared.AccessLogContext = (*FakeAccessLogContext)(nil) + +// FakeAccessLogContext is an in-memory implementation of shared.AccessLogContext for tests. +// +// All fields are public so tests can populate exactly the data their access logger reads. +// Any field left at its zero value behaves like "not set" — getters return an empty +// UnsafeEnvoyBuffer and false, getters that return only a value return zero. +// +// Headers are case-insensitive. Use NewFakeAccessLogContext to populate them; direct +// assignment to the maps is also supported but expects already-lowercased keys. +type FakeAccessLogContext struct { + // Headers indexed by HttpHeaderType. Each entry is a slice of [name, value] pairs in + // insertion order; the same name may appear multiple times. + Headers map[shared.HttpHeaderType][][2]string + + // Attribute values keyed by AttributeID. Only the type-appropriate map is consulted by the + // matching Get*. Tests should populate the right map for the AttributeID. + StringAttributes map[shared.AttributeID]string + NumberAttributes map[shared.AttributeID]uint64 + BoolAttributes map[shared.AttributeID]bool + + // Response flag bitmask. Bit i corresponds to ResponseFlag(i). + ResponseFlags uint64 + + TimingInfo shared.AccessLogTimingInfo + BytesInfo shared.AccessLogBytesInfo + + // ---- addresses (port = 0 + ok = false when address is empty) ---- + DownstreamRemoteAddress string + DownstreamRemotePort uint32 + DownstreamLocalAddress string + DownstreamLocalPort uint32 + DownstreamDirectRemoteAddress string + DownstreamDirectRemotePort uint32 + DownstreamDirectLocalAddress string + DownstreamDirectLocalPort uint32 + UpstreamRemoteAddress string + UpstreamRemotePort uint32 + UpstreamLocalAddress string + UpstreamLocalPort uint32 + + // ---- upstream ---- + UpstreamCluster string + UpstreamHost string + UpstreamConnectionID uint64 + UpstreamTLSCipher string + UpstreamTLSSessionID string + UpstreamPeerIssuer string + + UpstreamPeerCertValidityStart int64 + UpstreamPeerCertValidityEnd int64 + + UpstreamPeerURISans []string + UpstreamLocalURISans []string + UpstreamPeerDNSSans []string + UpstreamLocalDNSSans []string + + // ---- downstream TLS ---- + DownstreamTLSCipher string + DownstreamTLSSessionID string + DownstreamPeerIssuer string + DownstreamPeerSerial string + DownstreamPeerFingerprint1 string + + DownstreamPeerCertPresented bool + DownstreamPeerCertValidated bool + DownstreamPeerCertValidityStart int64 + DownstreamPeerCertValidityEnd int64 + + DownstreamPeerURISans []string + DownstreamLocalURISans []string + DownstreamPeerDNSSans []string + DownstreamLocalDNSSans []string + + // ---- metadata / filter state / tracing ---- + // DynamicMetadata is keyed by "/" (no leading or trailing slash). + DynamicMetadata map[string]string + FilterState map[string]string + + LocalReplyBody string + TraceID string + SpanID string + TraceSampled bool + + JA3Hash string + JA4Hash string + + RequestHeadersBytes uint64 + ResponseHeadersBytes uint64 + ResponseTrailersBytes uint64 + UpstreamProtocol string + UpstreamPoolReadyDurationNs int64 + + WorkerIndex uint32 +} + +// NewFakeAccessLogContext returns a FakeAccessLogContext with empty (but non-nil) maps so +// tests can call Set* helpers on the returned value without nil-checking. +func NewFakeAccessLogContext() *FakeAccessLogContext { + return &FakeAccessLogContext{ + Headers: make(map[shared.HttpHeaderType][][2]string), + StringAttributes: make(map[shared.AttributeID]string), + NumberAttributes: make(map[shared.AttributeID]uint64), + BoolAttributes: make(map[shared.AttributeID]bool), + DynamicMetadata: make(map[string]string), + FilterState: make(map[string]string), + } +} + +// AddHeader appends a header to the given header map. Names are normalized to lowercase. +func (c *FakeAccessLogContext) AddHeader(headerType shared.HttpHeaderType, name, value string) { + c.Headers[headerType] = append(c.Headers[headerType], [2]string{strings.ToLower(name), value}) +} + +// ---- headers ---- + +func (c *FakeAccessLogContext) GetHeadersSize(headerType shared.HttpHeaderType) uint64 { + return uint64(len(c.Headers[headerType])) +} + +func (c *FakeAccessLogContext) GetHeaders(headerType shared.HttpHeaderType) [][2]shared.UnsafeEnvoyBuffer { + hs := c.Headers[headerType] + out := make([][2]shared.UnsafeEnvoyBuffer, len(hs)) + for i, kv := range hs { + out[i] = [2]shared.UnsafeEnvoyBuffer{stringBuf(kv[0]), stringBuf(kv[1])} + } + return out +} + +func (c *FakeAccessLogContext) GetHeaderValue( + headerType shared.HttpHeaderType, key string, index uint64, +) (shared.UnsafeEnvoyBuffer, uint64, bool) { + lower := strings.ToLower(key) + var matched []string + for _, kv := range c.Headers[headerType] { + if kv[0] == lower { + matched = append(matched, kv[1]) + } + } + total := uint64(len(matched)) + if total == 0 || index >= total { + return shared.UnsafeEnvoyBuffer{}, total, false + } + return stringBuf(matched[index]), total, true +} + +// ---- attribute accessors ---- + +func (c *FakeAccessLogContext) GetAttributeString(id shared.AttributeID) (shared.UnsafeEnvoyBuffer, bool) { + v, ok := c.StringAttributes[id] + if !ok { + return shared.UnsafeEnvoyBuffer{}, false + } + return stringBuf(v), true +} + +func (c *FakeAccessLogContext) GetAttributeNumber(id shared.AttributeID) (uint64, bool) { + v, ok := c.NumberAttributes[id] + return v, ok +} + +func (c *FakeAccessLogContext) GetAttributeBool(id shared.AttributeID) (bool, bool) { + v, ok := c.BoolAttributes[id] + return v, ok +} + +// ---- response flags / timing / bytes ---- + +func (c *FakeAccessLogContext) HasResponseFlag(flag shared.ResponseFlag) bool { + return c.ResponseFlags&(1<ca_cert_pem_.empty()) { + return absl::OkStatus(); + } + bssl::UniquePtr bio(BIO_new_mem_buf(const_cast(config_->ca_cert_pem_.data()), + config_->ca_cert_pem_.size())); + RELEASE_ASSERT(bio != nullptr, ""); + bssl::UniquePtr list( + sk_X509_NAME_new([](auto* a, auto* b) -> int { return X509_NAME_cmp(*a, *b); })); + RELEASE_ASSERT(list != nullptr, ""); + for (;;) { + bssl::UniquePtr cert(PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr)); + if (cert == nullptr) { + break; + } + const X509_NAME* name = X509_get_subject_name(cert.get()); + if (name == nullptr) { + return absl::InvalidArgumentError("Failed to extract subject from trusted CA certificate"); + } + if (sk_X509_NAME_find(list.get(), nullptr, name)) { + continue; + } + bssl::UniquePtr name_dup(X509_NAME_dup(name)); + if (name_dup == nullptr || !sk_X509_NAME_push(list.get(), name_dup.release())) { + return absl::InvalidArgumentError("Failed to copy subject from trusted CA certificate"); + } + } + SSL_CTX_set_client_CA_list(context, list.release()); return absl::OkStatus(); } @@ -224,8 +261,13 @@ ValidationResults DynamicModuleCertValidator::doVerifyCertChain( stats_.fail_verify_error_.inc(); } + // Read the field through its underlying integer to avoid UBSAN tripping on out-of-range + // enum values. A misbehaving module could return any int; we map unknown values to + // NotValidated to fail open. + int raw_detailed_status; + safeMemcpy(&raw_detailed_status, &result.detailed_status); Envoy::Ssl::ClientValidationStatus detailed_status; - switch (result.detailed_status) { + switch (raw_detailed_status) { case envoy_dynamic_module_type_cert_validator_client_validation_status_NotValidated: detailed_status = Envoy::Ssl::ClientValidationStatus::NotValidated; break; @@ -330,6 +372,12 @@ absl::StatusOr DynamicModuleCertValidatorFactory::createCertVa return factory_config_or_error.status(); } + // Capture any trusted_ca PEM bytes so addClientValidationContext can populate the client CA + // list. The PEM bytes are not used for verification — that is the dynamic module's job — but + // they are necessary for the server's TLS CertificateRequest to advertise a non-empty list, + // which BoringSSL clients require before they will send their certificate. + factory_config_or_error.value()->ca_cert_pem_ = config->caCert(); + return std::make_unique(std::move(factory_config_or_error.value()), stats); } diff --git a/source/extensions/transport_sockets/tls/cert_validator/dynamic_modules/config.h b/source/extensions/transport_sockets/tls/cert_validator/dynamic_modules/config.h index cc0ad63d07f14..70c4626791456 100644 --- a/source/extensions/transport_sockets/tls/cert_validator/dynamic_modules/config.h +++ b/source/extensions/transport_sockets/tls/cert_validator/dynamic_modules/config.h @@ -59,6 +59,13 @@ class DynamicModuleCertValidatorConfig { // filter state callbacks can access the connection's stream info. Reset after each call. Network::TransportSocketCallbacks* current_callbacks_ = nullptr; + // Optional PEM bytes of trusted CA certificates from the surrounding + // CertificateValidationContext. The dynamic module fully replaces the chain-verification logic, + // so the CAs are not used to validate peer certs. They are only used to populate the client CA + // list in the server's TLS CertificateRequest, so BoringSSL clients know which CAs the server + // expects to see in their chain. Empty if no trusted_ca was configured. + std::string ca_cert_pem_; + private: friend absl::StatusOr> newDynamicModuleCertValidatorConfig( diff --git a/test/extensions/access_loggers/dynamic_modules/BUILD b/test/extensions/access_loggers/dynamic_modules/BUILD index dec6098d0b1a7..87e8f3e410e40 100644 --- a/test/extensions/access_loggers/dynamic_modules/BUILD +++ b/test/extensions/access_loggers/dynamic_modules/BUILD @@ -69,10 +69,15 @@ envoy_cc_test( name = "integration_test", srcs = ["integration_test.cc"], data = [ + "//test/extensions/dynamic_modules/test_data/go:access_log_integration_test", "//test/extensions/dynamic_modules/test_data/rust:access_log_integration_test", ], + env = {"GODEBUG": "cgocheck=0"}, deps = [ "//source/extensions/access_loggers/dynamic_modules:config", + # See //test/extensions/dynamic_modules/http:filter_test for why every test + # binary that loads a Go test_data .so depends on all_abi_impls. + "//source/extensions/dynamic_modules:all_abi_impls", "//test/integration:http_integration_lib", "//test/test_common:utility_lib", "@envoy_api//envoy/extensions/access_loggers/dynamic_modules/v3:pkg_cc_proto", diff --git a/test/extensions/access_loggers/dynamic_modules/integration_test.cc b/test/extensions/access_loggers/dynamic_modules/integration_test.cc index 233b5b1aa4fe6..8be330b756623 100644 --- a/test/extensions/access_loggers/dynamic_modules/integration_test.cc +++ b/test/extensions/access_loggers/dynamic_modules/integration_test.cc @@ -4,20 +4,29 @@ namespace Envoy { -class DynamicModulesAccessLogIntegrationTest - : public testing::TestWithParam, - public HttpIntegrationTest { +// Parameterized over (language, IP version). language selects which test_data subdirectory +// (rust, go) the access logger module is loaded from. Both languages ship a module named +// "access_log_integration_test" exposing a "test_logger" access logger that exercises the +// full AccessLogContext getter surface and records select getter results into per-config +// counters/gauges so this driver can verify correctness via /stats. +struct AccessLogParam { + std::string language; + Network::Address::IpVersion ip_version; +}; + +class DynamicModulesAccessLogIntegrationTest : public testing::TestWithParam, + public HttpIntegrationTest { public: DynamicModulesAccessLogIntegrationTest() - : HttpIntegrationTest(Http::CodecType::HTTP2, GetParam()) { + : HttpIntegrationTest(Http::CodecType::HTTP2, GetParam().ip_version) { setUpstreamProtocol(Http::CodecType::HTTP2); }; void initializeWithAccessLogger() { TestEnvironment::setEnvVar( "ENVOY_DYNAMIC_MODULES_SEARCH_PATH", - TestEnvironment::substitute( - "{{ test_rundir }}/test/extensions/dynamic_modules/test_data/rust"), + TestEnvironment::substitute("{{ test_rundir }}/test/extensions/dynamic_modules/test_data/" + + GetParam().language), 1); config_helper_.addConfigModifier( @@ -41,11 +50,37 @@ name: envoy.access_loggers.dynamic_modules initialize(); } + + // Stats from the access logger are scoped under the default metrics namespace + // "dynamicmodulescustom." — see access_loggers/dynamic_modules/config.cc. + uint64_t counter(absl::string_view name) { + return test_server_->counter(absl::StrCat("dynamicmodulescustom.", name))->value(); + } + + uint64_t gauge(absl::string_view name) { + return test_server_->gauge(absl::StrCat("dynamicmodulescustom.", name))->value(); + } }; -INSTANTIATE_TEST_SUITE_P(IpVersions, DynamicModulesAccessLogIntegrationTest, - testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), - TestUtility::ipTestParamsToString); +namespace { +std::vector getAccessLogTestParams() { + std::vector params; + for (const auto& language : {"rust", "go"}) { + for (const auto ip : TestEnvironment::getIpVersionsForTest()) { + params.push_back({language, ip}); + } + } + return params; +} + +std::string accessLogParamName(const testing::TestParamInfo& info) { + return info.param.language + "_" + + (info.param.ip_version == Network::Address::IpVersion::v4 ? "IPv4" : "IPv6"); +} +} // namespace + +INSTANTIATE_TEST_SUITE_P(SdkLanguagesAndIpVersions, DynamicModulesAccessLogIntegrationTest, + testing::ValuesIn(getAccessLogTestParams()), accessLogParamName); TEST_P(DynamicModulesAccessLogIntegrationTest, BasicLogging) { initializeWithAccessLogger(); @@ -57,13 +92,26 @@ TEST_P(DynamicModulesAccessLogIntegrationTest, BasicLogging) { auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); - // Verify the response was received. EXPECT_TRUE(upstream_request_->complete()); EXPECT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().Status()->value().getStringView()); - // The access logger was called. We can't easily verify this from the test since the logger - // doesn't modify headers, but the test passing means the logger loaded and ran without crashing. + // The Go and Rust modules increment counters and set gauges from data they read off + // the AccessLogContext — assert those values match the wire data we sent above. + test_server_->waitForCounterEq("dynamicmodulescustom.test_log_count", 1); + + // Specific getters: response code, method, path, request protocol. + EXPECT_EQ(1, counter("test_response_code_200")); + EXPECT_EQ(1, counter("test_method_get")); + EXPECT_EQ(1, counter("test_path_test")); + EXPECT_EQ(1, counter("test_request_protocol_http2")); + + // Gauges hold the last observed values. + EXPECT_EQ(200, gauge("test_response_code_last")); + // The 4-header request (method, path, scheme, authority) plus internal envoy headers + // means request_headers_count must be at least 4. The exact value depends on Envoy + // header insertions so we use a lower bound. + EXPECT_GE(gauge("test_request_headers_count"), 4u); } TEST_P(DynamicModulesAccessLogIntegrationTest, MultipleRequests) { @@ -71,7 +119,6 @@ TEST_P(DynamicModulesAccessLogIntegrationTest, MultipleRequests) { codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); - // Send multiple requests to verify logging works across requests. for (int i = 0; i < 3; i++) { Http::TestRequestHeaderMapImpl request_headers{ {":method", "GET"}, {":path", "/test"}, {":scheme", "http"}, {":authority", "host"}}; @@ -80,6 +127,37 @@ TEST_P(DynamicModulesAccessLogIntegrationTest, MultipleRequests) { EXPECT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().Status()->value().getStringView()); } + + // Counters accumulate across requests — assert all three were observed. + test_server_->waitForCounterEq("dynamicmodulescustom.test_log_count", 3); + EXPECT_EQ(3, counter("test_response_code_200")); + EXPECT_EQ(3, counter("test_method_get")); + EXPECT_EQ(3, counter("test_path_test")); +} + +// Verify a non-matching request increments the log_count counter but NOT the +// header-specific counters (i.e., the SDK getters didn't fabricate matches that aren't +// there). +TEST_P(DynamicModulesAccessLogIntegrationTest, GetterValuesMatchActualRequest) { + initializeWithAccessLogger(); + + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + + // Send a POST to a different path so :method != GET and :path != /test. + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "POST"}, {":path", "/other"}, {":scheme", "http"}, {":authority", "host"}}; + + auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); + + test_server_->waitForCounterEq("dynamicmodulescustom.test_log_count", 1); + // :method was POST, not GET — counter should remain 0. + EXPECT_EQ(0, counter("test_method_get")); + // :path was /other, not /test — counter should remain 0. + EXPECT_EQ(0, counter("test_path_test")); + // Response was still 200, so this counter does increment. + EXPECT_EQ(1, counter("test_response_code_200")); } } // namespace Envoy diff --git a/test/extensions/clusters/dynamic_modules/BUILD b/test/extensions/clusters/dynamic_modules/BUILD index 34ecf00e5d820..3b6055a306bbb 100644 --- a/test/extensions/clusters/dynamic_modules/BUILD +++ b/test/extensions/clusters/dynamic_modules/BUILD @@ -46,12 +46,18 @@ envoy_cc_test( name = "integration_test", srcs = ["integration_test.cc"], data = [ + "//test/extensions/dynamic_modules/test_data/go:cluster_integration_test", "//test/extensions/dynamic_modules/test_data/rust:cluster_integration_test", ], + # cgo modules trigger the "unaligned 64-bit atomic operation" check on some platforms; + # disabling cgocheck matches the Go integration tests for HTTP and network filters. + env = {"GODEBUG": "cgocheck=0"}, rbe_pool = "6gig", deps = [ "//source/extensions/clusters/dynamic_modules:cluster", - "//source/extensions/dynamic_modules:abi_impl", + # See //test/extensions/dynamic_modules/http:filter_test for why every test + # binary that loads a Go test_data .so depends on all_abi_impls. + "//source/extensions/dynamic_modules:all_abi_impls", "//source/extensions/load_balancing_policies/cluster_provided:config", "//test/extensions/dynamic_modules:util", "//test/integration:http_integration_lib", diff --git a/test/extensions/clusters/dynamic_modules/integration_test.cc b/test/extensions/clusters/dynamic_modules/integration_test.cc index 97388f3842263..0c3b98db7a756 100644 --- a/test/extensions/clusters/dynamic_modules/integration_test.cc +++ b/test/extensions/clusters/dynamic_modules/integration_test.cc @@ -12,19 +12,32 @@ namespace Extensions { namespace Clusters { namespace DynamicModules { -class DynamicModuleClusterIntegrationTest - : public testing::TestWithParam, - public HttpIntegrationTest { +// Parameterized over (language, IP version). language selects which test_data subdirectory +// is loaded as the dynamic module — currently rust or go. Each language ships a module named +// "cluster_integration_test" that exposes the same set of named cluster types +// (sync_host_selection, async_host_selection, scheduler_host_update, lifecycle_callbacks), +// so the same test bodies exercise both SDKs. +struct ClusterIntegrationParam { + std::string language; + Network::Address::IpVersion ip_version; +}; + +class DynamicModuleClusterIntegrationTest : public testing::TestWithParam, + public HttpIntegrationTest { public: - DynamicModuleClusterIntegrationTest() : HttpIntegrationTest(Http::CodecType::HTTP1, GetParam()) {} + DynamicModuleClusterIntegrationTest() + : HttpIntegrationTest(Http::CodecType::HTTP1, GetParam().ip_version) {} void initializeWithDecCluster(const std::string& cluster_name, const std::string& cluster_config = "") { TestEnvironment::setEnvVar( "ENVOY_DYNAMIC_MODULES_SEARCH_PATH", - TestEnvironment::runfilesPath("test/extensions/dynamic_modules/test_data/rust"), 1); + TestEnvironment::runfilesPath("test/extensions/dynamic_modules/test_data/" + + GetParam().language), + 1); - // Replace the default cluster_0 with a DEC cluster that uses the Rust module. + // Replace the default cluster_0 with a DEC cluster that uses the language module + // selected by the test parameter. config_helper_.addConfigModifier([this, cluster_name, cluster_config]( envoy::config::bootstrap::v3::Bootstrap& bootstrap) { auto* cluster = bootstrap.mutable_static_resources()->mutable_clusters(0); @@ -42,8 +55,8 @@ class DynamicModuleClusterIntegrationTest dec_config.mutable_dynamic_module_config()->set_name("cluster_integration_test"); dec_config.set_cluster_name(cluster_name); - // Pass the upstream address via the cluster config so the Rust module knows - // where to add hosts. + // Pass the upstream address via the cluster config so the module knows where to add + // hosts. const std::string config_value = cluster_config.empty() ? upstream_address : cluster_config; Protobuf::StringValue config_proto; config_proto.set_value(config_value); @@ -57,9 +70,27 @@ class DynamicModuleClusterIntegrationTest } }; -INSTANTIATE_TEST_SUITE_P(IpVersions, DynamicModuleClusterIntegrationTest, - testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), - TestUtility::ipTestParamsToString); +namespace { +std::vector getClusterIntegrationTestParams() { + std::vector params; + for (const auto& language : {"rust", "go"}) { + for (const auto ip : TestEnvironment::getIpVersionsForTest()) { + params.push_back({language, ip}); + } + } + return params; +} + +std::string +clusterIntegrationParamName(const testing::TestParamInfo& info) { + return info.param.language + "_" + + (info.param.ip_version == Network::Address::IpVersion::v4 ? "IPv4" : "IPv6"); +} +} // namespace + +INSTANTIATE_TEST_SUITE_P(SdkLanguagesAndIpVersions, DynamicModuleClusterIntegrationTest, + testing::ValuesIn(getClusterIntegrationTestParams()), + clusterIntegrationParamName); // Verifies that a cluster with synchronous host selection correctly routes requests // to the upstream added during on_init. @@ -90,7 +121,32 @@ TEST_P(DynamicModuleClusterIntegrationTest, SyncHostSelectionMultipleRequests) { } } +// Verifies that the cluster-level metric defined via DefineCounter (and incremented per +// host selection in ChooseHost) ends up in the /stats output. The Go module defines +// "requests_routed" at config-time and increments it for each ChooseHost call. The +// stat is scoped under "dynamicmodulescustom." (see cluster.cc). +TEST_P(DynamicModuleClusterIntegrationTest, ClusterCounterMetric) { + initializeWithDecCluster("sync_host_selection"); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + + for (int i = 0; i < 3; ++i) { + auto response = + sendRequestAndWaitForResponse(default_request_headers_, 0, default_response_headers_, 0); + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + } + + // The counter increments once per ChooseHost call. We sent 3 requests, so we expect + // at least 3 increments — there may be additional internal selections on retry/probe + // paths, so use a >= bound. + test_server_->waitForCounterGe("dynamicmodulescustom.requests_routed", 3); +} + // Verifies that a cluster with asynchronous host selection correctly routes requests. +// +// For Go specifically, this exercises the bug fix where ChooseHost was unable to honor a +// user-supplied ClusterAsyncHostSelection — the SDK previously discarded the user's +// returned handle and registered a fresh one, making Cancel dispatch unreachable. TEST_P(DynamicModuleClusterIntegrationTest, AsyncHostSelection) { initializeWithDecCluster("async_host_selection"); codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); @@ -117,7 +173,10 @@ TEST_P(DynamicModuleClusterIntegrationTest, SchedulerHostUpdate) { } // Verifies that the cluster lifecycle callbacks fire correctly during cluster -// initialization. +// initialization, and — critically for Go — that the shutdown completion callback +// reaches Envoy. A previous bug at the trampoline layer silently dropped the completion, +// causing test_server_.reset() at the end of the test to hang waiting for an event_cb +// that never arrived. TEST_P(DynamicModuleClusterIntegrationTest, LifecycleCallbacks) { EXPECT_LOG_CONTAINS_ALL_OF( Envoy::ExpectedLogMessages({{"info", "cluster lifecycle: on_init called"}, @@ -131,6 +190,11 @@ TEST_P(DynamicModuleClusterIntegrationTest, LifecycleCallbacks) { EXPECT_TRUE(upstream_request_->complete()); EXPECT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().getStatusValue()); + + // Tear the server down explicitly so the on_shutdown hook fires inside this test scope + // — that lets us assert the completion callback reaches Envoy. If the bug regresses on + // Go, the reset hangs and the test times out. + EXPECT_LOG_CONTAINS("info", "cluster lifecycle: on_shutdown called", { test_server_.reset(); }); } } // namespace DynamicModules diff --git a/test/extensions/dynamic_modules/bootstrap/BUILD b/test/extensions/dynamic_modules/bootstrap/BUILD index abfef68d91226..f482699b2e233 100644 --- a/test/extensions/dynamic_modules/bootstrap/BUILD +++ b/test/extensions/dynamic_modules/bootstrap/BUILD @@ -115,6 +115,17 @@ envoy_cc_test( data = [ "//test/config/integration/certs", "//test/extensions/dynamic_modules/test_data/c:bootstrap_no_op", + "//test/extensions/dynamic_modules/test_data/go:bootstrap_admin_handler_test", + "//test/extensions/dynamic_modules/test_data/go:bootstrap_cluster_lifecycle_test", + "//test/extensions/dynamic_modules/test_data/go:bootstrap_file_watcher_test", + "//test/extensions/dynamic_modules/test_data/go:bootstrap_function_registry_test", + "//test/extensions/dynamic_modules/test_data/go:bootstrap_http_combined_test", + "//test/extensions/dynamic_modules/test_data/go:bootstrap_init_target_test", + "//test/extensions/dynamic_modules/test_data/go:bootstrap_integration_test", + "//test/extensions/dynamic_modules/test_data/go:bootstrap_listener_lifecycle_test", + "//test/extensions/dynamic_modules/test_data/go:bootstrap_shared_data_test", + "//test/extensions/dynamic_modules/test_data/go:bootstrap_stats_test", + "//test/extensions/dynamic_modules/test_data/go:bootstrap_timer_test", "//test/extensions/dynamic_modules/test_data/rust:bootstrap_admin_handler_test", "//test/extensions/dynamic_modules/test_data/rust:bootstrap_cluster_lifecycle_test", "//test/extensions/dynamic_modules/test_data/rust:bootstrap_file_watcher_test", @@ -127,8 +138,12 @@ envoy_cc_test( "//test/extensions/dynamic_modules/test_data/rust:bootstrap_stats_test", "//test/extensions/dynamic_modules/test_data/rust:bootstrap_timer_test", ], + env = {"GODEBUG": "cgocheck=0"}, deps = [ "//source/extensions/bootstrap/dynamic_modules:config", + # See //test/extensions/dynamic_modules/http:filter_test for why every test + # binary that loads a Go test_data .so depends on all_abi_impls. + "//source/extensions/dynamic_modules:all_abi_impls", "//source/extensions/filters/http/dynamic_modules:factory_registration", "//test/integration:http_integration_lib", "//test/test_common:environment_lib", diff --git a/test/extensions/dynamic_modules/bootstrap/integration_test.cc b/test/extensions/dynamic_modules/bootstrap/integration_test.cc index dbb5358513f1a..85f6d1d0a95e0 100644 --- a/test/extensions/dynamic_modules/bootstrap/integration_test.cc +++ b/test/extensions/dynamic_modules/bootstrap/integration_test.cc @@ -19,20 +19,23 @@ class DynamicModulesBootstrapIntegrationTest void initializeWithBootstrapExtension(const std::string& module_dir, const std::string& module_name = "test", const std::string& extension_name = "test", - const std::string& extension_config = "test_config") { + const std::string& extension_config = "test_config", + bool do_not_close = false) { TestEnvironment::setEnvVar("ENVOY_DYNAMIC_MODULES_SEARCH_PATH", module_dir, 1); - const std::string yaml = fmt::format(R"EOF( + const std::string yaml = + fmt::format(R"EOF( name: envoy.bootstrap.dynamic_modules typed_config: "@type": type.googleapis.com/envoy.extensions.bootstrap.dynamic_modules.v3.DynamicModuleBootstrapExtension dynamic_module_config: name: {} + do_not_close: {} extension_name: {} extension_config: "@type": type.googleapis.com/google.protobuf.StringValue value: {} )EOF", - module_name, extension_name, extension_config); + module_name, do_not_close ? "true" : "false", extension_name, extension_config); config_helper_.addBootstrapExtension(yaml); HttpIntegrationTest::initialize(); @@ -65,6 +68,22 @@ TEST_P(DynamicModulesBootstrapIntegrationTest, BasicRust) { EXPECT_LOG_CONTAINS("info", "Bootstrap extension shutdown from Rust!", { test_server_.reset(); }); } +// Mirror of BasicRust against the Go SDK. Exercises the same lifecycle hooks +// (server_initialized / worker_thread_initialized / shutdown) and specifically validates +// that the shutdown completion callback actually reaches Envoy from Go — a bug previously +// existed where the trampoline discarded the callback, hanging Envoy teardown. +TEST_P(DynamicModulesBootstrapIntegrationTest, BasicGo) { + EXPECT_LOG_CONTAINS_ALL_OF( + Envoy::ExpectedLogMessages( + {{"info", "Bootstrap extension server initialized from Go!"}, + {"info", "Bootstrap extension worker thread initialized from Go!"}}), + initializeWithBootstrapExtension(testDataDir("go"), "bootstrap_integration_test")); + + // Verify the shutdown hook is called during server teardown. If the Go shutdown + // completion fix regresses, the test_server_.reset() call will hang and time out. + EXPECT_LOG_CONTAINS("info", "Bootstrap extension shutdown from Go!", { test_server_.reset(); }); +} + // This test verifies that the Rust bootstrap extension can access stats from the stats store // and define/update its own metrics (counters, gauges, histograms). TEST_P(DynamicModulesBootstrapIntegrationTest, StatsAccessRust) { @@ -84,12 +103,50 @@ TEST_P(DynamicModulesBootstrapIntegrationTest, StatsAccessRust) { initializeWithBootstrapExtension(testDataDir("rust"), "bootstrap_stats_test")); } +// Mirror of StatsAccessRust against the Go SDK. The Go module emits the same set of log +// lines so the same expectations apply. +TEST_P(DynamicModulesBootstrapIntegrationTest, StatsAccessGo) { + EXPECT_LOG_CONTAINS_ALL_OF( + Envoy::ExpectedLogMessages( + {{"info", "Counter incremented to expected value of 5"}, + {"info", "Gauge set to expected value of 80"}, + {"info", "Histogram values recorded successfully"}, + {"info", "Counter vec incremented successfully"}, + {"info", "Gauge vec manipulated successfully"}, + {"info", "Histogram vec recorded successfully"}, + {"info", "Bootstrap metrics definition and update test completed successfully!"}, + {"info", "Correctly returned None for non-existent counter"}, + {"info", "Correctly returned None for non-existent gauge"}, + {"info", "Correctly returned None for non-existent histogram"}, + {"info", "Bootstrap stats access test completed successfully!"}}), + initializeWithBootstrapExtension(testDataDir("go"), "bootstrap_stats_test")); +} + // This test verifies that the Rust bootstrap extension can register and resolve functions // via the process-wide function registry. +// +// do_not_close=true: the function registry is process-wide, but registered values are raw +// pointers into the loaded .so. The gtest parameterization runs IPv4 then IPv6 in the same +// process, which would dlclose+dlopen the module between iterations; on aarch64 the second +// dlopen tends to map the .so at a different address, leaving the cached pointers dangling +// and causing a segfault when the test calls them. RTLD_NODELETE keeps the .so resident so +// the cached pointers stay valid across iterations. TEST_P(DynamicModulesBootstrapIntegrationTest, FunctionRegistryRust) { - EXPECT_LOG_CONTAINS( - "info", "Bootstrap function registry test completed successfully!", - initializeWithBootstrapExtension(testDataDir("rust"), "bootstrap_function_registry_test")); + EXPECT_LOG_CONTAINS("info", "Bootstrap function registry test completed successfully!", + initializeWithBootstrapExtension(testDataDir("rust"), + "bootstrap_function_registry_test", "test", + "test_config", /*do_not_close=*/true)); +} + +// Mirror of FunctionRegistryRust against the Go SDK. The Go module exercises the +// register/get round-trip with sentinel pointers (Go can't directly produce C function +// pointers from Go functions). do_not_close is set to true for the same reason as the +// Rust variant above. +TEST_P(DynamicModulesBootstrapIntegrationTest, FunctionRegistryGo) { + EXPECT_LOG_CONTAINS("info", "Bootstrap function registry test completed successfully!", + initializeWithBootstrapExtension(testDataDir("go"), + "bootstrap_function_registry_test", "test", + "test_config", /*do_not_close=*/true)); } // This test verifies that the Rust bootstrap extension can register, retrieve, and overwrite @@ -100,6 +157,13 @@ TEST_P(DynamicModulesBootstrapIntegrationTest, SharedDataRegistryRust) { initializeWithBootstrapExtension(testDataDir("rust"), "bootstrap_shared_data_test")); } +// Mirror of SharedDataRegistryRust against the Go SDK. +TEST_P(DynamicModulesBootstrapIntegrationTest, SharedDataRegistryGo) { + EXPECT_LOG_CONTAINS( + "info", "Bootstrap shared data registry test completed successfully!", + initializeWithBootstrapExtension(testDataDir("go"), "bootstrap_shared_data_test")); +} + // This test verifies that Envoy automatically registers an init target for every bootstrap // extension and that the module can signal readiness to unblock startup. TEST_P(DynamicModulesBootstrapIntegrationTest, InitTargetRust) { @@ -109,6 +173,14 @@ TEST_P(DynamicModulesBootstrapIntegrationTest, InitTargetRust) { initializeWithBootstrapExtension(testDataDir("rust"), "bootstrap_init_target_test")); } +// Mirror of InitTargetRust against the Go SDK. +TEST_P(DynamicModulesBootstrapIntegrationTest, InitTargetGo) { + EXPECT_LOG_CONTAINS_ALL_OF( + Envoy::ExpectedLogMessages({{"info", "Init target signaled complete during config creation"}, + {"info", "Bootstrap init target test completed successfully!"}}), + initializeWithBootstrapExtension(testDataDir("go"), "bootstrap_init_target_test")); +} + // This test verifies that the Rust bootstrap extension timer API works correctly. // Two timers are created during config_new, armed with short delays, and on_timer_fired uses the // timer identity API to distinguish which timer fired. Init completes after both timers fire. @@ -118,6 +190,13 @@ TEST_P(DynamicModulesBootstrapIntegrationTest, TimerRust) { initializeWithBootstrapExtension(testDataDir("rust"), "bootstrap_timer_test")); } +// Mirror of TimerRust against the Go SDK. Go uses per-timer onFire closures rather than +// an on_timer_fired hook, but the externally observable behavior is the same. +TEST_P(DynamicModulesBootstrapIntegrationTest, TimerGo) { + EXPECT_LOG_CONTAINS("info", "Bootstrap timer test completed successfully!", + initializeWithBootstrapExtension(testDataDir("go"), "bootstrap_timer_test")); +} + // This test verifies that the Rust bootstrap extension file watcher API works correctly. // Two files are watched via separate add_file_watch calls. Three timed writes occur: file_a twice // and file_b once. on_file_changed tracks per-path counts, and signals init complete only after @@ -136,6 +215,19 @@ TEST_P(DynamicModulesBootstrapIntegrationTest, FileWatcherRust) { testDataDir("rust"), "bootstrap_file_watcher_test", "test", config)); } +// Mirror of FileWatcherRust against the Go SDK. +TEST_P(DynamicModulesBootstrapIntegrationTest, FileWatcherGo) { + const std::string path_a = + TestEnvironment::writeStringToFileForTest("file_watcher_test_go_a", "initial a"); + const std::string path_b = + TestEnvironment::writeStringToFileForTest("file_watcher_test_go_b", "initial b"); + const std::string config = path_a + "|" + path_b; + + EXPECT_LOG_CONTAINS("info", "Bootstrap file watcher test completed successfully!", + initializeWithBootstrapExtension( + testDataDir("go"), "bootstrap_file_watcher_test", "test", config)); +} + // This test verifies that the Rust bootstrap extension can register a custom admin HTTP endpoint // and respond to admin requests. TEST_P(DynamicModulesBootstrapIntegrationTest, AdminHandlerRust) { @@ -161,6 +253,28 @@ TEST_P(DynamicModulesBootstrapIntegrationTest, AdminHandlerRust) { }); } +// Mirror of AdminHandlerRust against the Go SDK. +TEST_P(DynamicModulesBootstrapIntegrationTest, AdminHandlerGo) { + EXPECT_LOG_CONTAINS( + "info", "Admin handler registered: true", + initializeWithBootstrapExtension(testDataDir("go"), "bootstrap_admin_handler_test")); + + BufferingStreamDecoderPtr response = + IntegrationUtil::makeSingleRequest(lookupPort("admin"), "GET", "/dynamic_module_admin_test", + "", Http::CodecType::HTTP1, version_); + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + EXPECT_THAT(response->body(), testing::HasSubstr("Hello from dynamic module admin handler!")); + + EXPECT_LOG_CONTAINS("info", "Admin request received: GET", { + response = IntegrationUtil::makeSingleRequest(lookupPort("admin"), "GET", + "/dynamic_module_admin_test?foo=bar", "", + Http::CodecType::HTTP1, version_); + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + }); +} + // This test verifies that the Rust bootstrap extension can receive cluster lifecycle events // (add/update and removal) via the ClusterUpdateCallbacks mechanism. TEST_P(DynamicModulesBootstrapIntegrationTest, ClusterLifecycleRust) { @@ -170,6 +284,14 @@ TEST_P(DynamicModulesBootstrapIntegrationTest, ClusterLifecycleRust) { initializeWithBootstrapExtension(testDataDir("rust"), "bootstrap_cluster_lifecycle_test")); } +// Mirror of ClusterLifecycleRust against the Go SDK. +TEST_P(DynamicModulesBootstrapIntegrationTest, ClusterLifecycleGo) { + EXPECT_LOG_CONTAINS_ALL_OF( + Envoy::ExpectedLogMessages({{"info", "Bootstrap cluster lifecycle test: server initialized"}, + {"info", "Cluster lifecycle enabled: true"}}), + initializeWithBootstrapExtension(testDataDir("go"), "bootstrap_cluster_lifecycle_test")); +} + // This test verifies that the Rust bootstrap extension can receive listener lifecycle events // (add/update and removal) via the ListenerUpdateCallbacks mechanism. TEST_P(DynamicModulesBootstrapIntegrationTest, ListenerLifecycleRust) { @@ -179,6 +301,14 @@ TEST_P(DynamicModulesBootstrapIntegrationTest, ListenerLifecycleRust) { initializeWithBootstrapExtension(testDataDir("rust"), "bootstrap_listener_lifecycle_test")); } +// Mirror of ListenerLifecycleRust against the Go SDK. +TEST_P(DynamicModulesBootstrapIntegrationTest, ListenerLifecycleGo) { + EXPECT_LOG_CONTAINS_ALL_OF( + Envoy::ExpectedLogMessages({{"info", "Bootstrap listener lifecycle test: server initialized"}, + {"info", "Listener lifecycle enabled: true"}}), + initializeWithBootstrapExtension(testDataDir("go"), "bootstrap_listener_lifecycle_test")); +} + // This test verifies that a bootstrap extension can register a function in the process-wide // function registry and an HTTP filter in the same module can resolve and call it during request // processing. The bootstrap extension asynchronously initializes a routing table and registers a @@ -189,13 +319,16 @@ TEST_P(DynamicModulesBootstrapIntegrationTest, FunctionRegistryCrossFilterRust) TestEnvironment::setEnvVar("ENVOY_DYNAMIC_MODULES_SEARCH_PATH", module_dir, 1); // Add the bootstrap extension that initializes the routing table and registers the lookup - // function. + // function. do_not_close=true because IPv4 and IPv6 share the same process; without + // RTLD_NODELETE the registered function pointer would be invalidated by the dlclose between + // iterations (especially on aarch64 where the .so reloads at a different address). const std::string bootstrap_yaml = R"EOF( name: envoy.bootstrap.dynamic_modules typed_config: "@type": type.googleapis.com/envoy.extensions.bootstrap.dynamic_modules.v3.DynamicModuleBootstrapExtension dynamic_module_config: name: bootstrap_http_combined_test + do_not_close: true extension_name: combined_test extension_config: "@type": type.googleapis.com/google.protobuf.StringValue @@ -299,6 +432,117 @@ name: envoy.extensions.filters.http.dynamic_modules } } +// Mirror of FunctionRegistryCrossFilterRust against the Go SDK. The Go module registers +// the address of a package-level Go func var via sdk.RegisterFunction, and the HTTP +// filter resolves it via sdk.GetFunction and dereferences it back to a callable. Both +// extensions live in the same .so so the in-process function can be safely re-cast +// from unsafe.Pointer; the cross-language goal is to prove the Envoy registry round-trip +// preserves the pointer. +TEST_P(DynamicModulesBootstrapIntegrationTest, FunctionRegistryCrossFilterGo) { + const std::string module_dir = testDataDir("go"); + TestEnvironment::setEnvVar("ENVOY_DYNAMIC_MODULES_SEARCH_PATH", module_dir, 1); + TestEnvironment::setEnvVar("GODEBUG", "cgocheck=0", 1); + + // do_not_close=true: see FunctionRegistryCrossFilterRust for the rationale. + const std::string bootstrap_yaml = R"EOF( + name: envoy.bootstrap.dynamic_modules + typed_config: + "@type": type.googleapis.com/envoy.extensions.bootstrap.dynamic_modules.v3.DynamicModuleBootstrapExtension + dynamic_module_config: + name: bootstrap_http_combined_test + do_not_close: true + extension_name: combined_test + extension_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: test + )EOF"; + config_helper_.addBootstrapExtension(bootstrap_yaml); + + const std::string http_filter_yaml = R"EOF( +name: envoy.extensions.filters.http.dynamic_modules +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_modules.v3.DynamicModuleFilter + dynamic_module_config: + name: bootstrap_http_combined_test + filter_name: combined_filter + filter_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: "" +)EOF"; + config_helper_.prependFilter(http_filter_yaml); + + EXPECT_LOG_CONTAINS_ALL_OF( + Envoy::ExpectedLogMessages( + {{"info", "bootstrap init signaled complete after async initialization"}, + {"info", "http filter config created (function resolution deferred to request time)"}}), + HttpIntegrationTest::initialize()); + + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + + // Case 1: Known service routes successfully. + { + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/test"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-target-service", "service-a"}}; + auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); + EXPECT_EQ("10.0.0.1:8080", upstream_request_->headers() + .get(Http::LowerCaseString("x-routed-to"))[0] + ->value() + .getStringView()); + } + + // Case 2: Different known service. + { + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/test"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-target-service", "service-b"}}; + auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); + EXPECT_EQ("10.0.0.2:9090", upstream_request_->headers() + .get(Http::LowerCaseString("x-routed-to"))[0] + ->value() + .getStringView()); + } + + // Case 3: Unknown service gets a 503 local reply. + { + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/test"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-target-service", "unknown-service"}}; + auto encoder_decoder = codec_client_->startRequest(request_headers, true); + auto response = std::move(encoder_decoder.second); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(response->complete()); + EXPECT_EQ("503", response->headers().Status()->value().getStringView()); + EXPECT_EQ("service_not_onboarded", response->headers() + .get(Http::LowerCaseString("x-error-reason"))[0] + ->value() + .getStringView()); + } + + // Case 4: No x-target-service header — pass through, no routing header set. + { + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/test"}, {":scheme", "http"}, {":authority", "host"}}; + auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); + EXPECT_TRUE(upstream_request_->headers().get(Http::LowerCaseString("x-routed-to")).empty()); + } +} + } // namespace DynamicModules } // namespace Bootstrap } // namespace Extensions diff --git a/test/extensions/dynamic_modules/http/BUILD b/test/extensions/dynamic_modules/http/BUILD index 10a3a7812329a..ee843fdf6a6e2 100644 --- a/test/extensions/dynamic_modules/http/BUILD +++ b/test/extensions/dynamic_modules/http/BUILD @@ -90,8 +90,13 @@ envoy_cc_test( deps = [ "//envoy/registry", "//source/common/router:string_accessor_lib", - "//source/extensions/dynamic_modules:abi_impl", - "//source/extensions/filters/http/dynamic_modules:abi_impl", + # The Go SDK's `abi` Go package references every surface's + # `envoy_dynamic_module_callback_*` symbols, so any Go test_data .so has + # unresolved cross-surface callback references. With RTLD_LAZY the loader + # accepts these as long as the test binary provides them. all_abi_impls + # aggregates every per-surface `abi_impl`/`filter_lib`/`*_lib` library so a + # single dep gives the test binary the complete callback symbol surface. + "//source/extensions/dynamic_modules:all_abi_impls", "//source/extensions/filters/http/dynamic_modules:filter_lib", "//test/extensions/dynamic_modules:util", "//test/mocks/http:http_mocks", @@ -150,8 +155,9 @@ envoy_cc_test( }, rbe_pool = "6gig", deps = [ - "//source/extensions/dynamic_modules:abi_impl", - "//source/extensions/filters/http/dynamic_modules:abi_impl", + "//source/common/config:metadata_lib", + # See comment on filter_test above for why we link every per-surface abi_impl. + "//source/extensions/dynamic_modules:all_abi_impls", "//source/extensions/filters/http/dynamic_modules:factory_registration", "//test/extensions/dynamic_modules:util", "//test/extensions/dynamic_modules/test_data/rust:http_integration_test_static", diff --git a/test/extensions/dynamic_modules/http/integration_test.cc b/test/extensions/dynamic_modules/http/integration_test.cc index f800a6aa29ebd..bd2ccff9ee36b 100644 --- a/test/extensions/dynamic_modules/http/integration_test.cc +++ b/test/extensions/dynamic_modules/http/integration_test.cc @@ -1,6 +1,7 @@ #include "envoy/extensions/filters/http/dynamic_modules/v3/dynamic_modules.pb.h" #include "source/common/common/base64.h" +#include "source/common/config/metadata.h" #include "test/extensions/dynamic_modules/util.h" #include "test/integration/http_integration.h" @@ -1024,6 +1025,85 @@ TEST_P(DynamicModulesIntegrationTest, ConfigScheduler) { FAIL() << "Config was not updated in time"; } +// Verifies the config-time HttpCallout API: the filter config Create() initiates a +// callout via HttpFilterConfigHandle.HttpCallout against cluster_0. Per-request filters +// short-circuit with x-config-callout: success once the callout completes +// (503 + "pending" until then). +TEST_P(DynamicModulesIntegrationTest, ConfigCallout) { + // C++ SDK does not expose the config-time callout API; skip. + if (GetParam() == "cpp") { + return; + } + initializeFilter("http_config_callout", "cluster_0"); + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + + // First downstream request: the dispatcher run-loop drives the in-flight config-time + // callout out to cluster_0. Serve it so the callout completes; the factory's atomic + // flag flips on completion. The downstream request itself short-circuits with 503. + { + auto encoder_decoder = codec_client_->startRequest(default_request_headers_); + auto response = std::move(encoder_decoder.second); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(response->complete()); + } + + // Subsequent requests should see "success" once the atomic flag has been observed. + for (int i = 0; i < 20; ++i) { + auto encoder_decoder = codec_client_->startRequest(default_request_headers_); + auto response = std::move(encoder_decoder.second); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(response->complete()); + + auto hdr = response->headers().get(Http::LowerCaseString("x-config-callout")); + ASSERT_FALSE(hdr.empty()); + if (hdr[0]->value().getStringView() == "success") { + return; + } + absl::SleepFor(absl::Milliseconds(50)); + } + FAIL() << "Config callout did not complete in time"; +} + +// Verifies the config-time StartHttpStream API: the filter config Create() opens an HTTP +// stream via HttpFilterConfigHandle.StartHttpStream against cluster_0. Per-request filters +// short-circuit with x-config-stream: success once the stream completes +// (503 + "pending" until then). +TEST_P(DynamicModulesIntegrationTest, ConfigStream) { + // C++ SDK does not expose the config-time stream API; skip. + if (GetParam() == "cpp") { + return; + } + initializeFilter("http_config_stream", "cluster_0"); + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + + // Serve the in-flight config-time stream against cluster_0 on the first request. + { + auto encoder_decoder = codec_client_->startRequest(default_request_headers_); + auto response = std::move(encoder_decoder.second); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(response->complete()); + } + + for (int i = 0; i < 20; ++i) { + auto encoder_decoder = codec_client_->startRequest(default_request_headers_); + auto response = std::move(encoder_decoder.second); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(response->complete()); + + auto hdr = response->headers().get(Http::LowerCaseString("x-config-stream")); + ASSERT_FALSE(hdr.empty()); + if (hdr[0]->value().getStringView() == "success") { + return; + } + absl::SleepFor(absl::Milliseconds(50)); + } + FAIL() << "Config stream did not complete in time"; +} + // Test buffer limit callbacks for non-terminal filters. TEST_P(DynamicModulesIntegrationTest, BufferLimitFilter) { initializeFilter("buffer_limit_filter"); @@ -1106,4 +1186,119 @@ TEST_P(DynamicModulesIntegrationTest, ListMetadataCallbacks) { EXPECT_EQ("false", bool_1[0]->value().getStringView()); } +// Verifies the scalar dynamic-metadata getters and SetMetadata. The route is configured +// with metadata under "test_ns" containing string/number/bool values; the filter reads +// them via Route source, writes them into Dynamic source under a test namespace, and on +// the response side reads them back from Dynamic source and surfaces them via headers. +// +// Verifies: GetMetadataString, GetMetadataNumber, GetMetadataBool, SetMetadata, +// GetMetadataKeys. +TEST_P(DynamicModulesIntegrationTest, DynamicMetadata) { + // C++ SDK doesn't currently surface the same scalar metadata API in its integration + // module; skip. + if (GetParam() == "cpp") { + return; + } + config_helper_.addConfigModifier([](envoy::extensions::filters::network::http_connection_manager:: + v3::HttpConnectionManager& hcm) { + // Add metadata to the (single) route under test_ns. + auto* route = + hcm.mutable_route_config()->mutable_virtual_hosts(0)->mutable_routes(0)->mutable_metadata(); + Envoy::Config::Metadata::mutableMetadataValue(*route, "test_ns", "string_key") + .set_string_value("hello_metadata"); + Envoy::Config::Metadata::mutableMetadataValue(*route, "test_ns", "number_key") + .set_number_value(42.0); + Envoy::Config::Metadata::mutableMetadataValue(*route, "test_ns", "bool_key") + .set_bool_value(true); + }); + initializeFilter("dynamic_metadata"); + + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + auto response = + sendRequestAndWaitForResponse(default_request_headers_, 0, default_response_headers_, 0); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); + + auto str_hdr = response->headers().get(Http::LowerCaseString("x-dm-string")); + ASSERT_FALSE(str_hdr.empty()); + EXPECT_EQ("hello_metadata", str_hdr[0]->value().getStringView()); + + auto num_hdr = response->headers().get(Http::LowerCaseString("x-dm-number")); + ASSERT_FALSE(num_hdr.empty()); + // The number is float-formatted; "42" is what both Go's strconv.FormatFloat(42, 'f', -1, 64) + // and Rust's f64::to_string(42.0) produce. + EXPECT_EQ("42", num_hdr[0]->value().getStringView()); + + auto bool_hdr = response->headers().get(Http::LowerCaseString("x-dm-bool")); + ASSERT_FALSE(bool_hdr.empty()); + EXPECT_EQ("true", bool_hdr[0]->value().getStringView()); + + // GetMetadataKeys should report exactly the 3 keys we wrote. + auto count_hdr = response->headers().get(Http::LowerCaseString("x-dm-key-count")); + ASSERT_FALSE(count_hdr.empty()); + EXPECT_EQ("3", count_hdr[0]->value().getStringView()); +} + +// Verifies SetFilterState/GetFilterState round-trip across filter boundaries: +// filter_state_writer (configured with "round_trip_value") sets a key on the request +// side; filter_state_reader (chained after) reads it on the request side and surfaces +// it via x-filter-state-value on the response. +TEST_P(DynamicModulesIntegrationTest, FilterStateRoundTrip) { + if (GetParam() == "cpp") { + return; + } + + // Set up the env var first since initializeFilter normally does it but we're going to + // hand-build the filter chain. + std::string module_name = "http_integration_test"; + if (GetParam() != "rust_static") { + TestEnvironment::setEnvVar( + "ENVOY_DYNAMIC_MODULES_SEARCH_PATH", + TestEnvironment::substitute("{{ test_rundir }}/test/extensions/dynamic_modules/test_data/" + + GetParam()), + 1); + } else { + module_name += "_static"; + } + TestEnvironment::setEnvVar("GODEBUG", "cgocheck=0", 1); + + // The reader must run AFTER the writer — prependFilter inserts at the front, so we + // prepend reader first, then writer (which then ends up at the front). + config_helper_.prependFilter(fmt::format(R"EOF( +name: envoy.extensions.filters.http.dynamic_modules +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_modules.v3.DynamicModuleFilter + dynamic_module_config: + name: {} + filter_name: filter_state_reader + filter_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: "" +)EOF", + module_name)); + config_helper_.prependFilter(fmt::format(R"EOF( +name: envoy.extensions.filters.http.dynamic_modules +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.dynamic_modules.v3.DynamicModuleFilter + dynamic_module_config: + name: {} + filter_name: filter_state_writer + filter_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: round_trip_value +)EOF", + module_name)); + initialize(); + + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + auto response = + sendRequestAndWaitForResponse(default_request_headers_, 0, default_response_headers_, 0); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); + + auto fs_hdr = response->headers().get(Http::LowerCaseString("x-filter-state-value")); + ASSERT_FALSE(fs_hdr.empty()); + EXPECT_EQ("round_trip_value", fs_hdr[0]->value().getStringView()); +} + } // namespace Envoy diff --git a/test/extensions/dynamic_modules/listener/BUILD b/test/extensions/dynamic_modules/listener/BUILD index f0e5cef73156b..18485eff4fc0b 100644 --- a/test/extensions/dynamic_modules/listener/BUILD +++ b/test/extensions/dynamic_modules/listener/BUILD @@ -49,6 +49,31 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "integration_test", + srcs = ["integration_test.cc"], + data = [ + # Rust variant disabled — the Rust SDK is missing listener filter ABI + # exports. See integration_test.cc::getListenerTestParams. + "//test/extensions/dynamic_modules/test_data/go:listener_integration_test", + ], + env = {"GODEBUG": "cgocheck=0"}, + rbe_pool = "6gig", + deps = [ + # See //test/extensions/dynamic_modules/http:filter_test for why every test + # binary that loads a Go test_data .so depends on all_abi_impls. + "//source/extensions/dynamic_modules:all_abi_impls", + "//source/extensions/filters/listener/dynamic_modules:config", + "//source/extensions/filters/network/tcp_proxy:config", + "//test/extensions/dynamic_modules:util", + "//test/integration:integration_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/listener/dynamic_modules/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/network/tcp_proxy/v3:pkg_cc_proto", + ], +) + envoy_cc_test( name = "abi_impl_test", srcs = ["abi_impl_test.cc"], diff --git a/test/extensions/dynamic_modules/listener/integration_test.cc b/test/extensions/dynamic_modules/listener/integration_test.cc new file mode 100644 index 0000000000000..48a68c7895bb5 --- /dev/null +++ b/test/extensions/dynamic_modules/listener/integration_test.cc @@ -0,0 +1,106 @@ +#include "envoy/config/bootstrap/v3/bootstrap.pb.h" +#include "envoy/extensions/filters/listener/dynamic_modules/v3/dynamic_modules.pb.h" +#include "envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.pb.h" + +#include "test/extensions/dynamic_modules/util.h" +#include "test/integration/integration.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace ListenerFilters { +namespace DynamicModules { +namespace { + +// Parameterized over (language, ip_version). Each language ships a +// listener_integration_test module exposing a "test_filter" listener filter that returns +// Continue on accept, allowing the connection to flow through to a TCP proxy upstream. +// This validates the filter is invoked end-to-end without breaking connection setup. +struct ListenerIntegrationParam { + std::string language; + Network::Address::IpVersion ip_version; +}; + +class DynamicModulesListenerIntegrationTest + : public testing::TestWithParam, + public BaseIntegrationTest { +public: + DynamicModulesListenerIntegrationTest() + : BaseIntegrationTest(GetParam().ip_version, ConfigHelper::tcpProxyConfig()) {} + + void SetUp() override { + TestEnvironment::setEnvVar( + "ENVOY_DYNAMIC_MODULES_SEARCH_PATH", + TestEnvironment::substitute("{{ test_rundir }}/test/extensions/dynamic_modules/test_data/" + + GetParam().language), + 1); + TestEnvironment::setEnvVar("GODEBUG", "cgocheck=0", 1); + } + + void initializeWithListenerFilter() { + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + // Insert the dynamic-module listener filter at the front of the listener filter chain. + auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(0); + auto* lf = listener->add_listener_filters(); + lf->set_name("envoy.filters.listener.dynamic_modules"); + envoy::extensions::filters::listener::dynamic_modules::v3::DynamicModuleListenerFilter + filter_proto; + filter_proto.mutable_dynamic_module_config()->set_name("listener_integration_test"); + filter_proto.set_filter_name("test_filter"); + Protobuf::StringValue value; + value.set_value("test_config"); + filter_proto.mutable_filter_config()->PackFrom(value); + lf->mutable_typed_config()->PackFrom(filter_proto); + }); + BaseIntegrationTest::initialize(); + } +}; + +namespace { +std::vector getListenerTestParams() { + std::vector params; + // The Rust SDK currently does not export every listener-filter ABI symbol that + // Envoy expects (e.g. envoy_dynamic_module_on_listener_filter_get_max_read_bytes + // is missing from sdk/rust/src/listener.rs). Once the Rust SDK is brought up to + // parity, "rust" can be added back to this list. + for (const auto& language : {"go"}) { + for (const auto ip : TestEnvironment::getIpVersionsForTest()) { + params.push_back({language, ip}); + } + } + return params; +} + +std::string listenerParamName(const testing::TestParamInfo& info) { + return info.param.language + "_" + + (info.param.ip_version == Network::Address::IpVersion::v4 ? "IPv4" : "IPv6"); +} +} // namespace + +INSTANTIATE_TEST_SUITE_P(LanguagesAndIpVersions, DynamicModulesListenerIntegrationTest, + testing::ValuesIn(getListenerTestParams()), listenerParamName); + +TEST_P(DynamicModulesListenerIntegrationTest, PassthroughTcpProxy) { + initializeWithListenerFilter(); + + IntegrationTcpClientPtr client = makeTcpConnection(lookupPort("listener_0")); + FakeRawConnectionPtr fake_upstream_connection; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_upstream_connection)); + + // Round-trip a payload to confirm the listener filter passed the connection through. + ASSERT_TRUE(client->write("hello")); + ASSERT_TRUE(fake_upstream_connection->waitForData(5)); + ASSERT_TRUE(fake_upstream_connection->write("world")); + client->waitForData("world"); + + client->close(); + ASSERT_TRUE(fake_upstream_connection->waitForDisconnect()); +} + +} // namespace +} // namespace DynamicModules +} // namespace ListenerFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/dynamic_modules/network/BUILD b/test/extensions/dynamic_modules/network/BUILD index f7a405593dc0e..c6241a4a0ffd6 100644 --- a/test/extensions/dynamic_modules/network/BUILD +++ b/test/extensions/dynamic_modules/network/BUILD @@ -83,8 +83,9 @@ envoy_cc_test( env = {"GODEBUG": "cgocheck=0"}, rbe_pool = "6gig", deps = [ - "//source/extensions/dynamic_modules:abi_impl", - "//source/extensions/filters/http/dynamic_modules:abi_impl", + # See //test/extensions/dynamic_modules/http:filter_test for why every test + # binary that loads a Go test_data .so depends on all_abi_impls. + "//source/extensions/dynamic_modules:all_abi_impls", "//source/extensions/filters/network/dynamic_modules:config", "//source/extensions/filters/network/tcp_proxy:config", "//test/integration:integration_lib", diff --git a/test/extensions/dynamic_modules/network/integration_test.cc b/test/extensions/dynamic_modules/network/integration_test.cc index 47dbd4527f9fe..695ebac69a890 100644 --- a/test/extensions/dynamic_modules/network/integration_test.cc +++ b/test/extensions/dynamic_modules/network/integration_test.cc @@ -258,4 +258,62 @@ TEST_P(DynamicModulesNetworkSdkIntegrationTest, BufferLimits) { tcp_client->close(); } +// Verifies StopIteration + ContinueReading: the filter pauses iteration on the first +// OnRead and schedules a continue; without the resume, the upstream never receives any +// bytes. If StopIteration is broken (e.g. ignored, or ContinueReading is broken), this +// test will hang waiting for waitForData and time out. +// +// The C++ test_data module does not implement this filter shape; this test is +// rust/go-only. +TEST_P(DynamicModulesNetworkSdkIntegrationTest, PauseResume) { + if (GetParam() == "cpp") { + return; + } + initializeSdkFilter("pause_resume"); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); + ASSERT_TRUE(tcp_client->connected()); + + FakeRawConnectionPtr fake_upstream_connection; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_upstream_connection)); + + ASSERT_TRUE(tcp_client->write("hello", false)); + ASSERT_TRUE(fake_upstream_connection->waitForData(5)); + + ASSERT_TRUE(tcp_client->write("", true)); + ASSERT_TRUE(fake_upstream_connection->waitForHalfClose()); + ASSERT_TRUE(fake_upstream_connection->close()); + tcp_client->waitForHalfClose(); + tcp_client->close(); +} + +// Verifies the filter can mutate the read buffer (Append/append_read_buffer). The +// upstream should observe "hello|appended" instead of just "hello". +// +// The C++ test_data module does not implement this filter shape; this test is +// rust/go-only. +TEST_P(DynamicModulesNetworkSdkIntegrationTest, DataAppender) { + if (GetParam() == "cpp") { + return; + } + initializeSdkFilter("data_appender"); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); + ASSERT_TRUE(tcp_client->connected()); + + FakeRawConnectionPtr fake_upstream_connection; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_upstream_connection)); + + ASSERT_TRUE(tcp_client->write("hello", false)); + // The filter appends "|appended" so the upstream should see 5 + 9 = 14 bytes. + ASSERT_TRUE(fake_upstream_connection->waitForData( + FakeRawConnection::waitForInexactMatch("hello|appended"))); + + ASSERT_TRUE(tcp_client->write("", true)); + ASSERT_TRUE(fake_upstream_connection->waitForHalfClose()); + ASSERT_TRUE(fake_upstream_connection->close()); + tcp_client->waitForHalfClose(); + tcp_client->close(); +} + } // namespace Envoy diff --git a/test/extensions/dynamic_modules/test_data/c/BUILD b/test/extensions/dynamic_modules/test_data/c/BUILD index 925c7d0465875..bfc43ca3a5e8a 100644 --- a/test/extensions/dynamic_modules/test_data/c/BUILD +++ b/test/extensions/dynamic_modules/test_data/c/BUILD @@ -190,8 +190,20 @@ test_program(name = "cert_validator_empty_digest") test_program(name = "cert_validator_filter_state") +test_program(name = "cert_validator_missing_config_destroy") + +test_program(name = "cert_validator_missing_do_verify") + +test_program(name = "cert_validator_missing_get_verify_mode") + +test_program(name = "cert_validator_missing_update_digest") + +test_program(name = "cert_validator_unknown_status") + test_program(name = "upstream_bridge_no_op") +test_program(name = "upstream_bridge_send_upstream") + test_program(name = "upstream_bridge_config_new_fail") test_program(name = "upstream_bridge_new_fail") diff --git a/test/extensions/dynamic_modules/test_data/c/cert_validator_missing_config_destroy.c b/test/extensions/dynamic_modules/test_data/c/cert_validator_missing_config_destroy.c new file mode 100644 index 0000000000000..753799ddc0bc5 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/c/cert_validator_missing_config_destroy.c @@ -0,0 +1,61 @@ +// Cert validator fake that omits envoy_dynamic_module_on_cert_validator_config_destroy. Used to +// exercise the matching symbol-resolution failure path in newDynamicModuleCertValidatorConfig. +#include +#include + +#include "source/extensions/dynamic_modules/abi/abi.h" + +envoy_dynamic_module_type_abi_version_module_ptr envoy_dynamic_module_on_program_init(void) { + return envoy_dynamic_modules_abi_version; +} + +static int config_dummy = 0; + +envoy_dynamic_module_type_cert_validator_config_module_ptr +envoy_dynamic_module_on_cert_validator_config_new( + envoy_dynamic_module_type_cert_validator_config_envoy_ptr config_envoy_ptr, + envoy_dynamic_module_type_envoy_buffer name, envoy_dynamic_module_type_envoy_buffer config) { + (void)config_envoy_ptr; + (void)name; + (void)config; + return &config_dummy; +} + +// Intentionally NOT defining envoy_dynamic_module_on_cert_validator_config_destroy. + +envoy_dynamic_module_type_cert_validator_validation_result +envoy_dynamic_module_on_cert_validator_do_verify_cert_chain( + envoy_dynamic_module_type_cert_validator_config_envoy_ptr config_envoy_ptr, + envoy_dynamic_module_type_cert_validator_config_module_ptr config_module_ptr, + envoy_dynamic_module_type_envoy_buffer* certs, size_t certs_count, + envoy_dynamic_module_type_envoy_buffer host_name, bool is_server) { + (void)config_envoy_ptr; + (void)config_module_ptr; + (void)certs; + (void)certs_count; + (void)host_name; + (void)is_server; + envoy_dynamic_module_type_cert_validator_validation_result result; + result.status = envoy_dynamic_module_type_cert_validator_validation_status_Successful; + result.detailed_status = + envoy_dynamic_module_type_cert_validator_client_validation_status_Validated; + result.tls_alert = 0; + result.has_tls_alert = false; + return result; +} + +int envoy_dynamic_module_on_cert_validator_get_ssl_verify_mode( + envoy_dynamic_module_type_cert_validator_config_module_ptr config_module_ptr, + bool handshaker_provides_certificates) { + (void)config_module_ptr; + (void)handshaker_provides_certificates; + return 0; +} + +void envoy_dynamic_module_on_cert_validator_update_digest( + envoy_dynamic_module_type_cert_validator_config_module_ptr config_module_ptr, + envoy_dynamic_module_type_module_buffer* out_data) { + (void)config_module_ptr; + out_data->ptr = NULL; + out_data->length = 0; +} diff --git a/test/extensions/dynamic_modules/test_data/c/cert_validator_missing_do_verify.c b/test/extensions/dynamic_modules/test_data/c/cert_validator_missing_do_verify.c new file mode 100644 index 0000000000000..e77a85443c343 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/c/cert_validator_missing_do_verify.c @@ -0,0 +1,45 @@ +// Cert validator fake that omits envoy_dynamic_module_on_cert_validator_do_verify_cert_chain. Used +// to exercise the matching symbol-resolution failure path in newDynamicModuleCertValidatorConfig. +#include +#include + +#include "source/extensions/dynamic_modules/abi/abi.h" + +envoy_dynamic_module_type_abi_version_module_ptr envoy_dynamic_module_on_program_init(void) { + return envoy_dynamic_modules_abi_version; +} + +static int config_dummy = 0; + +envoy_dynamic_module_type_cert_validator_config_module_ptr +envoy_dynamic_module_on_cert_validator_config_new( + envoy_dynamic_module_type_cert_validator_config_envoy_ptr config_envoy_ptr, + envoy_dynamic_module_type_envoy_buffer name, envoy_dynamic_module_type_envoy_buffer config) { + (void)config_envoy_ptr; + (void)name; + (void)config; + return &config_dummy; +} + +void envoy_dynamic_module_on_cert_validator_config_destroy( + envoy_dynamic_module_type_cert_validator_config_module_ptr config_module_ptr) { + (void)config_module_ptr; +} + +// Intentionally NOT defining envoy_dynamic_module_on_cert_validator_do_verify_cert_chain. + +int envoy_dynamic_module_on_cert_validator_get_ssl_verify_mode( + envoy_dynamic_module_type_cert_validator_config_module_ptr config_module_ptr, + bool handshaker_provides_certificates) { + (void)config_module_ptr; + (void)handshaker_provides_certificates; + return 0; +} + +void envoy_dynamic_module_on_cert_validator_update_digest( + envoy_dynamic_module_type_cert_validator_config_module_ptr config_module_ptr, + envoy_dynamic_module_type_module_buffer* out_data) { + (void)config_module_ptr; + out_data->ptr = NULL; + out_data->length = 0; +} diff --git a/test/extensions/dynamic_modules/test_data/c/cert_validator_missing_get_verify_mode.c b/test/extensions/dynamic_modules/test_data/c/cert_validator_missing_get_verify_mode.c new file mode 100644 index 0000000000000..83f43e7d7c290 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/c/cert_validator_missing_get_verify_mode.c @@ -0,0 +1,58 @@ +// Cert validator fake that omits envoy_dynamic_module_on_cert_validator_get_ssl_verify_mode. Used +// to exercise the matching symbol-resolution failure path in newDynamicModuleCertValidatorConfig. +#include +#include + +#include "source/extensions/dynamic_modules/abi/abi.h" + +envoy_dynamic_module_type_abi_version_module_ptr envoy_dynamic_module_on_program_init(void) { + return envoy_dynamic_modules_abi_version; +} + +static int config_dummy = 0; + +envoy_dynamic_module_type_cert_validator_config_module_ptr +envoy_dynamic_module_on_cert_validator_config_new( + envoy_dynamic_module_type_cert_validator_config_envoy_ptr config_envoy_ptr, + envoy_dynamic_module_type_envoy_buffer name, envoy_dynamic_module_type_envoy_buffer config) { + (void)config_envoy_ptr; + (void)name; + (void)config; + return &config_dummy; +} + +void envoy_dynamic_module_on_cert_validator_config_destroy( + envoy_dynamic_module_type_cert_validator_config_module_ptr config_module_ptr) { + (void)config_module_ptr; +} + +envoy_dynamic_module_type_cert_validator_validation_result +envoy_dynamic_module_on_cert_validator_do_verify_cert_chain( + envoy_dynamic_module_type_cert_validator_config_envoy_ptr config_envoy_ptr, + envoy_dynamic_module_type_cert_validator_config_module_ptr config_module_ptr, + envoy_dynamic_module_type_envoy_buffer* certs, size_t certs_count, + envoy_dynamic_module_type_envoy_buffer host_name, bool is_server) { + (void)config_envoy_ptr; + (void)config_module_ptr; + (void)certs; + (void)certs_count; + (void)host_name; + (void)is_server; + envoy_dynamic_module_type_cert_validator_validation_result result; + result.status = envoy_dynamic_module_type_cert_validator_validation_status_Successful; + result.detailed_status = + envoy_dynamic_module_type_cert_validator_client_validation_status_Validated; + result.tls_alert = 0; + result.has_tls_alert = false; + return result; +} + +// Intentionally NOT defining envoy_dynamic_module_on_cert_validator_get_ssl_verify_mode. + +void envoy_dynamic_module_on_cert_validator_update_digest( + envoy_dynamic_module_type_cert_validator_config_module_ptr config_module_ptr, + envoy_dynamic_module_type_module_buffer* out_data) { + (void)config_module_ptr; + out_data->ptr = NULL; + out_data->length = 0; +} diff --git a/test/extensions/dynamic_modules/test_data/c/cert_validator_missing_update_digest.c b/test/extensions/dynamic_modules/test_data/c/cert_validator_missing_update_digest.c new file mode 100644 index 0000000000000..83ed9bf5d1724 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/c/cert_validator_missing_update_digest.c @@ -0,0 +1,58 @@ +// Cert validator fake that omits envoy_dynamic_module_on_cert_validator_update_digest. Used to +// exercise the matching symbol-resolution failure path in newDynamicModuleCertValidatorConfig. +#include +#include + +#include "source/extensions/dynamic_modules/abi/abi.h" + +envoy_dynamic_module_type_abi_version_module_ptr envoy_dynamic_module_on_program_init(void) { + return envoy_dynamic_modules_abi_version; +} + +static int config_dummy = 0; + +envoy_dynamic_module_type_cert_validator_config_module_ptr +envoy_dynamic_module_on_cert_validator_config_new( + envoy_dynamic_module_type_cert_validator_config_envoy_ptr config_envoy_ptr, + envoy_dynamic_module_type_envoy_buffer name, envoy_dynamic_module_type_envoy_buffer config) { + (void)config_envoy_ptr; + (void)name; + (void)config; + return &config_dummy; +} + +void envoy_dynamic_module_on_cert_validator_config_destroy( + envoy_dynamic_module_type_cert_validator_config_module_ptr config_module_ptr) { + (void)config_module_ptr; +} + +envoy_dynamic_module_type_cert_validator_validation_result +envoy_dynamic_module_on_cert_validator_do_verify_cert_chain( + envoy_dynamic_module_type_cert_validator_config_envoy_ptr config_envoy_ptr, + envoy_dynamic_module_type_cert_validator_config_module_ptr config_module_ptr, + envoy_dynamic_module_type_envoy_buffer* certs, size_t certs_count, + envoy_dynamic_module_type_envoy_buffer host_name, bool is_server) { + (void)config_envoy_ptr; + (void)config_module_ptr; + (void)certs; + (void)certs_count; + (void)host_name; + (void)is_server; + envoy_dynamic_module_type_cert_validator_validation_result result; + result.status = envoy_dynamic_module_type_cert_validator_validation_status_Successful; + result.detailed_status = + envoy_dynamic_module_type_cert_validator_client_validation_status_Validated; + result.tls_alert = 0; + result.has_tls_alert = false; + return result; +} + +int envoy_dynamic_module_on_cert_validator_get_ssl_verify_mode( + envoy_dynamic_module_type_cert_validator_config_module_ptr config_module_ptr, + bool handshaker_provides_certificates) { + (void)config_module_ptr; + (void)handshaker_provides_certificates; + return 0; +} + +// Intentionally NOT defining envoy_dynamic_module_on_cert_validator_update_digest. diff --git a/test/extensions/dynamic_modules/test_data/c/cert_validator_unknown_status.c b/test/extensions/dynamic_modules/test_data/c/cert_validator_unknown_status.c new file mode 100644 index 0000000000000..d0c69cce6938a --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/c/cert_validator_unknown_status.c @@ -0,0 +1,65 @@ +// Cert validator fake that returns an out-of-range detailed_status to exercise the default +// branch of the switch in DynamicModuleCertValidator::doVerifyCertChain. +#include +#include + +#include "source/extensions/dynamic_modules/abi/abi.h" + +envoy_dynamic_module_type_abi_version_module_ptr envoy_dynamic_module_on_program_init(void) { + return envoy_dynamic_modules_abi_version; +} + +static int config_dummy = 0; + +envoy_dynamic_module_type_cert_validator_config_module_ptr +envoy_dynamic_module_on_cert_validator_config_new( + envoy_dynamic_module_type_cert_validator_config_envoy_ptr config_envoy_ptr, + envoy_dynamic_module_type_envoy_buffer name, envoy_dynamic_module_type_envoy_buffer config) { + (void)config_envoy_ptr; + (void)name; + (void)config; + return &config_dummy; +} + +void envoy_dynamic_module_on_cert_validator_config_destroy( + envoy_dynamic_module_type_cert_validator_config_module_ptr config_module_ptr) { + (void)config_module_ptr; +} + +envoy_dynamic_module_type_cert_validator_validation_result +envoy_dynamic_module_on_cert_validator_do_verify_cert_chain( + envoy_dynamic_module_type_cert_validator_config_envoy_ptr config_envoy_ptr, + envoy_dynamic_module_type_cert_validator_config_module_ptr config_module_ptr, + envoy_dynamic_module_type_envoy_buffer* certs, size_t certs_count, + envoy_dynamic_module_type_envoy_buffer host_name, bool is_server) { + (void)config_envoy_ptr; + (void)config_module_ptr; + (void)certs; + (void)certs_count; + (void)host_name; + (void)is_server; + envoy_dynamic_module_type_cert_validator_validation_result result; + result.status = envoy_dynamic_module_type_cert_validator_validation_status_Successful; + // 9999 is not a valid client_validation_status enum value; the consumer must fall through + // to the default branch. + result.detailed_status = (envoy_dynamic_module_type_cert_validator_client_validation_status)9999; + result.tls_alert = 0; + result.has_tls_alert = false; + return result; +} + +int envoy_dynamic_module_on_cert_validator_get_ssl_verify_mode( + envoy_dynamic_module_type_cert_validator_config_module_ptr config_module_ptr, + bool handshaker_provides_certificates) { + (void)config_module_ptr; + (void)handshaker_provides_certificates; + return 0; +} + +void envoy_dynamic_module_on_cert_validator_update_digest( + envoy_dynamic_module_type_cert_validator_config_module_ptr config_module_ptr, + envoy_dynamic_module_type_module_buffer* out_data) { + (void)config_module_ptr; + out_data->ptr = NULL; + out_data->length = 0; +} diff --git a/test/extensions/dynamic_modules/test_data/c/upstream_bridge_send_upstream.c b/test/extensions/dynamic_modules/test_data/c/upstream_bridge_send_upstream.c new file mode 100644 index 0000000000000..a6910008753c2 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/c/upstream_bridge_send_upstream.c @@ -0,0 +1,110 @@ +// Upstream bridge fake that exercises the send_upstream_data and send_response_trailers +// ABI callbacks (untested by the other fakes). encode_headers writes upstream data and +// half-closes the upstream connection; on_upstream_data emits response trailers. +#include +#include +#include + +#include "source/extensions/dynamic_modules/abi/abi.h" + +envoy_dynamic_module_type_abi_version_module_ptr envoy_dynamic_module_on_program_init(void) { + return envoy_dynamic_modules_abi_version; +} + +envoy_dynamic_module_type_upstream_http_tcp_bridge_config_module_ptr +envoy_dynamic_module_on_upstream_http_tcp_bridge_config_new( + envoy_dynamic_module_type_upstream_http_tcp_bridge_config_envoy_ptr config_envoy_ptr, + envoy_dynamic_module_type_envoy_buffer name, envoy_dynamic_module_type_envoy_buffer config) { + (void)config_envoy_ptr; + (void)name; + (void)config; + return (envoy_dynamic_module_type_upstream_http_tcp_bridge_config_module_ptr)0x1; +} + +void envoy_dynamic_module_on_upstream_http_tcp_bridge_config_destroy( + envoy_dynamic_module_type_upstream_http_tcp_bridge_config_module_ptr config_module_ptr) { + (void)config_module_ptr; +} + +envoy_dynamic_module_type_upstream_http_tcp_bridge_module_ptr +envoy_dynamic_module_on_upstream_http_tcp_bridge_new( + envoy_dynamic_module_type_upstream_http_tcp_bridge_config_module_ptr config_module_ptr, + envoy_dynamic_module_type_upstream_http_tcp_bridge_envoy_ptr bridge_envoy_ptr) { + (void)config_module_ptr; + (void)bridge_envoy_ptr; + return (envoy_dynamic_module_type_upstream_http_tcp_bridge_module_ptr)0x2; +} + +void envoy_dynamic_module_on_upstream_http_tcp_bridge_encode_headers( + envoy_dynamic_module_type_upstream_http_tcp_bridge_envoy_ptr bridge_envoy_ptr, + envoy_dynamic_module_type_upstream_http_tcp_bridge_module_ptr bridge_module_ptr, + bool end_of_stream) { + (void)bridge_module_ptr; + (void)end_of_stream; + + // Write some bytes upstream and end-stream the connection. This walks the buffer.add + + // enableHalfClose + connection.write paths in HttpTcpBridge::sendUpstreamData. + const char* payload = "hello-upstream"; + envoy_dynamic_module_type_module_buffer payload_buf = {.ptr = payload, .length = 14}; + envoy_dynamic_module_callback_upstream_http_tcp_bridge_send_upstream_data(bridge_envoy_ptr, + payload_buf, true); + + // Also exercise the empty-data, end_stream=true branch (buffer empty, half-close still fires). + envoy_dynamic_module_type_module_buffer empty_buf = {.ptr = NULL, .length = 0}; + envoy_dynamic_module_callback_upstream_http_tcp_bridge_send_upstream_data(bridge_envoy_ptr, + empty_buf, true); +} + +void envoy_dynamic_module_on_upstream_http_tcp_bridge_encode_data( + envoy_dynamic_module_type_upstream_http_tcp_bridge_envoy_ptr bridge_envoy_ptr, + envoy_dynamic_module_type_upstream_http_tcp_bridge_module_ptr bridge_module_ptr, + bool end_of_stream) { + (void)bridge_envoy_ptr; + (void)bridge_module_ptr; + (void)end_of_stream; +} + +void envoy_dynamic_module_on_upstream_http_tcp_bridge_encode_trailers( + envoy_dynamic_module_type_upstream_http_tcp_bridge_envoy_ptr bridge_envoy_ptr, + envoy_dynamic_module_type_upstream_http_tcp_bridge_module_ptr bridge_module_ptr) { + (void)bridge_envoy_ptr; + (void)bridge_module_ptr; +} + +void envoy_dynamic_module_on_upstream_http_tcp_bridge_on_upstream_data( + envoy_dynamic_module_type_upstream_http_tcp_bridge_envoy_ptr bridge_envoy_ptr, + envoy_dynamic_module_type_upstream_http_tcp_bridge_module_ptr bridge_module_ptr, + bool end_of_stream) { + (void)bridge_module_ptr; + (void)end_of_stream; + + // Send a response with headers, then trailers — this hits sendResponseHeaders and the + // sendResponseTrailers paths in HttpTcpBridge. + envoy_dynamic_module_callback_upstream_http_tcp_bridge_send_response_headers(bridge_envoy_ptr, + 200, NULL, 0, false); + + envoy_dynamic_module_type_module_http_header trailers[2]; + const char* k0 = "x-trailer"; + const char* v0 = "first"; + trailers[0].key_ptr = (char*)k0; + trailers[0].key_length = 9; + trailers[0].value_ptr = (char*)v0; + trailers[0].value_length = 5; + const char* k1 = "x-status"; + const char* v1 = "ok"; + trailers[1].key_ptr = (char*)k1; + trailers[1].key_length = 8; + trailers[1].value_ptr = (char*)v1; + trailers[1].value_length = 2; + envoy_dynamic_module_callback_upstream_http_tcp_bridge_send_response_trailers(bridge_envoy_ptr, + trailers, 2); + + // Also exercise send_response_trailers with NULL trailers vector (early-return loop guard). + envoy_dynamic_module_callback_upstream_http_tcp_bridge_send_response_trailers(bridge_envoy_ptr, + NULL, 0); +} + +void envoy_dynamic_module_on_upstream_http_tcp_bridge_destroy( + envoy_dynamic_module_type_upstream_http_tcp_bridge_module_ptr bridge_module_ptr) { + (void)bridge_module_ptr; +} diff --git a/test/extensions/dynamic_modules/test_data/cpp/network_integration_test.cc b/test/extensions/dynamic_modules/test_data/cpp/network_integration_test.cc index 000cbef832ce6..7783e13e8ecaf 100644 --- a/test/extensions/dynamic_modules/test_data/cpp/network_integration_test.cc +++ b/test/extensions/dynamic_modules/test_data/cpp/network_integration_test.cc @@ -13,12 +13,12 @@ class FlowControlFilter : public NetworkFilter { NetworkFilterStatus onRead(NetworkBuffer&, bool) override { if (!reads_disabled_) { - const auto disable_status = handle_.readDisable(true); + [[maybe_unused]] const auto disable_status = handle_.readDisable(true); assert(disable_status == NetworkReadDisableStatus::TransitionedToReadDisabled); assert(!handle_.readEnabled()); reads_disabled_ = true; - const auto enable_status = handle_.readDisable(false); + [[maybe_unused]] const auto enable_status = handle_.readDisable(false); assert(enable_status == NetworkReadDisableStatus::TransitionedToReadEnabled); assert(handle_.readEnabled()); } @@ -77,7 +77,8 @@ class ConnectionStateFilter : public NetworkFilter { void onDestroy() override {} private: - NetworkFilterHandle& handle_; + // Only referenced inside assert(), which is compiled out in release builds. + [[maybe_unused]] NetworkFilterHandle& handle_; }; class ConnectionStateFactory : public NetworkFilterFactory { diff --git a/test/extensions/dynamic_modules/test_data/go/BUILD b/test/extensions/dynamic_modules/test_data/go/BUILD index 20415b8a5fe9c..dec13e5c24978 100644 --- a/test/extensions/dynamic_modules/test_data/go/BUILD +++ b/test/extensions/dynamic_modules/test_data/go/BUILD @@ -7,3 +7,45 @@ test_program(name = "http") test_program(name = "http_integration_test") test_program(name = "network_integration_test") + +test_program(name = "bootstrap_admin_handler_test") + +test_program(name = "bootstrap_cluster_lifecycle_test") + +test_program(name = "bootstrap_file_watcher_test") + +test_program(name = "bootstrap_function_registry_test") + +test_program(name = "bootstrap_init_target_test") + +test_program(name = "bootstrap_integration_test") + +test_program(name = "bootstrap_listener_lifecycle_test") + +test_program(name = "bootstrap_shared_data_test") + +test_program(name = "bootstrap_stats_test") + +test_program(name = "bootstrap_timer_test") + +test_program(name = "cluster_integration_test") + +test_program(name = "access_log_integration_test") + +test_program(name = "http_stream_callouts_test") + +test_program(name = "upstream_http_tcp_bridge") + +test_program(name = "bootstrap_http_combined_test") + +test_program(name = "udp_integration_test") + +test_program(name = "matcher_integration_test") + +test_program(name = "listener_integration_test") + +test_program(name = "tracer_integration_test") + +test_program(name = "load_balancer_integration_test") + +test_program(name = "cert_validator_test") diff --git a/test/extensions/dynamic_modules/test_data/go/access_log_integration_test/access_log_integration_test.go b/test/extensions/dynamic_modules/test_data/go/access_log_integration_test/access_log_integration_test.go new file mode 100644 index 0000000000000..ed65fedc536a0 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/access_log_integration_test/access_log_integration_test.go @@ -0,0 +1,226 @@ +// Integration test module for access logger dynamic modules. Mirrors +// test_data/rust/access_log_integration_test.rs. +// +// Registers an access logger that: +// - Calls every AccessLogContext getter (~50 of them) to ensure the dispatch path is +// wired and no method panics on real Envoy data. +// - Captures select getter results into per-config counters/gauges so the C++ driver +// can assert via /stats that the values match what Envoy actually saw on the wire. +// +// Counters defined (all incremented per Log() call): +// +// test_log_count — number of Log() calls observed. +// test_request_protocol_http2 — incremented when request_protocol == "HTTP/2". +// test_response_code_200 — incremented when response_code == 200. +// test_method_get — incremented when :method == "GET". +// test_path_test — incremented when :path == "/test". +// test_has_route_name — incremented when xds_route_name returned ok. +// +// Gauges (set per Log() call): +// +// test_response_code_last — last observed response_code as uint64. +// test_bytes_sent_last — last observed bytes_sent from BytesInfo. +// test_request_headers_count — current request header count. +package main + +import ( + "sync/atomic" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterAccessLoggerConfigFactories(map[string]shared.AccessLoggerConfigFactory{ + "test_logger": &testLoggerConfigFactory{}, + }) +} + +func main() {} + +// Process-wide counters; the test asserts indirectly by sending requests and checking the +// upstream side completes without crash, so we use these mainly for diagnostics. +var ( + logCount atomic.Uint32 + flushCount atomic.Uint32 +) + +type testLoggerConfigFactory struct { + shared.EmptyAccessLoggerConfigFactory +} + +func (f *testLoggerConfigFactory) Create(handle shared.AccessLoggerConfigHandle, _ []byte) (shared.AccessLoggerFactory, error) { + logCountID, _ := handle.DefineCounter("test_log_count") + httpProtoID, _ := handle.DefineCounter("test_request_protocol_http2") + resp200ID, _ := handle.DefineCounter("test_response_code_200") + methodGetID, _ := handle.DefineCounter("test_method_get") + pathTestID, _ := handle.DefineCounter("test_path_test") + hasRouteID, _ := handle.DefineCounter("test_has_route_name") + + respCodeGaugeID, _ := handle.DefineGauge("test_response_code_last") + bytesSentGaugeID, _ := handle.DefineGauge("test_bytes_sent_last") + hdrCountGaugeID, _ := handle.DefineGauge("test_request_headers_count") + + return &testLoggerFactory{ + handle: handle, + logCountID: logCountID, + httpProtoID: httpProtoID, + resp200ID: resp200ID, + methodGetID: methodGetID, + pathTestID: pathTestID, + hasRouteID: hasRouteID, + respCodeGaugeID: respCodeGaugeID, + bytesSentGaugeID: bytesSentGaugeID, + hdrCountGaugeID: hdrCountGaugeID, + }, nil +} + +type testLoggerFactory struct { + shared.EmptyAccessLoggerFactory + handle shared.AccessLoggerConfigHandle + logCountID shared.MetricID + httpProtoID shared.MetricID + resp200ID shared.MetricID + methodGetID shared.MetricID + pathTestID shared.MetricID + hasRouteID shared.MetricID + respCodeGaugeID shared.MetricID + bytesSentGaugeID shared.MetricID + hdrCountGaugeID shared.MetricID +} + +func (f *testLoggerFactory) Create() shared.AccessLogger { + return &testLogger{factory: f} +} + +type testLogger struct { + shared.EmptyAccessLogger + factory *testLoggerFactory +} + +func (l *testLogger) Log(ctx shared.AccessLogContext, _ shared.AccessLogType) { + logCount.Add(1) + f := l.factory + f.handle.IncrementCounter(f.logCountID, 1) + + // Exercise every AccessLogContext getter so the dispatch path for each ABI callback + // is hit on every request. We additionally record values from a small subset into + // counters/gauges so the C++ driver can assert correctness via /stats. + + _ = ctx.GetWorkerIndex() + + // :method header — bump counter when GET. + if v, n, ok := ctx.GetHeaderValue(shared.HttpHeaderTypeRequestHeader, ":method", 0); ok && n > 0 { + if v.ToUnsafeString() == "GET" { + f.handle.IncrementCounter(f.methodGetID, 1) + } + } + // :path — bump counter when "/test". + if v, n, ok := ctx.GetHeaderValue(shared.HttpHeaderTypeRequestHeader, ":path", 0); ok && n > 0 { + if v.ToUnsafeString() == "/test" { + f.handle.IncrementCounter(f.pathTestID, 1) + } + } + + // Bulk header iteration on all three maps to make sure dispatch survives. + for _, ht := range []shared.HttpHeaderType{ + shared.HttpHeaderTypeRequestHeader, + shared.HttpHeaderTypeResponseHeader, + shared.HttpHeaderTypeResponseTrailer, + } { + _ = ctx.GetHeadersSize(ht) + _ = ctx.GetHeaders(ht) + } + // Record request header count as a gauge. + f.handle.SetGauge(f.hdrCountGaugeID, ctx.GetHeadersSize(shared.HttpHeaderTypeRequestHeader)) + + // Attribute getters across all three return types. + if v, ok := ctx.GetAttributeString(shared.AttributeIDRequestProtocol); ok { + if v.ToUnsafeString() == "HTTP/2" { + f.handle.IncrementCounter(f.httpProtoID, 1) + } + } + _, _ = ctx.GetAttributeString(shared.AttributeIDXdsRouteName) + _, _ = ctx.GetAttributeString(shared.AttributeIDRequestPath) + if code, ok := ctx.GetAttributeNumber(shared.AttributeIDResponseCode); ok { + f.handle.SetGauge(f.respCodeGaugeID, uint64(code)) + if uint64(code) == 200 { + f.handle.IncrementCounter(f.resp200ID, 1) + } + } + _, _ = ctx.GetAttributeNumber(shared.AttributeIDConnectionId) + _, _ = ctx.GetAttributeBool(shared.AttributeIDConnectionMTLS) + + // Response flags. + _ = ctx.HasResponseFlag(shared.ResponseFlagNoRouteFound) + _ = ctx.GetResponseFlags() + + // Timing + bytes — record bytes_sent into a gauge. + _ = ctx.GetTimingInfo() + bi := ctx.GetBytesInfo() + f.handle.SetGauge(f.bytesSentGaugeID, bi.BytesSent) + + // Addresses. + _, _, _ = ctx.GetDownstreamRemoteAddress() + _, _, _ = ctx.GetDownstreamLocalAddress() + _, _, _ = ctx.GetDownstreamDirectRemoteAddress() + _, _, _ = ctx.GetDownstreamDirectLocalAddress() + _, _, _ = ctx.GetUpstreamRemoteAddress() + _, _, _ = ctx.GetUpstreamLocalAddress() + + // Upstream info. + _, _ = ctx.GetUpstreamCluster() + _, _ = ctx.GetUpstreamHost() + _ = ctx.GetUpstreamConnectionID() + _, _ = ctx.GetUpstreamTLSCipher() + _, _ = ctx.GetUpstreamTLSSessionID() + _, _ = ctx.GetUpstreamPeerIssuer() + _ = ctx.GetUpstreamPeerCertValidityStart() + _ = ctx.GetUpstreamPeerCertValidityEnd() + _ = ctx.GetUpstreamPeerURISans() + _ = ctx.GetUpstreamLocalURISans() + _ = ctx.GetUpstreamPeerDNSSans() + _ = ctx.GetUpstreamLocalDNSSans() + + // Downstream TLS / connection info. + _, _ = ctx.GetDownstreamTLSCipher() + _, _ = ctx.GetDownstreamTLSSessionID() + _, _ = ctx.GetDownstreamPeerIssuer() + _, _ = ctx.GetDownstreamPeerSerial() + _, _ = ctx.GetDownstreamPeerFingerprint1() + _ = ctx.GetDownstreamPeerCertPresented() + _ = ctx.GetDownstreamPeerCertValidated() + _ = ctx.GetDownstreamPeerCertValidityStart() + _ = ctx.GetDownstreamPeerCertValidityEnd() + _ = ctx.GetDownstreamPeerURISans() + _ = ctx.GetDownstreamLocalURISans() + _ = ctx.GetDownstreamPeerDNSSans() + _ = ctx.GetDownstreamLocalDNSSans() + + // Metadata / filter state / tracing. + _, _ = ctx.GetDynamicMetadata("envoy.filters.http.dynamic_modules", "test_key") + _, _ = ctx.GetFilterState("test_key") + _, _ = ctx.GetLocalReplyBody() + _, _ = ctx.GetTraceID() + _, _ = ctx.GetSpanID() + _ = ctx.IsTraceSampled() + + // Misc. + _, _ = ctx.GetJA3Hash() + _, _ = ctx.GetJA4Hash() + _ = ctx.GetRequestHeadersBytes() + _ = ctx.GetResponseHeadersBytes() + _ = ctx.GetResponseTrailersBytes() + _, _ = ctx.GetUpstreamProtocol() + _ = ctx.GetUpstreamPoolReadyDurationNs() + + // xds_route_name presence check (route is configured on the test). + if _, ok := ctx.GetAttributeString(shared.AttributeIDXdsRouteName); ok { + f.handle.IncrementCounter(f.hasRouteID, 1) + } +} + +func (l *testLogger) Flush() { + flushCount.Add(1) +} diff --git a/test/extensions/dynamic_modules/test_data/go/bootstrap_admin_handler_test/bootstrap_admin_handler_test.go b/test/extensions/dynamic_modules/test_data/go/bootstrap_admin_handler_test/bootstrap_admin_handler_test.go new file mode 100644 index 0000000000000..e7040e1c9fae5 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/bootstrap_admin_handler_test/bootstrap_admin_handler_test.go @@ -0,0 +1,55 @@ +// Test module for the Bootstrap admin-handler API. Mirrors +// test_data/rust/bootstrap_admin_handler_test.rs. +// +// Registers a custom admin endpoint during config_new, then logs the request method/path +// when the endpoint is hit. The C++ integration test asserts on the registered-success +// log line and on the response body. +package main + +import ( + "fmt" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterBootstrapExtensionConfigFactories(map[string]shared.BootstrapExtensionConfigFactory{ + "test": &adminHandlerConfigFactory{}, + }) +} + +func main() {} + +type adminHandlerConfigFactory struct { + shared.EmptyBootstrapExtensionConfigFactory +} + +func (f *adminHandlerConfigFactory) Create(handle shared.BootstrapExtensionConfigHandle, _ []byte) (shared.BootstrapExtension, error) { + // Register the admin handler during config_new — admin is available at this point. + registered := handle.RegisterAdminHandler( + "/dynamic_module_admin_test", + "Dynamic module admin handler test endpoint.", + true, // removable + false, // mutatesServerState + &adminHandler{}, + ) + sdk.Log(shared.LogLevelInfo, "Admin handler registered: %v", registered) + if !registered { + return nil, fmt.Errorf("admin handler registration failed") + } + + handle.SignalInitComplete() + return &shared.EmptyBootstrapExtension{}, nil +} + +type adminHandler struct{} + +func (*adminHandler) HandleAdminRequest(handle shared.BootstrapExtensionConfigHandle, + method, path string, _ []byte) uint32 { + sdk.Log(shared.LogLevelInfo, "Admin request received: %s %s", method, path) + body := fmt.Sprintf("Hello from dynamic module admin handler! method=%s path=%s", method, path) + handle.SetAdminResponse([]byte(body)) + return 200 +} diff --git a/test/extensions/dynamic_modules/test_data/go/bootstrap_cluster_lifecycle_test/bootstrap_cluster_lifecycle_test.go b/test/extensions/dynamic_modules/test_data/go/bootstrap_cluster_lifecycle_test/bootstrap_cluster_lifecycle_test.go new file mode 100644 index 0000000000000..3f53366469224 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/bootstrap_cluster_lifecycle_test/bootstrap_cluster_lifecycle_test.go @@ -0,0 +1,57 @@ +// Test module for the Bootstrap cluster-lifecycle event API. Mirrors +// test_data/rust/bootstrap_cluster_lifecycle_test.rs. +// +// On server initialization, schedules a main-thread closure that calls +// EnableClusterLifecycle() and signals init complete. The extension implements +// BootstrapClusterLifecycleListener so subsequent cluster add/update/removal events flow +// in through the type-asserted dispatch. +package main + +import ( + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterBootstrapExtensionConfigFactories(map[string]shared.BootstrapExtensionConfigFactory{ + "test": &clusterLifecycleConfigFactory{}, + }) +} + +func main() {} + +type clusterLifecycleConfigFactory struct { + shared.EmptyBootstrapExtensionConfigFactory +} + +func (f *clusterLifecycleConfigFactory) Create(handle shared.BootstrapExtensionConfigHandle, _ []byte) (shared.BootstrapExtension, error) { + // Defer SignalInitComplete to OnServerInitialized → scheduled work, mirroring the + // Rust pattern where the scheduler.commit happens in on_server_initialized. + return &clusterLifecycleExtension{handle: handle}, nil +} + +type clusterLifecycleExtension struct { + shared.EmptyBootstrapExtension + handle shared.BootstrapExtensionConfigHandle +} + +func (e *clusterLifecycleExtension) OnServerInitialized(_ shared.BootstrapExtensionHandle) { + sdk.Log(shared.LogLevelInfo, "Bootstrap cluster lifecycle test: server initialized") + scheduler := e.handle.NewScheduler() + scheduler.Schedule(func() { + enabled := e.handle.EnableClusterLifecycle() + sdk.Log(shared.LogLevelInfo, "Cluster lifecycle enabled: %v", enabled) + e.handle.SignalInitComplete() + }) +} + +// BootstrapClusterLifecycleListener implementation — picked up by the SDK via type +// assertion on the BootstrapExtension instance. +func (*clusterLifecycleExtension) OnClusterAddOrUpdate(clusterName string) { + sdk.Log(shared.LogLevelInfo, "Cluster added or updated: %s", clusterName) +} + +func (*clusterLifecycleExtension) OnClusterRemoval(clusterName string) { + sdk.Log(shared.LogLevelInfo, "Cluster removed: %s", clusterName) +} diff --git a/test/extensions/dynamic_modules/test_data/go/bootstrap_file_watcher_test/bootstrap_file_watcher_test.go b/test/extensions/dynamic_modules/test_data/go/bootstrap_file_watcher_test/bootstrap_file_watcher_test.go new file mode 100644 index 0000000000000..d9c59b2f53af1 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/bootstrap_file_watcher_test/bootstrap_file_watcher_test.go @@ -0,0 +1,121 @@ +// Test module for the Bootstrap file-watcher API. Mirrors +// test_data/rust/bootstrap_file_watcher_test.rs. +// +// Config format: two file paths separated by `|`. The module: +// 1. Adds a watch on each path via AddFileWatch. +// 2. Schedules three timer-driven writes (timer A → file_a; timer B → file_b; +// timer C → file_a again) so the watcher must report at least 2 changes on file_a +// and 1 change on file_b. +// 3. Defers SignalInitComplete until all expected change counts are seen, proving the +// watch dispatch path works end-to-end. +package main + +import ( + "os" + "strings" + "sync" + "sync/atomic" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterBootstrapExtensionConfigFactories(map[string]shared.BootstrapExtensionConfigFactory{ + "test": &fileWatcherConfigFactory{}, + }) +} + +func main() {} + +type fileWatcherConfigFactory struct { + shared.EmptyBootstrapExtensionConfigFactory +} + +func (f *fileWatcherConfigFactory) Create(handle shared.BootstrapExtensionConfigHandle, config []byte) (shared.BootstrapExtension, error) { + parts := strings.Split(string(config), "|") + if len(parts) != 2 { + panic("bootstrap_file_watcher_test: config must be two paths separated by |") + } + pathA, pathB := parts[0], parts[1] + sdk.Log(shared.LogLevelInfo, "Watching file_a=%s and file_b=%s", pathA, pathB) + + state := &fileWatcherState{handle: handle, pathA: pathA, pathB: pathB} + + events := shared.FileWatcherEventMovedTo | shared.FileWatcherEventModified + if !handle.AddFileWatch(pathA, events, state.onAChanged) { + panic("add_file_watch for file_a must succeed") + } + if !handle.AddFileWatch(pathB, events, state.onBChanged) { + panic("add_file_watch for file_b must succeed") + } + sdk.Log(shared.LogLevelInfo, "File watches added for 2 files") + + // Schedule three timer-driven writes. Each timer carries its own onFire closure since the + // Go SDK uses per-timer callbacks rather than a centralized on_timer_fired hook. + timerA := handle.NewTimer(func(t shared.BootstrapTimer) { + sdk.Log(shared.LogLevelInfo, "Timer A fired: writing to file_a=%s", pathA) + if err := os.WriteFile(pathA, []byte("change 1 by timer_a"), 0644); err != nil { + sdk.Log(shared.LogLevelError, "Failed to write to file_a: %v", err) + } + t.Delete() + }) + timerB := handle.NewTimer(func(t shared.BootstrapTimer) { + sdk.Log(shared.LogLevelInfo, "Timer B fired: writing to file_b=%s", pathB) + if err := os.WriteFile(pathB, []byte("change 1 by timer_b"), 0644); err != nil { + sdk.Log(shared.LogLevelError, "Failed to write to file_b: %v", err) + } + t.Delete() + }) + timerC := handle.NewTimer(func(t shared.BootstrapTimer) { + sdk.Log(shared.LogLevelInfo, "Timer C fired: writing to file_a again=%s", pathA) + if err := os.WriteFile(pathA, []byte("change 2 by timer_c"), 0644); err != nil { + sdk.Log(shared.LogLevelError, "Failed to write to file_a again: %v", err) + } + t.Delete() + }) + timerA.Enable(10) + timerB.Enable(100) + timerC.Enable(200) + + // Defer SignalInitComplete to onAChanged/onBChanged once the expected change counts + // have been observed. + return &shared.EmptyBootstrapExtension{}, nil +} + +type fileWatcherState struct { + handle shared.BootstrapExtensionConfigHandle + pathA string + pathB string + mu sync.Mutex + aCount atomic.Uint32 + bCount atomic.Uint32 + initSignaled atomic.Bool +} + +func (s *fileWatcherState) onAChanged(path string, events shared.FileWatcherEvent) { + count := s.aCount.Add(1) + sdk.Log(shared.LogLevelInfo, "file_a changed: path=%s, events=0x%x, count=%d", path, uint32(events), count) + s.maybeSignalInitComplete() +} + +func (s *fileWatcherState) onBChanged(path string, events shared.FileWatcherEvent) { + count := s.bCount.Add(1) + sdk.Log(shared.LogLevelInfo, "file_b changed: path=%s, events=0x%x, count=%d", path, uint32(events), count) + s.maybeSignalInitComplete() +} + +func (s *fileWatcherState) maybeSignalInitComplete() { + a := s.aCount.Load() + b := s.bCount.Load() + if a < 2 || b < 1 { + return + } + if s.initSignaled.Swap(true) { + return + } + sdk.Log(shared.LogLevelInfo, "All expected file changes received: file_a=%d, file_b=%d", a, b) + s.handle.SignalInitComplete() + sdk.Log(shared.LogLevelInfo, "Bootstrap file watcher test completed successfully!") +} diff --git a/test/extensions/dynamic_modules/test_data/go/bootstrap_function_registry_test/bootstrap_function_registry_test.go b/test/extensions/dynamic_modules/test_data/go/bootstrap_function_registry_test/bootstrap_function_registry_test.go new file mode 100644 index 0000000000000..3fe6eeaad2521 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/bootstrap_function_registry_test/bootstrap_function_registry_test.go @@ -0,0 +1,89 @@ +// Test module for the process-wide function registry. Mirrors +// test_data/rust/bootstrap_function_registry_test.rs in spirit. +// +// The Rust test registers actual extern "C" functions and invokes them through the +// resolved pointer. Go can't directly convert Go funcs to C function pointers (cgo +// limitation) — and using cgo in this _test.go-suffixed source would make it +// non-buildable under "go build ./...". We therefore exercise the registry mechanism by +// registering two distinct sentinel addresses, retrieving them by key, and asserting +// pointer equality. This validates the SDK code paths (RegisterFunction / GetFunction) +// that the Rust test exercises end-to-end; the function-invocation aspect is exercised +// elsewhere through C test_data fakes. +package main + +import ( + "unsafe" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterBootstrapExtensionConfigFactories(map[string]shared.BootstrapExtensionConfigFactory{ + "test": &functionRegistryConfigFactory{}, + }) +} + +func main() {} + +type functionRegistryConfigFactory struct { + shared.EmptyBootstrapExtensionConfigFactory +} + +func (f *functionRegistryConfigFactory) Create(handle shared.BootstrapExtensionConfigHandle, _ []byte) (shared.BootstrapExtension, error) { + handle.SignalInitComplete() + return &functionRegistryExtension{}, nil +} + +type functionRegistryExtension struct { + shared.EmptyBootstrapExtension +} + +// Sentinel storage we register pointers to. Package-level vars have stable addresses for +// the lifetime of the process — Go's GC won't move them, and they're never dropped. +var ( + answerSentinel uint64 = 0xA1 + doubleSentinel uint64 = 0xB2 +) + +// Keys are namespaced with a "go_" prefix so they don't collide with the Rust +// FunctionRegistryRust test, which registers different pointer values under the +// bare names "get_answer" and "double_it". The registry is process-wide and +// holds whatever was first registered, so under the gtest parameterization the +// Rust run executes first and the bare names already point at Rust functions +// when the Go run starts. +const ( + answerKey = "go_get_answer" + doubleKey = "go_double_it" +) + +func (*functionRegistryExtension) OnServerInitialized(_ shared.BootstrapExtensionHandle) { + answerPtr := unsafe.Pointer(&answerSentinel) + doublePtr := unsafe.Pointer(&doubleSentinel) + + // Under parameterized runs (IPv4 then IPv6) the keys may already exist from the + // prior parameterization; the registry retains the first value. We assert that + // either: (a) registration succeeded and the round-trip returns our pointer, or + // (b) registration was rejected because the same value is already present. + _ = sdk.RegisterFunction(answerKey, answerPtr) + _ = sdk.RegisterFunction(doubleKey, doublePtr) + + if got, ok := sdk.GetFunction(answerKey); !ok { + panic("registered function should be found") + } else if got != answerPtr { + panic("get_answer round-trip returned wrong pointer") + } + + if got, ok := sdk.GetFunction(doubleKey); !ok { + panic("registered function should be found") + } else if got != doublePtr { + panic("double_it round-trip returned wrong pointer") + } + + if _, ok := sdk.GetFunction("no_such_fn"); ok { + panic("non-existent function should not be found") + } + + sdk.Log(shared.LogLevelInfo, "Bootstrap function registry test completed successfully!") +} diff --git a/test/extensions/dynamic_modules/test_data/go/bootstrap_http_combined_test/bootstrap_http_combined_test.go b/test/extensions/dynamic_modules/test_data/go/bootstrap_http_combined_test/bootstrap_http_combined_test.go new file mode 100644 index 0000000000000..09779f2d2233f --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/bootstrap_http_combined_test/bootstrap_http_combined_test.go @@ -0,0 +1,201 @@ +// Test module demonstrating cross-extension data sharing via the function registry. +// Mirrors test_data/rust/bootstrap_http_combined_test.rs. +// +// A single dynamic module registers BOTH a bootstrap extension AND an HTTP filter. The +// bootstrap extension performs asynchronous initialization (off the main thread, via a +// scheduler), populates a routing table, and exposes a lookup function through the +// process-wide function registry. The HTTP filter, on each request, resolves the +// function from the registry and uses it to route requests based on the +// x-target-service header. +// +// Behavior matches the Rust test exactly: +// - x-target-service: "service-a" (or "service-b") -> 200, x-routed-to set on upstream req +// - x-target-service: "unknown-service" -> 503, x-error-reason: service_not_onboarded +// - no x-target-service header -> pass through, no x-routed-to +// +// Cross-language note: the Rust SDK passes a real C function pointer through the +// registry. Go can't synthesize C function pointers without cgo //export, and the +// Bazel-mandated _test.go filename precludes cgo at file level here. We instead +// register the address of a package-level Go function variable. Both extensions live +// in the same .so, so the consumer can dereference the registered unsafe.Pointer back +// to a *func and call it. This still exercises the Envoy round-trip: +// +// sdk.RegisterFunction (host-side store) -> Envoy registry -> sdk.GetFunction (host-side load) +// +// and asserts the registered pointer survives that round-trip with bit-equality. +package main + +import ( + "sync" + "sync/atomic" + "time" + "unsafe" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterBootstrapExtensionConfigFactories(map[string]shared.BootstrapExtensionConfigFactory{ + "combined_test": &combinedBootstrapConfigFactory{}, + }) + sdk.RegisterHttpFilterConfigFactories(map[string]shared.HttpFilterConfigFactory{ + "combined_filter": &combinedHttpFilterConfigFactory{}, + }) +} + +func main() {} //nolint:all + +// ------------------------------------------------------------------------------------- +// Shared state. +// +// routingTableReady gates HTTP filter requests until the bootstrap extension has +// populated the table and registered the lookup. Without this gate the filter could +// race the bootstrap async init. +// +// In production code modules would rely on Envoy's "init target" mechanism — the +// bootstrap extension calls SignalInitComplete only after async work finishes, and +// Envoy holds back listener traffic until then. The Rust test relies on exactly this +// guarantee. We do the same here, but also keep the atomic for an in-process check. +// ------------------------------------------------------------------------------------- + +var ( + routingTable sync.Map // string -> string + routingTableReady atomic.Bool +) + +// getRouteEndpoint is the lookup function the bootstrap extension publishes. The +// HTTP filter retrieves the address of this var, dereferences it back to a func, and +// calls it. Storing the function in a package-level var (not just `func`) gives us a +// stable address for &getRouteEndpoint. +// +// Returns (endpoint, true) on hit, ("", false) on miss. +var getRouteEndpoint = func(service string) (string, bool) { + v, ok := routingTable.Load(service) + if !ok { + return "", false + } + return v.(string), true +} + +// Namespaced with a "go_" prefix so it doesn't collide with the Rust mirror +// (bootstrap_http_combined_test.rs) which registers a Rust extern "C" fn under +// the bare name "get_route_endpoint". The function registry is process-wide and +// the gtest parameterization runs FunctionRegistryCrossFilterRust before +// FunctionRegistryCrossFilterGo within the same process, so the bare key would +// already point at a Rust function pointer when this Go filter resolves it — +// which would crash when re-cast to a Go closure pointer. +const registryKey = "go_get_route_endpoint" + +// ------------------------------------------------------------------------------------- +// Bootstrap extension. +// ------------------------------------------------------------------------------------- + +type combinedBootstrapConfigFactory struct { + shared.EmptyBootstrapExtensionConfigFactory +} + +func (combinedBootstrapConfigFactory) Create(handle shared.BootstrapExtensionConfigHandle, _ []byte) (shared.BootstrapExtension, error) { + scheduler := handle.NewScheduler() + + // Simulate async initialization on a background goroutine. Once done we hop back to + // the main thread via the scheduler to register the lookup function and signal + // init complete. + go func() { + time.Sleep(50 * time.Millisecond) + routingTable.Store("service-a", "10.0.0.1:8080") + routingTable.Store("service-b", "10.0.0.2:9090") + routingTableReady.Store(true) + sdk.Log(shared.LogLevelInfo, "async initialization complete, scheduling readiness signal") + scheduler.Schedule(func() { + ptr := unsafe.Pointer(&getRouteEndpoint) + registered := sdk.RegisterFunction(registryKey, ptr) + sdk.Log(shared.LogLevelInfo, "function registry registration: %v", registered) + handle.SignalInitComplete() + sdk.Log(shared.LogLevelInfo, "bootstrap init signaled complete after async initialization") + }) + }() + + return &combinedBootstrapExtension{}, nil +} + +type combinedBootstrapExtension struct { + shared.EmptyBootstrapExtension +} + +func (*combinedBootstrapExtension) OnServerInitialized(_ shared.BootstrapExtensionHandle) { + sdk.Log(shared.LogLevelInfo, "combined module: server initialized") +} + +func (*combinedBootstrapExtension) OnWorkerThreadInitialized(_ shared.BootstrapExtensionHandle) { + sdk.Log(shared.LogLevelInfo, "combined module: worker thread initialized") +} + +func (*combinedBootstrapExtension) OnShutdown(_ shared.BootstrapExtensionHandle, completion func()) { + sdk.Log(shared.LogLevelInfo, "combined module: shutdown") + completion() +} + +// ------------------------------------------------------------------------------------- +// HTTP filter. +// ------------------------------------------------------------------------------------- + +type combinedHttpFilterConfigFactory struct { + shared.EmptyHttpFilterConfigFactory +} + +func (combinedHttpFilterConfigFactory) Create(_ shared.HttpFilterConfigHandle, _ []byte) (shared.HttpFilterFactory, error) { + // The Rust test logs this exact message; the C++ driver matches it via + // EXPECT_LOG_CONTAINS_ALL_OF. + sdk.Log(shared.LogLevelInfo, "http filter config created (function resolution deferred to request time)") + return &combinedHttpFilterFactory{}, nil +} + +type combinedHttpFilterFactory struct { + shared.EmptyHttpFilterFactory +} + +func (*combinedHttpFilterFactory) Create(handle shared.HttpFilterHandle) shared.HttpFilter { + return &combinedHttpFilter{handle: handle} +} + +type combinedHttpFilter struct { + shared.EmptyHttpFilter + handle shared.HttpFilterHandle +} + +func (p *combinedHttpFilter) OnRequestHeaders(headers shared.HeaderMap, _ bool) shared.HeadersStatus { + // Resolve the lookup function from the process-wide registry. The bootstrap init + // target gates listener traffic, so this MUST succeed by the time we get here. + ptr, ok := sdk.GetFunction(registryKey) + if !ok { + sdk.Log(shared.LogLevelError, "%s not found in function registry", registryKey) + p.handle.SendLocalResponse(503, + [][2]string{{"x-error-reason", "function_not_registered"}}, + []byte("routing function not registered"), + "function_not_registered") + return shared.HeadersStatusStop + } + lookup := *(*func(string) (string, bool))(ptr) + + // Read the target service header. If absent, pass through. + svcBuf := headers.GetOne("x-target-service") + if svcBuf.Len == 0 { + return shared.HeadersStatusContinue + } + svc := svcBuf.ToString() + + endpoint, found := lookup(svc) + if !found { + p.handle.SendLocalResponse(503, + [][2]string{{"x-error-reason", "service_not_onboarded"}}, + []byte("service '"+svc+"' is not onboarded"), + "service_not_onboarded") + return shared.HeadersStatusStop + } + + headers.Set("x-routed-to", endpoint) + sdk.Log(shared.LogLevelInfo, "routed service '%s' to endpoint '%s'", svc, endpoint) + return shared.HeadersStatusContinue +} diff --git a/test/extensions/dynamic_modules/test_data/go/bootstrap_init_target_test/bootstrap_init_target_test.go b/test/extensions/dynamic_modules/test_data/go/bootstrap_init_target_test/bootstrap_init_target_test.go new file mode 100644 index 0000000000000..20d0881fab06b --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/bootstrap_init_target_test/bootstrap_init_target_test.go @@ -0,0 +1,41 @@ +// Test module verifying that Envoy registers an init target for every bootstrap +// extension and unblocks server start once SignalInitComplete is called. Mirrors +// test_data/rust/bootstrap_init_target_test.rs. +package main + +import ( + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterBootstrapExtensionConfigFactories(map[string]shared.BootstrapExtensionConfigFactory{ + "test": &initTargetConfigFactory{}, + }) +} + +func main() {} + +type initTargetConfigFactory struct { + shared.EmptyBootstrapExtensionConfigFactory +} + +func (f *initTargetConfigFactory) Create(handle shared.BootstrapExtensionConfigHandle, _ []byte) (shared.BootstrapExtension, error) { + // Signal completion immediately — this test does not require asynchronous init. + handle.SignalInitComplete() + sdk.Log(shared.LogLevelInfo, "Init target signaled complete during config creation") + return &initTargetExtension{}, nil +} + +type initTargetExtension struct { + shared.EmptyBootstrapExtension +} + +func (*initTargetExtension) OnServerInitialized(_ shared.BootstrapExtensionHandle) { + sdk.Log(shared.LogLevelInfo, "Bootstrap init target test: server initialized after init target completed") +} + +func (*initTargetExtension) OnWorkerThreadInitialized(_ shared.BootstrapExtensionHandle) { + sdk.Log(shared.LogLevelInfo, "Bootstrap init target test completed successfully!") +} diff --git a/test/extensions/dynamic_modules/test_data/go/bootstrap_integration_test/bootstrap_integration_test.go b/test/extensions/dynamic_modules/test_data/go/bootstrap_integration_test/bootstrap_integration_test.go new file mode 100644 index 0000000000000..ce931acf503f2 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/bootstrap_integration_test/bootstrap_integration_test.go @@ -0,0 +1,54 @@ +// Test module for bootstrap extension lifecycle from the Go SDK. Mirrors the Rust +// bootstrap_integration_test.rs: emits log messages at each lifecycle hook so the C++ +// integration test driver can assert via EXPECT_LOG_CONTAINS that the dispatch path is +// wired up correctly. Specifically validates the shutdown completion fix from the code +// review (the trampoline previously discarded the completion callback, hanging Envoy +// teardown). +package main + +import ( + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterBootstrapExtensionConfigFactories(map[string]shared.BootstrapExtensionConfigFactory{ + "test": &basicConfigFactory{}, + }) +} + +func main() {} + +type basicConfigFactory struct { + shared.EmptyBootstrapExtensionConfigFactory +} + +func (f *basicConfigFactory) Create(handle shared.BootstrapExtensionConfigHandle, _ []byte) (shared.BootstrapExtension, error) { + // Signal init complete synchronously so Envoy can start accepting traffic. + handle.SignalInitComplete() + return &basicExtension{}, nil +} + +type basicExtension struct { + shared.EmptyBootstrapExtension +} + +func (*basicExtension) OnServerInitialized(_ shared.BootstrapExtensionHandle) { + sdk.Log(shared.LogLevelInfo, "Bootstrap extension server initialized from Go!") +} + +func (*basicExtension) OnWorkerThreadInitialized(_ shared.BootstrapExtensionHandle) { + sdk.Log(shared.LogLevelInfo, "Bootstrap extension worker thread initialized from Go!") +} + +func (*basicExtension) OnDrainStarted(_ shared.BootstrapExtensionHandle) { + sdk.Log(shared.LogLevelInfo, "Bootstrap extension drain started from Go!") +} + +func (*basicExtension) OnShutdown(_ shared.BootstrapExtensionHandle, completion func()) { + sdk.Log(shared.LogLevelInfo, "Bootstrap extension shutdown from Go!") + // MUST call completion exactly once so Envoy can finish teardown. The bug fix in + // abi/bootstrap.go ensures this actually reaches Envoy's event_cb. + completion() +} diff --git a/test/extensions/dynamic_modules/test_data/go/bootstrap_listener_lifecycle_test/bootstrap_listener_lifecycle_test.go b/test/extensions/dynamic_modules/test_data/go/bootstrap_listener_lifecycle_test/bootstrap_listener_lifecycle_test.go new file mode 100644 index 0000000000000..06dd437a3a62c --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/bootstrap_listener_lifecycle_test/bootstrap_listener_lifecycle_test.go @@ -0,0 +1,50 @@ +// Test module for the Bootstrap listener-lifecycle event API. Mirrors +// test_data/rust/bootstrap_listener_lifecycle_test.rs. Same shape as the cluster +// lifecycle test but for listener events. +package main + +import ( + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterBootstrapExtensionConfigFactories(map[string]shared.BootstrapExtensionConfigFactory{ + "test": &listenerLifecycleConfigFactory{}, + }) +} + +func main() {} + +type listenerLifecycleConfigFactory struct { + shared.EmptyBootstrapExtensionConfigFactory +} + +func (f *listenerLifecycleConfigFactory) Create(handle shared.BootstrapExtensionConfigHandle, _ []byte) (shared.BootstrapExtension, error) { + return &listenerLifecycleExtension{handle: handle}, nil +} + +type listenerLifecycleExtension struct { + shared.EmptyBootstrapExtension + handle shared.BootstrapExtensionConfigHandle +} + +func (e *listenerLifecycleExtension) OnServerInitialized(_ shared.BootstrapExtensionHandle) { + sdk.Log(shared.LogLevelInfo, "Bootstrap listener lifecycle test: server initialized") + scheduler := e.handle.NewScheduler() + scheduler.Schedule(func() { + enabled := e.handle.EnableListenerLifecycle() + sdk.Log(shared.LogLevelInfo, "Listener lifecycle enabled: %v", enabled) + e.handle.SignalInitComplete() + }) +} + +// BootstrapListenerLifecycleListener implementation. +func (*listenerLifecycleExtension) OnListenerAddOrUpdate(listenerName string) { + sdk.Log(shared.LogLevelInfo, "Listener added or updated: %s", listenerName) +} + +func (*listenerLifecycleExtension) OnListenerRemoval(listenerName string) { + sdk.Log(shared.LogLevelInfo, "Listener removed: %s", listenerName) +} diff --git a/test/extensions/dynamic_modules/test_data/go/bootstrap_shared_data_test/bootstrap_shared_data_test.go b/test/extensions/dynamic_modules/test_data/go/bootstrap_shared_data_test/bootstrap_shared_data_test.go new file mode 100644 index 0000000000000..3279d0cac889e --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/bootstrap_shared_data_test/bootstrap_shared_data_test.go @@ -0,0 +1,73 @@ +// Test module for the process-wide shared-data registry. Mirrors +// test_data/rust/bootstrap_shared_data_test.rs. +// +// Stores a sentinel pointer under a key, retrieves it back, overwrites it with a +// different value, retrieves again, and verifies non-existent key lookups return false. +// The Rust version uses static C uint64 storage; we use package-level Go uint64 vars, +// whose addresses are stable for the process lifetime since they're never dropped. +package main + +import ( + "unsafe" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterBootstrapExtensionConfigFactories(map[string]shared.BootstrapExtensionConfigFactory{ + "test": &sharedDataConfigFactory{}, + }) +} + +func main() {} + +type sharedDataConfigFactory struct { + shared.EmptyBootstrapExtensionConfigFactory +} + +func (f *sharedDataConfigFactory) Create(handle shared.BootstrapExtensionConfigHandle, _ []byte) (shared.BootstrapExtension, error) { + handle.SignalInitComplete() + return &sharedDataExtension{}, nil +} + +type sharedDataExtension struct { + shared.EmptyBootstrapExtension +} + +var ( + initialValue uint64 = 42 + updatedValue uint64 = 84 +) + +func (*sharedDataExtension) OnServerInitialized(_ shared.BootstrapExtensionHandle) { + const key = "test.shared.value" + + // First registration. The registry is process-wide; under parameterized test runs the + // key may already exist from a prior run, so ignore the bool — overwrites are allowed. + _ = sdk.RegisterSharedData(key, unsafe.Pointer(&initialValue)) + + if p, ok := sdk.GetSharedData(key); !ok { + panic("shared data should be found") + } else if got := *(*uint64)(p); got != 42 { + panic("first read returned unexpected value") + } + + // Overwrite with a different pointer. + if !sdk.RegisterSharedData(key, unsafe.Pointer(&updatedValue)) { + panic("overwrite should succeed") + } + + if p, ok := sdk.GetSharedData(key); !ok { + panic("shared data should still be found after overwrite") + } else if got := *(*uint64)(p); got != 84 { + panic("second read returned unexpected value") + } + + if _, ok := sdk.GetSharedData("no_such_data"); ok { + panic("non-existent key should return false") + } + + sdk.Log(shared.LogLevelInfo, "Bootstrap shared data registry test completed successfully!") +} diff --git a/test/extensions/dynamic_modules/test_data/go/bootstrap_stats_test/bootstrap_stats_test.go b/test/extensions/dynamic_modules/test_data/go/bootstrap_stats_test/bootstrap_stats_test.go new file mode 100644 index 0000000000000..d3f99ce8d62e3 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/bootstrap_stats_test/bootstrap_stats_test.go @@ -0,0 +1,157 @@ +// Test module for Bootstrap stats access + metrics definition. Mirrors +// test_data/rust/bootstrap_stats_test.rs. +// +// Two phases: +// 1. config_new — define unlabeled and labeled counters/gauges/histograms, mutate them, +// and emit log lines the C++ test asserts on. +// 2. on_server_initialized — exercise read-only stats access (get_counter_value / +// get_gauge_value / get_histogram_summary / iterate_*) including non-existent keys. +package main + +import ( + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterBootstrapExtensionConfigFactories(map[string]shared.BootstrapExtensionConfigFactory{ + "test": &statsConfigFactory{}, + }) +} + +func main() {} + +type statsConfigFactory struct { + shared.EmptyBootstrapExtensionConfigFactory +} + +func (f *statsConfigFactory) Create(handle shared.BootstrapExtensionConfigHandle, _ []byte) (shared.BootstrapExtension, error) { + // ---- Unlabeled metrics (no label names) ---- + counterID, res := handle.DefineCounter("refresh_success_total", nil) + if res != shared.MetricsSuccess { + panic("failed to define counter") + } + gaugeID, res := handle.DefineGauge("connection_state", nil) + if res != shared.MetricsSuccess { + panic("failed to define gauge") + } + histogramID, res := handle.DefineHistogram("refresh_duration_ms", nil) + if res != shared.MetricsSuccess { + panic("failed to define histogram") + } + + // Counter +3, +2 = 5. + if r := handle.IncrementCounter(counterID, nil, 3); r != shared.MetricsSuccess { + panic("failed to increment counter") + } + if r := handle.IncrementCounter(counterID, nil, 2); r != shared.MetricsSuccess { + panic("failed to increment counter") + } + sdk.Log(shared.LogLevelInfo, "Counter incremented to expected value of 5") + + // Gauge: set 100, +10, -30 = 80. + if r := handle.SetGauge(gaugeID, nil, 100); r != shared.MetricsSuccess { + panic("failed to set gauge") + } + if r := handle.IncrementGauge(gaugeID, nil, 10); r != shared.MetricsSuccess { + panic("failed to increment gauge") + } + if r := handle.DecrementGauge(gaugeID, nil, 30); r != shared.MetricsSuccess { + panic("failed to decrement gauge") + } + sdk.Log(shared.LogLevelInfo, "Gauge set to expected value of 80") + + // Histogram: record two values. + if r := handle.RecordHistogramValue(histogramID, nil, 42); r != shared.MetricsSuccess { + panic("failed to record histogram value") + } + if r := handle.RecordHistogramValue(histogramID, nil, 100); r != shared.MetricsSuccess { + panic("failed to record histogram value") + } + sdk.Log(shared.LogLevelInfo, "Histogram values recorded successfully") + + // ---- Labeled (vec) metrics ---- + counterVecID, res := handle.DefineCounter("request_total", []string{"method", "status"}) + if res != shared.MetricsSuccess { + panic("failed to define counter vec") + } + if r := handle.IncrementCounter(counterVecID, []string{"GET", "200"}, 7); r != shared.MetricsSuccess { + panic("failed to increment counter vec") + } + sdk.Log(shared.LogLevelInfo, "Counter vec incremented successfully") + + gaugeVecID, res := handle.DefineGauge("active_connections", []string{"upstream"}) + if res != shared.MetricsSuccess { + panic("failed to define gauge vec") + } + if r := handle.SetGauge(gaugeVecID, []string{"svc_a"}, 50); r != shared.MetricsSuccess { + panic("failed to set gauge vec") + } + if r := handle.IncrementGauge(gaugeVecID, []string{"svc_a"}, 5); r != shared.MetricsSuccess { + panic("failed to increment gauge vec") + } + if r := handle.DecrementGauge(gaugeVecID, []string{"svc_a"}, 10); r != shared.MetricsSuccess { + panic("failed to decrement gauge vec") + } + sdk.Log(shared.LogLevelInfo, "Gauge vec manipulated successfully") + + histogramVecID, res := handle.DefineHistogram("latency_ms", []string{"endpoint"}) + if res != shared.MetricsSuccess { + panic("failed to define histogram vec") + } + if r := handle.RecordHistogramValue(histogramVecID, []string{"backend_a"}, 15); r != shared.MetricsSuccess { + panic("failed to record histogram vec value") + } + sdk.Log(shared.LogLevelInfo, "Histogram vec recorded successfully") + + sdk.Log(shared.LogLevelInfo, "Bootstrap metrics definition and update test completed successfully!") + + handle.SignalInitComplete() + return &statsExtension{}, nil +} + +type statsExtension struct { + shared.EmptyBootstrapExtension +} + +func (*statsExtension) OnServerInitialized(handle shared.BootstrapExtensionHandle) { + // server.live should exist after init — we don't assert on its presence (some test + // configs may omit it) but we do log if found. + if value, ok := handle.GetGaugeValue("server.live"); ok { + sdk.Log(shared.LogLevelInfo, "Found server.live gauge with value: %d", value) + } else { + sdk.Log(shared.LogLevelInfo, "server.live gauge not found (this is expected in some test configs)") + } + + // Iterate counters and gauges to exercise the stats-iteration trampolines. + counterCount := 0 + handle.IterateCounters(func(_ shared.UnsafeEnvoyBuffer, _ uint64) shared.StatsIterationAction { + counterCount++ + return shared.StatsIterationActionContinue + }) + sdk.Log(shared.LogLevelInfo, "Found %d counters in stats store", counterCount) + + gaugeCount := 0 + handle.IterateGauges(func(_ shared.UnsafeEnvoyBuffer, _ uint64) shared.StatsIterationAction { + gaugeCount++ + return shared.StatsIterationActionContinue + }) + sdk.Log(shared.LogLevelInfo, "Found %d gauges in stats store", gaugeCount) + + if _, ok := handle.GetCounterValue("non.existent.counter"); !ok { + sdk.Log(shared.LogLevelInfo, "Correctly returned None for non-existent counter") + } + if _, ok := handle.GetGaugeValue("non.existent.gauge"); !ok { + sdk.Log(shared.LogLevelInfo, "Correctly returned None for non-existent gauge") + } + if _, _, ok := handle.GetHistogramSummary("non.existent.histogram"); !ok { + sdk.Log(shared.LogLevelInfo, "Correctly returned None for non-existent histogram") + } + + sdk.Log(shared.LogLevelInfo, "Bootstrap stats access test completed successfully!") +} + +func (*statsExtension) OnWorkerThreadInitialized(_ shared.BootstrapExtensionHandle) { + sdk.Log(shared.LogLevelInfo, "Bootstrap extension worker thread initialized with stats access!") +} diff --git a/test/extensions/dynamic_modules/test_data/go/bootstrap_timer_test/bootstrap_timer_test.go b/test/extensions/dynamic_modules/test_data/go/bootstrap_timer_test/bootstrap_timer_test.go new file mode 100644 index 0000000000000..622c9ecacc4b7 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/bootstrap_timer_test/bootstrap_timer_test.go @@ -0,0 +1,108 @@ +// Test module for the Bootstrap timer API. Mirrors test_data/rust/bootstrap_timer_test.rs. +// +// Creates two timers during config_new with different per-timer callbacks, arms them with +// short delays, and signals init-complete only after both have fired. The Go SDK uses +// per-timer onFire callbacks (rather than a centralized on_timer_fired hook) so timer +// identity is captured in the closure. +package main + +import ( + "sync" + "sync/atomic" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterBootstrapExtensionConfigFactories(map[string]shared.BootstrapExtensionConfigFactory{ + "test": &timerConfigFactory{}, + }) +} + +func main() {} + +type timerConfigFactory struct { + shared.EmptyBootstrapExtensionConfigFactory +} + +func (f *timerConfigFactory) Create(handle shared.BootstrapExtensionConfigHandle, _ []byte) (shared.BootstrapExtension, error) { + state := &timerTestState{handle: handle} + + timerA := handle.NewTimer(state.onTimerAFired) + timerB := handle.NewTimer(state.onTimerBFired) + + if timerA == nil || timerB == nil { + // If either timer can't be created, fall back to signaling init so the test fails + // loudly via the absence of the expected log lines rather than hanging. + handle.SignalInitComplete() + return &shared.EmptyBootstrapExtension{}, nil + } + + if timerA.Enabled() { + panic("Timer A should not be enabled upon creation") + } + if timerB.Enabled() { + panic("Timer B should not be enabled upon creation") + } + + state.mu.Lock() + state.timerA = timerA + state.timerB = timerB + state.mu.Unlock() + + timerA.Enable(10) + timerB.Enable(20) + + sdk.Log(shared.LogLevelInfo, "Two timers created and armed during config_new") + + // Do NOT signal init here — wait until both timers have fired so the test proves the + // timer-fired dispatch actually reaches our callbacks. + return &shared.EmptyBootstrapExtension{}, nil +} + +type timerTestState struct { + handle shared.BootstrapExtensionConfigHandle + mu sync.Mutex + timerA shared.BootstrapTimer + timerB shared.BootstrapTimer + aFired atomic.Bool + bFired atomic.Bool + doneSet atomic.Bool +} + +func (s *timerTestState) onTimerAFired(_ shared.BootstrapTimer) { + sdk.Log(shared.LogLevelInfo, "Timer A fired, identified by callback") + s.aFired.Store(true) + s.mu.Lock() + if s.timerA != nil { + s.timerA.Delete() + s.timerA = nil + } + s.mu.Unlock() + s.maybeSignalInitComplete() +} + +func (s *timerTestState) onTimerBFired(_ shared.BootstrapTimer) { + sdk.Log(shared.LogLevelInfo, "Timer B fired, identified by callback") + s.bFired.Store(true) + s.mu.Lock() + if s.timerB != nil { + s.timerB.Delete() + s.timerB = nil + } + s.mu.Unlock() + s.maybeSignalInitComplete() +} + +func (s *timerTestState) maybeSignalInitComplete() { + if !s.aFired.Load() || !s.bFired.Load() { + return + } + if s.doneSet.Swap(true) { + return + } + s.handle.SignalInitComplete() + sdk.Log(shared.LogLevelInfo, "Bootstrap timer test completed successfully!") +} diff --git a/test/extensions/dynamic_modules/test_data/go/cert_validator_test/cert_validator_test.go b/test/extensions/dynamic_modules/test_data/go/cert_validator_test/cert_validator_test.go new file mode 100644 index 0000000000000..97306dc36d225 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/cert_validator_test/cert_validator_test.go @@ -0,0 +1,31 @@ +// Cert validator test module. +// +// Registers a "test" cert validator that always returns Successful. Loaded by the +// dynamic_modules_cert_validator_test under test/extensions/transport_sockets/tls/cert_validator/dynamic_modules. +package main + +import ( + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterCertValidatorConfigFactories(map[string]shared.CertValidatorConfigFactory{ + "test": &noOpConfigFactory{}, + }) +} + +func main() {} //nolint:all + +type noOpConfigFactory struct { + shared.EmptyCertValidatorConfigFactory +} + +func (noOpConfigFactory) Create(_ string, _ []byte) (shared.CertValidator, error) { + return &noOpValidator{}, nil +} + +type noOpValidator struct { + shared.EmptyCertValidator +} diff --git a/test/extensions/dynamic_modules/test_data/go/cluster_integration_test/cluster_integration_test.go b/test/extensions/dynamic_modules/test_data/go/cluster_integration_test/cluster_integration_test.go new file mode 100644 index 0000000000000..078c416834b9d --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/cluster_integration_test/cluster_integration_test.go @@ -0,0 +1,353 @@ +// Test module for the Cluster SDK surface from Go. Mirrors test_data/rust/cluster_integration_test.rs. +// +// Validates the bug fixes that came out of the code review: +// - Shutdown completion callback actually reaches Envoy (was silently dropped before — would +// hang Envoy teardown). +// - Async host selection's Complete() does not leak the wrapper into the global manager. +// - User-supplied ClusterAsyncHostSelection.Cancel is invoked on cancellation. +// - destroyed flag short-circuits late callbacks (scheduler events, host-membership updates, +// callouts that fire while OnDestroy is running). +// +// Each scenario is exposed as a named cluster type the C++ integration test driver can +// instantiate by name. +package main + +import ( + "strings" + "sync" + "sync/atomic" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterClusterConfigFactories(map[string]shared.ClusterConfigFactory{ + "sync_host_selection": &syncHostSelectionConfigFactory{}, + "async_host_selection": &asyncHostSelectionConfigFactory{}, + "scheduler_host_update": &schedulerHostUpdateConfigFactory{}, + "lifecycle_callbacks": &lifecycleCallbacksConfigFactory{}, + }) +} + +func main() {} + +// hostList wraps a slice of ClusterHost so a mutex can guard updates from multiple callbacks. +type hostList struct { + mu sync.Mutex + hosts []shared.ClusterHost +} + +func (l *hostList) set(hosts []shared.ClusterHost) { + l.mu.Lock() + l.hosts = hosts + l.mu.Unlock() +} + +func (l *hostList) get() []shared.ClusterHost { + l.mu.Lock() + defer l.mu.Unlock() + if len(l.hosts) == 0 { + return nil + } + out := make([]shared.ClusterHost, len(l.hosts)) + copy(out, l.hosts) + return out +} + +// addressFromConfig extracts the upstream address from the test config bytes (the C++ driver +// passes the upstream address verbatim as the config string). +func addressFromConfig(config []byte) string { + return strings.TrimSpace(string(config)) +} + +// ============================================================================= +// Synchronous host selection. +// ============================================================================= + +type syncHostSelectionConfigFactory struct { + shared.EmptyClusterConfigFactory +} + +func (f *syncHostSelectionConfigFactory) Create(handle shared.ClusterConfigHandle, config []byte) (shared.ClusterFactory, error) { + counterID, _ := handle.DefineCounter("requests_routed", nil) + return &syncHostSelectionFactory{ + address: addressFromConfig(config), + counterID: counterID, + handle: handle, + }, nil +} + +type syncHostSelectionFactory struct { + shared.EmptyClusterFactory + address string + counterID shared.MetricID + handle shared.ClusterConfigHandle +} + +func (f *syncHostSelectionFactory) Create(_ shared.ClusterConfigHandle) shared.Cluster { + return &syncHostSelectionCluster{ + address: f.address, + hosts: &hostList{}, + counterID: f.counterID, + handle: f.handle, + } +} + +type syncHostSelectionCluster struct { + shared.EmptyCluster + address string + hosts *hostList + counterID shared.MetricID + handle shared.ClusterConfigHandle +} + +func (c *syncHostSelectionCluster) OnInit(handle shared.ClusterHandle) { + specs := []shared.ClusterHostSpec{{Address: c.address, Weight: 1}} + if hosts, ok := handle.AddHosts(0, specs); ok { + c.hosts.set(hosts) + } + handle.PreInitComplete() +} + +func (c *syncHostSelectionCluster) NewLoadBalancer(_ shared.ClusterLoadBalancerHandle) shared.ClusterLoadBalancer { + return &syncHostSelectionLb{ + hosts: c.hosts, + counterID: c.counterID, + handle: c.handle, + } +} + +type syncHostSelectionLb struct { + shared.EmptyClusterLoadBalancer + hosts *hostList + counterID shared.MetricID + index atomic.Uint64 + handle shared.ClusterConfigHandle +} + +func (lb *syncHostSelectionLb) ChooseHost( + _ shared.ClusterLoadBalancerHandle, _ shared.ClusterLoadBalancerContext, _ shared.ClusterAsyncCompletion, +) (shared.ClusterHost, shared.ClusterAsyncHostSelection, bool) { + hosts := lb.hosts.get() + if len(hosts) == 0 { + return shared.ClusterHost{}, nil, false + } + idx := lb.index.Add(1) - 1 + host := hosts[idx%uint64(len(hosts))] + if lb.counterID != 0 { + lb.handle.IncrementCounter(lb.counterID, nil, 1) + } + return host, nil, true +} + +// ============================================================================= +// Asynchronous host selection via background goroutine. +// ============================================================================= + +type asyncHostSelectionConfigFactory struct { + shared.EmptyClusterConfigFactory +} + +func (f *asyncHostSelectionConfigFactory) Create(_ shared.ClusterConfigHandle, config []byte) (shared.ClusterFactory, error) { + return &asyncHostSelectionFactory{address: addressFromConfig(config)}, nil +} + +type asyncHostSelectionFactory struct { + shared.EmptyClusterFactory + address string +} + +func (f *asyncHostSelectionFactory) Create(_ shared.ClusterConfigHandle) shared.Cluster { + return &asyncHostSelectionCluster{address: f.address, hosts: &hostList{}} +} + +type asyncHostSelectionCluster struct { + shared.EmptyCluster + address string + hosts *hostList +} + +func (c *asyncHostSelectionCluster) OnInit(handle shared.ClusterHandle) { + if hosts, ok := handle.AddHosts(0, []shared.ClusterHostSpec{{Address: c.address, Weight: 1}}); ok { + c.hosts.set(hosts) + } + handle.PreInitComplete() +} + +func (c *asyncHostSelectionCluster) NewLoadBalancer(_ shared.ClusterLoadBalancerHandle) shared.ClusterLoadBalancer { + return &asyncHostSelectionLb{hosts: c.hosts} +} + +type asyncHostSelectionLb struct { + shared.EmptyClusterLoadBalancer + hosts *hostList +} + +func (lb *asyncHostSelectionLb) ChooseHost( + _ shared.ClusterLoadBalancerHandle, _ shared.ClusterLoadBalancerContext, completion shared.ClusterAsyncCompletion, +) (shared.ClusterHost, shared.ClusterAsyncHostSelection, bool) { + hosts := lb.hosts.get() + if len(hosts) == 0 { + return shared.ClusterHost{}, nil, false + } + host := hosts[0] + cancelled := &atomic.Bool{} + // Resolve in a background goroutine. The SDK posts the completion to the correct worker + // thread via Envoy's async-host-selection callback; modules don't have to worry about + // thread affinity here. + go func() { + if cancelled.Load() { + return + } + completion.Complete(host, "async_resolved") + }() + return shared.ClusterHost{}, &asyncHandle{cancelled: cancelled}, true +} + +type asyncHandle struct { + cancelled *atomic.Bool +} + +func (h *asyncHandle) Cancel() { h.cancelled.Store(true) } + +// ============================================================================= +// Scheduler-based host updates. +// ============================================================================= + +const addHostEventID uint64 = 100 + +type schedulerHostUpdateConfigFactory struct { + shared.EmptyClusterConfigFactory +} + +func (f *schedulerHostUpdateConfigFactory) Create(_ shared.ClusterConfigHandle, config []byte) (shared.ClusterFactory, error) { + return &schedulerHostUpdateFactory{address: addressFromConfig(config)}, nil +} + +type schedulerHostUpdateFactory struct { + shared.EmptyClusterFactory + address string +} + +func (f *schedulerHostUpdateFactory) Create(_ shared.ClusterConfigHandle) shared.Cluster { + return &schedulerHostUpdateCluster{address: f.address, hosts: &hostList{}} +} + +type schedulerHostUpdateCluster struct { + shared.EmptyCluster + address string + hosts *hostList + handle shared.ClusterHandle +} + +func (c *schedulerHostUpdateCluster) OnInit(handle shared.ClusterHandle) { + c.handle = handle + handle.PreInitComplete() + // Schedule a deferred host-add to exercise the scheduler dispatch path. The Go SDK + // translates this into an Envoy main-thread event. + scheduler := handle.NewScheduler() + scheduler.Schedule(func() { + hosts, ok := handle.AddHosts(0, []shared.ClusterHostSpec{{Address: c.address, Weight: 1}}) + if ok { + c.hosts.set(hosts) + } + }) +} + +func (c *schedulerHostUpdateCluster) NewLoadBalancer(_ shared.ClusterLoadBalancerHandle) shared.ClusterLoadBalancer { + return &schedulerHostUpdateLb{hosts: c.hosts} +} + +type schedulerHostUpdateLb struct { + shared.EmptyClusterLoadBalancer + hosts *hostList + membershipUpdateCount atomic.Uint64 +} + +func (lb *schedulerHostUpdateLb) ChooseHost( + _ shared.ClusterLoadBalancerHandle, _ shared.ClusterLoadBalancerContext, _ shared.ClusterAsyncCompletion, +) (shared.ClusterHost, shared.ClusterAsyncHostSelection, bool) { + hosts := lb.hosts.get() + if len(hosts) == 0 { + return shared.ClusterHost{}, nil, false + } + return hosts[0], nil, true +} + +func (lb *schedulerHostUpdateLb) OnHostMembershipUpdate(_ shared.ClusterLoadBalancerHandle, _, _ uint64) { + lb.membershipUpdateCount.Add(1) +} + +// ============================================================================= +// Lifecycle callbacks (server_initialized / drain_started / shutdown). +// ============================================================================= +// +// The shutdown branch is the most important one — it locks in the bug fix where the +// completion callback was being silently dropped at the C trampoline layer. If the bug +// regresses, server teardown will hang waiting for an event_cb that never arrives. + +type lifecycleCallbacksConfigFactory struct { + shared.EmptyClusterConfigFactory +} + +func (f *lifecycleCallbacksConfigFactory) Create(_ shared.ClusterConfigHandle, config []byte) (shared.ClusterFactory, error) { + return &lifecycleCallbacksFactory{address: addressFromConfig(config)}, nil +} + +type lifecycleCallbacksFactory struct { + shared.EmptyClusterFactory + address string +} + +func (f *lifecycleCallbacksFactory) Create(_ shared.ClusterConfigHandle) shared.Cluster { + return &lifecycleCallbacksCluster{address: f.address, hosts: &hostList{}} +} + +type lifecycleCallbacksCluster struct { + shared.EmptyCluster + address string + hosts *hostList +} + +func (c *lifecycleCallbacksCluster) OnInit(handle shared.ClusterHandle) { + sdk.Log(shared.LogLevelInfo, "cluster lifecycle: on_init called") + if hosts, ok := handle.AddHosts(0, []shared.ClusterHostSpec{{Address: c.address, Weight: 1}}); ok { + c.hosts.set(hosts) + } + handle.PreInitComplete() +} + +func (c *lifecycleCallbacksCluster) NewLoadBalancer(_ shared.ClusterLoadBalancerHandle) shared.ClusterLoadBalancer { + return &lifecycleCallbacksLb{hosts: c.hosts} +} + +func (c *lifecycleCallbacksCluster) OnServerInitialized(_ shared.ClusterHandle) { + sdk.Log(shared.LogLevelInfo, "cluster lifecycle: on_server_initialized called") +} + +func (c *lifecycleCallbacksCluster) OnDrainStarted(_ shared.ClusterHandle) { + sdk.Log(shared.LogLevelInfo, "cluster lifecycle: on_drain_started called") +} + +func (c *lifecycleCallbacksCluster) OnShutdown(_ shared.ClusterHandle, completion func()) { + sdk.Log(shared.LogLevelInfo, "cluster lifecycle: on_shutdown called") + // MUST call completion exactly once; if the trampoline regresses, Envoy hangs. + completion() +} + +type lifecycleCallbacksLb struct { + shared.EmptyClusterLoadBalancer + hosts *hostList +} + +func (lb *lifecycleCallbacksLb) ChooseHost( + _ shared.ClusterLoadBalancerHandle, _ shared.ClusterLoadBalancerContext, _ shared.ClusterAsyncCompletion, +) (shared.ClusterHost, shared.ClusterAsyncHostSelection, bool) { + hosts := lb.hosts.get() + if len(hosts) == 0 { + return shared.ClusterHost{}, nil, false + } + return hosts[0], nil, true +} diff --git a/test/extensions/dynamic_modules/test_data/go/http_integration_test/http_integration_test.go b/test/extensions/dynamic_modules/test_data/go/http_integration_test/http_integration_test.go index 473934a85c4be..58d13315c890e 100644 --- a/test/extensions/dynamic_modules/test_data/go/http_integration_test/http_integration_test.go +++ b/test/extensions/dynamic_modules/test_data/go/http_integration_test/http_integration_test.go @@ -37,6 +37,9 @@ func init() { "http_config_stream": &HttpConfigStreamConfigFactory{}, "http_struct_config": &HttpStructConfigFactory{}, "list_metadata_callbacks": &ListMetadataCallbacksConfigFactory{}, + "dynamic_metadata": &DynamicMetadataConfigFactory{}, + "filter_state_writer": &FilterStateWriterConfigFactory{}, + "filter_state_reader": &FilterStateReaderConfigFactory{}, }) } @@ -1471,3 +1474,157 @@ func (f *ListMetadataCallbacksFilter) OnResponseHeaders(headers shared.HeaderMap return shared.HeadersStatusContinue } + +// ----------------------------------------------------------------------------- +// DynamicMetadata: scalar Get/Set on dynamic metadata. +// +// Reads route metadata (configured by the test driver) and writes it back to dynamic +// metadata. Then writes summary headers so the test can assert via response headers +// that the read+write cycle preserved values. +// ----------------------------------------------------------------------------- + +type DynamicMetadataConfigFactory struct { + shared.EmptyHttpFilterConfigFactory +} + +func (DynamicMetadataConfigFactory) Create(_ shared.HttpFilterConfigHandle, _ []byte) (shared.HttpFilterFactory, error) { + return &DynamicMetadataFilterFactory{}, nil +} + +type DynamicMetadataFilterFactory struct { + shared.EmptyHttpFilterFactory +} + +func (*DynamicMetadataFilterFactory) Create(handle shared.HttpFilterHandle) shared.HttpFilter { + return &DynamicMetadataFilter{handle: handle} +} + +type DynamicMetadataFilter struct { + shared.EmptyHttpFilter + handle shared.HttpFilterHandle +} + +func (f *DynamicMetadataFilter) OnRequestHeaders(_ shared.HeaderMap, _ bool) shared.HeadersStatus { + // Read three scalar values from route metadata (set by the test driver in the + // route config). Then write the same values into dynamic metadata under a + // different namespace so we can read them back on the response side. + if v, ok := f.handle.GetMetadataString(shared.MetadataSourceTypeRoute, "test_ns", "string_key"); ok { + f.handle.SetMetadata("dm_test", "string_key", v.ToString()) + } + if v, ok := f.handle.GetMetadataNumber(shared.MetadataSourceTypeRoute, "test_ns", "number_key"); ok { + f.handle.SetMetadata("dm_test", "number_key", v) + } + if v, ok := f.handle.GetMetadataBool(shared.MetadataSourceTypeRoute, "test_ns", "bool_key"); ok { + f.handle.SetMetadata("dm_test", "bool_key", v) + } + return shared.HeadersStatusContinue +} + +func (f *DynamicMetadataFilter) OnResponseHeaders(headers shared.HeaderMap, _ bool) shared.HeadersStatus { + // Read back from dynamic metadata and surface via response headers. + if v, ok := f.handle.GetMetadataString(shared.MetadataSourceTypeDynamic, "dm_test", "string_key"); ok { + headers.Set("x-dm-string", v.ToString()) + } + if v, ok := f.handle.GetMetadataNumber(shared.MetadataSourceTypeDynamic, "dm_test", "number_key"); ok { + headers.Set("x-dm-number", strconv.FormatFloat(v, 'f', -1, 64)) + } + if v, ok := f.handle.GetMetadataBool(shared.MetadataSourceTypeDynamic, "dm_test", "bool_key"); ok { + if v { + headers.Set("x-dm-bool", "true") + } else { + headers.Set("x-dm-bool", "false") + } + } + + // Surface the keys the SDK reports for our namespace, so we can verify + // GetMetadataKeys works. + keys := f.handle.GetMetadataKeys(shared.MetadataSourceTypeDynamic, "dm_test") + headers.Set("x-dm-key-count", strconv.Itoa(len(keys))) + + return shared.HeadersStatusContinue +} + +// ----------------------------------------------------------------------------- +// FilterState: round-trip across two filters. +// +// filter_state_writer sets a filter-state value on the request side. +// filter_state_reader reads the value on the request side and surfaces it via +// response headers. The test wires both filters in order and asserts the reader +// observed what the writer set. +// ----------------------------------------------------------------------------- + +type FilterStateWriterConfigFactory struct { + shared.EmptyHttpFilterConfigFactory +} + +func (FilterStateWriterConfigFactory) Create(_ shared.HttpFilterConfigHandle, config []byte) (shared.HttpFilterFactory, error) { + // config bytes are the value to write. Empty config = "default_value". + value := string(config) + if value == "" { + value = "default_value" + } + return &FilterStateWriterFilterFactory{value: value}, nil +} + +type FilterStateWriterFilterFactory struct { + shared.EmptyHttpFilterFactory + value string +} + +func (f *FilterStateWriterFilterFactory) Create(handle shared.HttpFilterHandle) shared.HttpFilter { + return &FilterStateWriterFilter{handle: handle, value: f.value} +} + +type FilterStateWriterFilter struct { + shared.EmptyHttpFilter + handle shared.HttpFilterHandle + value string +} + +func (f *FilterStateWriterFilter) OnRequestHeaders(_ shared.HeaderMap, _ bool) shared.HeadersStatus { + f.handle.SetFilterState("test_filter_state_key", []byte(f.value)) + return shared.HeadersStatusContinue +} + +type FilterStateReaderConfigFactory struct { + shared.EmptyHttpFilterConfigFactory +} + +func (FilterStateReaderConfigFactory) Create(_ shared.HttpFilterConfigHandle, _ []byte) (shared.HttpFilterFactory, error) { + return &FilterStateReaderFilterFactory{}, nil +} + +type FilterStateReaderFilterFactory struct { + shared.EmptyHttpFilterFactory +} + +func (*FilterStateReaderFilterFactory) Create(handle shared.HttpFilterHandle) shared.HttpFilter { + return &FilterStateReaderFilter{handle: handle} +} + +type FilterStateReaderFilter struct { + shared.EmptyHttpFilter + handle shared.HttpFilterHandle + // Captured on the request side and re-emitted on the response side because + // filter state may not be preserved exactly the same way for the response + // callbacks of the same stream. + captured string + hasValue bool +} + +func (f *FilterStateReaderFilter) OnRequestHeaders(_ shared.HeaderMap, _ bool) shared.HeadersStatus { + if v, ok := f.handle.GetFilterState("test_filter_state_key"); ok { + f.captured = v.ToString() + f.hasValue = true + } + return shared.HeadersStatusContinue +} + +func (f *FilterStateReaderFilter) OnResponseHeaders(headers shared.HeaderMap, _ bool) shared.HeadersStatus { + if f.hasValue { + headers.Set("x-filter-state-value", f.captured) + } else { + headers.Set("x-filter-state-value", "") + } + return shared.HeadersStatusContinue +} diff --git a/test/extensions/dynamic_modules/test_data/go/http_stream_callouts_test/http_stream_callouts_test.go b/test/extensions/dynamic_modules/test_data/go/http_stream_callouts_test/http_stream_callouts_test.go new file mode 100644 index 0000000000000..52af57164e019 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/http_stream_callouts_test/http_stream_callouts_test.go @@ -0,0 +1,351 @@ +// HTTP stream callouts test module. Mirrors test_data/rust/http_stream_callouts_test.rs. +// +// Exercises the StartHttpStream / SendHttpStreamData / SendHttpStreamTrailers / +// ResetHttpStream API family across five filter shapes: +// +// basic_stream_lifecycle - start, receive headers/data, complete +// bidirectional_streaming - send chunks + trailers, count received chunks +// multiple_streams - 3 concurrent streams +// stream_reset - reset on first headers callback +// upstream_reset - rely on upstream to reset +// +// This module is built but currently has no integration driver — its purpose is to +// exercise the SDK API surface at compile time, paralleling the Rust module of the +// same name. +package main + +import ( + "fmt" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterHttpFilterConfigFactories(map[string]shared.HttpFilterConfigFactory{ + "basic_stream_lifecycle": &basicStreamLifecycleFactory{}, + "bidirectional_streaming": &bidirectionalStreamingFactory{}, + "multiple_streams": &multipleStreamsFactory{}, + "stream_reset": &streamResetFactory{}, + "upstream_reset": &upstreamResetFactory{}, + }) +} + +func main() {} //nolint:all + +// ============================================================================= +// Test 1: Basic Stream Lifecycle +// ============================================================================= + +type basicStreamLifecycleFactory struct { + shared.EmptyHttpFilterConfigFactory +} + +func (basicStreamLifecycleFactory) Create(_ shared.HttpFilterConfigHandle, c []byte) (shared.HttpFilterFactory, error) { + return &basicStreamLifecycleFilterFactory{cluster: string(c)}, nil +} + +type basicStreamLifecycleFilterFactory struct { + shared.EmptyHttpFilterFactory + cluster string +} + +func (f *basicStreamLifecycleFilterFactory) Create(h shared.HttpFilterHandle) shared.HttpFilter { + return &basicStreamLifecycleFilter{handle: h, cluster: f.cluster} +} + +type basicStreamLifecycleFilter struct { + shared.EmptyHttpFilter + handle shared.HttpFilterHandle + cluster string + receivedResponse bool +} + +func (p *basicStreamLifecycleFilter) OnRequestHeaders(_ shared.HeaderMap, _ bool) shared.HeadersStatus { + res, _ := p.handle.StartHttpStream(p.cluster, + [][2]string{{":path", "/test"}, {":method", "GET"}, {"host", "example.com"}}, + nil, true, 5000, p) + if res != shared.HttpCalloutInitSuccess { + p.handle.SendLocalResponse(500, [][2]string{{"x-error", "stream_init_failed"}}, nil, "") + return shared.HeadersStatusStop + } + return shared.HeadersStatusStop +} + +func (p *basicStreamLifecycleFilter) OnHttpStreamHeaders(_ uint64, _ [][2]shared.UnsafeEnvoyBuffer, _ bool) { +} +func (p *basicStreamLifecycleFilter) OnHttpStreamData(_ uint64, _ []shared.UnsafeEnvoyBuffer, _ bool) { +} +func (p *basicStreamLifecycleFilter) OnHttpStreamTrailers(_ uint64, _ [][2]shared.UnsafeEnvoyBuffer) { +} +func (p *basicStreamLifecycleFilter) OnHttpStreamComplete(_ uint64) { + p.receivedResponse = true + p.handle.SendLocalResponse(200, + [][2]string{{"x-stream", "success"}}, + []byte("stream_callout_success"), "") +} +func (p *basicStreamLifecycleFilter) OnHttpStreamReset(_ uint64, _ shared.HttpStreamResetReason) { +} + +// ============================================================================= +// Test 2: Bidirectional Streaming +// ============================================================================= + +type bidirectionalStreamingFactory struct { + shared.EmptyHttpFilterConfigFactory +} + +func (bidirectionalStreamingFactory) Create(_ shared.HttpFilterConfigHandle, c []byte) (shared.HttpFilterFactory, error) { + return &bidirectionalStreamingFilterFactory{cluster: string(c)}, nil +} + +type bidirectionalStreamingFilterFactory struct { + shared.EmptyHttpFilterFactory + cluster string +} + +func (f *bidirectionalStreamingFilterFactory) Create(h shared.HttpFilterHandle) shared.HttpFilter { + return &bidirectionalStreamingFilter{handle: h, cluster: f.cluster} +} + +type bidirectionalStreamingFilter struct { + shared.EmptyHttpFilter + handle shared.HttpFilterHandle + cluster string + streamHandle uint64 + chunksReceived int +} + +func (p *bidirectionalStreamingFilter) OnRequestHeaders(_ shared.HeaderMap, _ bool) shared.HeadersStatus { + res, id := p.handle.StartHttpStream(p.cluster, + [][2]string{ + {":path", "/stream"}, + {":method", "POST"}, + {"host", "example.com"}, + {"content-type", "application/octet-stream"}, + }, + nil, false, 10000, p) + if res != shared.HttpCalloutInitSuccess { + p.handle.SendLocalResponse(500, [][2]string{{"x-error", "stream_init_failed"}}, nil, "") + return shared.HeadersStatusStop + } + p.streamHandle = id + + // Send chunks + trailers. + if !p.handle.SendHttpStreamData(id, []byte("chunk1"), false) { + p.handle.SendLocalResponse(500, [][2]string{{"x-error", "send_chunk1"}}, nil, "") + return shared.HeadersStatusStop + } + if !p.handle.SendHttpStreamData(id, []byte("chunk2"), false) { + p.handle.SendLocalResponse(500, [][2]string{{"x-error", "send_chunk2"}}, nil, "") + return shared.HeadersStatusStop + } + if !p.handle.SendHttpStreamTrailers(id, [][2]string{{"x-trailer", "value"}}) { + p.handle.SendLocalResponse(500, [][2]string{{"x-error", "send_trailers"}}, nil, "") + return shared.HeadersStatusStop + } + + return shared.HeadersStatusStop +} + +func (p *bidirectionalStreamingFilter) OnHttpStreamHeaders(_ uint64, _ [][2]shared.UnsafeEnvoyBuffer, _ bool) { +} +func (p *bidirectionalStreamingFilter) OnHttpStreamData(id uint64, _ []shared.UnsafeEnvoyBuffer, _ bool) { + if id == p.streamHandle { + p.chunksReceived++ + } +} +func (p *bidirectionalStreamingFilter) OnHttpStreamTrailers(_ uint64, _ [][2]shared.UnsafeEnvoyBuffer) { +} +func (p *bidirectionalStreamingFilter) OnHttpStreamComplete(id uint64) { + if id != p.streamHandle { + return + } + p.handle.SendLocalResponse(200, + [][2]string{{"x-chunks-received", fmt.Sprintf("%d", p.chunksReceived)}}, + []byte("bidirectional_success"), "") +} +func (p *bidirectionalStreamingFilter) OnHttpStreamReset(_ uint64, _ shared.HttpStreamResetReason) { +} + +// ============================================================================= +// Test 3: Multiple Streams +// ============================================================================= + +type multipleStreamsFactory struct { + shared.EmptyHttpFilterConfigFactory +} + +func (multipleStreamsFactory) Create(_ shared.HttpFilterConfigHandle, c []byte) (shared.HttpFilterFactory, error) { + return &multipleStreamsFilterFactory{cluster: string(c)}, nil +} + +type multipleStreamsFilterFactory struct { + shared.EmptyHttpFilterFactory + cluster string +} + +func (f *multipleStreamsFilterFactory) Create(h shared.HttpFilterHandle) shared.HttpFilter { + return &multipleStreamsFilter{handle: h, cluster: f.cluster} +} + +type multipleStreamsFilter struct { + shared.EmptyHttpFilter + handle shared.HttpFilterHandle + cluster string + streamHandles []uint64 + completedStreams int +} + +func (p *multipleStreamsFilter) OnRequestHeaders(_ shared.HeaderMap, _ bool) shared.HeadersStatus { + for i := 1; i <= 3; i++ { + path := fmt.Sprintf("/stream%d", i) + res, id := p.handle.StartHttpStream(p.cluster, + [][2]string{{":path", path}, {":method", "GET"}, {"host", "example.com"}}, + nil, true, 5000, p) + if res == shared.HttpCalloutInitSuccess { + p.streamHandles = append(p.streamHandles, id) + } + } + if len(p.streamHandles) != 3 { + p.handle.SendLocalResponse(500, [][2]string{{"x-error", "stream_init_failed"}}, nil, "") + } + return shared.HeadersStatusStop +} + +func (p *multipleStreamsFilter) OnHttpStreamHeaders(_ uint64, _ [][2]shared.UnsafeEnvoyBuffer, _ bool) { +} +func (p *multipleStreamsFilter) OnHttpStreamData(_ uint64, _ []shared.UnsafeEnvoyBuffer, _ bool) { +} +func (p *multipleStreamsFilter) OnHttpStreamTrailers(_ uint64, _ [][2]shared.UnsafeEnvoyBuffer) { +} +func (p *multipleStreamsFilter) OnHttpStreamComplete(id uint64) { + for _, h := range p.streamHandles { + if h == id { + p.completedStreams++ + break + } + } + if p.completedStreams == 3 { + p.handle.SendLocalResponse(200, [][2]string{{"x-stream", "all_success"}}, nil, "") + } +} +func (p *multipleStreamsFilter) OnHttpStreamReset(_ uint64, _ shared.HttpStreamResetReason) {} + +// ============================================================================= +// Test 4: Stream Reset +// ============================================================================= + +type streamResetFactory struct { + shared.EmptyHttpFilterConfigFactory +} + +func (streamResetFactory) Create(_ shared.HttpFilterConfigHandle, c []byte) (shared.HttpFilterFactory, error) { + return &streamResetFilterFactory{cluster: string(c)}, nil +} + +type streamResetFilterFactory struct { + shared.EmptyHttpFilterFactory + cluster string +} + +func (f *streamResetFilterFactory) Create(h shared.HttpFilterHandle) shared.HttpFilter { + return &streamResetFilter{handle: h, cluster: f.cluster} +} + +type streamResetFilter struct { + shared.EmptyHttpFilter + handle shared.HttpFilterHandle + cluster string + streamHandle uint64 + receivedHdrs bool + resetCalled bool +} + +func (p *streamResetFilter) OnRequestHeaders(_ shared.HeaderMap, _ bool) shared.HeadersStatus { + res, id := p.handle.StartHttpStream(p.cluster, + [][2]string{{":path", "/slow"}, {":method", "GET"}, {"host", "example.com"}}, + nil, true, 5000, p) + if res != shared.HttpCalloutInitSuccess { + p.handle.SendLocalResponse(500, [][2]string{{"x-error", "stream_init_failed"}}, nil, "") + return shared.HeadersStatusStop + } + p.streamHandle = id + return shared.HeadersStatusStop +} + +func (p *streamResetFilter) OnHttpStreamHeaders(id uint64, _ [][2]shared.UnsafeEnvoyBuffer, _ bool) { + if id != p.streamHandle { + return + } + p.receivedHdrs = true + // Immediately reset the stream after receiving headers. + p.handle.ResetHttpStream(id) +} +func (p *streamResetFilter) OnHttpStreamData(_ uint64, _ []shared.UnsafeEnvoyBuffer, _ bool) {} +func (p *streamResetFilter) OnHttpStreamTrailers(_ uint64, _ [][2]shared.UnsafeEnvoyBuffer) {} +func (p *streamResetFilter) OnHttpStreamComplete(_ uint64) {} +func (p *streamResetFilter) OnHttpStreamReset(id uint64, _ shared.HttpStreamResetReason) { + if id != p.streamHandle { + return + } + p.resetCalled = true + p.handle.SendLocalResponse(200, + [][2]string{{"x-stream", "reset_ok"}}, + []byte("stream_was_reset"), "") +} + +// ============================================================================= +// Test 5: Upstream Reset +// ============================================================================= + +type upstreamResetFactory struct { + shared.EmptyHttpFilterConfigFactory +} + +func (upstreamResetFactory) Create(_ shared.HttpFilterConfigHandle, c []byte) (shared.HttpFilterFactory, error) { + return &upstreamResetFilterFactory{cluster: string(c)}, nil +} + +type upstreamResetFilterFactory struct { + shared.EmptyHttpFilterFactory + cluster string +} + +func (f *upstreamResetFilterFactory) Create(h shared.HttpFilterHandle) shared.HttpFilter { + return &upstreamResetFilter{handle: h, cluster: f.cluster} +} + +type upstreamResetFilter struct { + shared.EmptyHttpFilter + handle shared.HttpFilterHandle + cluster string + streamHandle uint64 +} + +func (p *upstreamResetFilter) OnRequestHeaders(_ shared.HeaderMap, _ bool) shared.HeadersStatus { + res, id := p.handle.StartHttpStream(p.cluster, + [][2]string{{":path", "/reset"}, {":method", "GET"}, {"host", "example.com"}}, + nil, true, 5000, p) + if res != shared.HttpCalloutInitSuccess { + p.handle.SendLocalResponse(500, [][2]string{{"x-error", "stream_init_failed"}}, nil, "") + return shared.HeadersStatusStop + } + p.streamHandle = id + return shared.HeadersStatusStop +} + +func (p *upstreamResetFilter) OnHttpStreamHeaders(_ uint64, _ [][2]shared.UnsafeEnvoyBuffer, _ bool) { +} +func (p *upstreamResetFilter) OnHttpStreamData(_ uint64, _ []shared.UnsafeEnvoyBuffer, _ bool) {} +func (p *upstreamResetFilter) OnHttpStreamTrailers(_ uint64, _ [][2]shared.UnsafeEnvoyBuffer) {} +func (p *upstreamResetFilter) OnHttpStreamComplete(_ uint64) {} +func (p *upstreamResetFilter) OnHttpStreamReset(id uint64, _ shared.HttpStreamResetReason) { + if id != p.streamHandle { + return + } + p.handle.SendLocalResponse(200, + [][2]string{{"x-reset", "true"}}, + []byte("upstream_reset"), "") +} diff --git a/test/extensions/dynamic_modules/test_data/go/listener_integration_test/listener_integration_test.go b/test/extensions/dynamic_modules/test_data/go/listener_integration_test/listener_integration_test.go new file mode 100644 index 0000000000000..4e2980c2a5a5d --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/listener_integration_test/listener_integration_test.go @@ -0,0 +1,47 @@ +// Listener filter integration test module. +// +// Registers a single filter "test_filter" that returns Continue from OnAccept (allowing +// the filter chain to proceed) and exercises a few read-only handle accessors on each +// invocation. +package main + +import ( + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterListenerFilterConfigFactories(map[string]shared.ListenerFilterConfigFactory{ + "test_filter": &passthroughConfigFactory{}, + }) +} + +func main() {} //nolint:all + +type passthroughConfigFactory struct { + shared.EmptyListenerFilterConfigFactory +} + +func (passthroughConfigFactory) Create(_ shared.ListenerFilterConfigHandle, _ []byte) (shared.ListenerFilterFactory, error) { + return &passthroughFactory{}, nil +} + +type passthroughFactory struct { + shared.EmptyListenerFilterFactory +} + +func (*passthroughFactory) Create(_ shared.ListenerFilterHandle) shared.ListenerFilter { + return &passthroughFilter{} +} + +type passthroughFilter struct { + shared.EmptyListenerFilter +} + +func (*passthroughFilter) OnAccept(handle shared.ListenerFilterHandle) shared.ListenerFilterStatus { + // Exercise a handful of read-only handle methods so they're hit at runtime. + _, _, _ = handle.GetRemoteAddress() + _, _, _ = handle.GetLocalAddress() + return shared.ListenerFilterStatusContinue +} diff --git a/test/extensions/dynamic_modules/test_data/go/load_balancer_integration_test/load_balancer_integration_test.go b/test/extensions/dynamic_modules/test_data/go/load_balancer_integration_test/load_balancer_integration_test.go new file mode 100644 index 0000000000000..ea877187788a0 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/load_balancer_integration_test/load_balancer_integration_test.go @@ -0,0 +1,49 @@ +// Load balancer integration test module. +// +// Registers a "first_host_lb" load balancer that always picks priority 0, index 0 (the +// only host in the test setup). The test asserts that requests successfully route +// through Envoy with the dynamic-module LB attached, exercising the full +// ChooseHost / OnHostMembershipUpdate dispatch surface. +package main + +import ( + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterLoadBalancerConfigFactories(map[string]shared.LoadBalancerConfigFactory{ + "first_host_lb": &firstHostConfigFactory{}, + }) +} + +func main() {} //nolint:all + +type firstHostConfigFactory struct { + shared.EmptyLoadBalancerConfigFactory +} + +func (firstHostConfigFactory) Create(_ shared.LoadBalancerConfigHandle, _ []byte) (shared.LoadBalancerFactory, error) { + return &firstHostFactory{}, nil +} + +type firstHostFactory struct { + shared.EmptyLoadBalancerFactory +} + +func (*firstHostFactory) Create(_ shared.LoadBalancerHandle) shared.LoadBalancer { + return &firstHostLB{} +} + +type firstHostLB struct { + shared.EmptyLoadBalancer +} + +func (*firstHostLB) ChooseHost(handle shared.LoadBalancerHandle, _ shared.LoadBalancerContext) (shared.HostSelection, bool) { + // Bail if no hosts are available. + if handle.GetHostsCount(0) == 0 { + return shared.HostSelection{}, false + } + return shared.HostSelection{Priority: 0, Index: 0}, true +} diff --git a/test/extensions/dynamic_modules/test_data/go/matcher_integration_test/matcher_integration_test.go b/test/extensions/dynamic_modules/test_data/go/matcher_integration_test/matcher_integration_test.go new file mode 100644 index 0000000000000..9054bd832812f --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/matcher_integration_test/matcher_integration_test.go @@ -0,0 +1,40 @@ +// Input matcher integration test module. Mirror of the C +// matcher_check_headers fake at test_data/c/matcher_check_headers.c. +// +// Registers a matcher named "header_check" that takes the header name to inspect via +// matcher_config bytes. OnMatch returns true iff the named request header is present +// with value exactly "match". +package main + +import ( + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterMatcherConfigFactories(map[string]shared.MatcherConfigFactory{ + "header_check": &headerCheckConfigFactory{}, + }) +} + +func main() {} //nolint:all + +type headerCheckConfigFactory struct{} + +func (headerCheckConfigFactory) Create(_ string, config []byte) (shared.Matcher, error) { + return &headerCheckMatcher{headerName: string(config)}, nil +} + +type headerCheckMatcher struct { + shared.EmptyMatcher + headerName string +} + +func (m *headerCheckMatcher) OnMatch(ctx shared.MatchInputContext) bool { + val, count, ok := ctx.GetHeaderValue(shared.HttpHeaderTypeRequestHeader, m.headerName, 0) + if !ok || count == 0 { + return false + } + return val.ToUnsafeString() == "match" +} diff --git a/test/extensions/dynamic_modules/test_data/go/network_integration_test/network_integration_test.go b/test/extensions/dynamic_modules/test_data/go/network_integration_test/network_integration_test.go index 58ed5dfba45c2..f5035d059b6b5 100644 --- a/test/extensions/dynamic_modules/test_data/go/network_integration_test/network_integration_test.go +++ b/test/extensions/dynamic_modules/test_data/go/network_integration_test/network_integration_test.go @@ -12,6 +12,8 @@ func init() { "connection_state": &connectionStateConfigFactory{}, "half_close": &halfCloseConfigFactory{}, "buffer_limits": &bufferLimitsConfigFactory{}, + "pause_resume": &pauseResumeConfigFactory{}, + "data_appender": &dataAppenderConfigFactory{}, }) } @@ -170,3 +172,86 @@ func (f *bufferLimitsFilter) OnNewConnection() shared.NetworkFilterStatus { } return shared.NetworkFilterStatusContinue } + +// ============================================================================= +// pause_resume: returns Stop on the first OnRead call, schedules a ContinueReading +// via the filter scheduler, and lets the data flow normally on subsequent reads. +// +// Verifies that NetworkFilterStatusStop genuinely pauses iteration and that +// ContinueReading from a scheduled task resumes it. Without the resume the +// upstream connection never sees the data and the test would hang. +// ============================================================================= + +type pauseResumeConfigFactory struct { + shared.EmptyNetworkFilterConfigFactory +} + +func (pauseResumeConfigFactory) Create(shared.NetworkFilterConfigHandle, []byte) (shared.NetworkFilterFactory, error) { + return &pauseResumeFactory{}, nil +} + +type pauseResumeFactory struct { + shared.EmptyNetworkFilterFactory +} + +func (*pauseResumeFactory) Create(handle shared.NetworkFilterHandle) shared.NetworkFilter { + return &pauseResumeFilter{handle: handle} +} + +type pauseResumeFilter struct { + shared.EmptyNetworkFilter + handle shared.NetworkFilterHandle + paused bool +} + +func (f *pauseResumeFilter) OnRead(shared.NetworkBuffer, bool) shared.NetworkFilterStatus { + if !f.paused { + f.paused = true + // Schedule the resume on the same worker thread. The scheduler defers the + // continuation until after this OnRead returns; otherwise ContinueReading is + // a no-op since iteration hasn't been paused yet. + f.handle.GetScheduler().Schedule(func() { + f.handle.ContinueReading() + }) + return shared.NetworkFilterStatusStop + } + return shared.NetworkFilterStatusContinue +} + +// ============================================================================= +// data_appender: appends a fixed suffix to every read buffer before letting it +// flow downstream. Verifies ReadBuffer().Append() works end-to-end (the upstream +// must observe the suffix). +// ============================================================================= + +type dataAppenderConfigFactory struct { + shared.EmptyNetworkFilterConfigFactory +} + +func (dataAppenderConfigFactory) Create(shared.NetworkFilterConfigHandle, []byte) (shared.NetworkFilterFactory, error) { + return &dataAppenderFactory{}, nil +} + +type dataAppenderFactory struct { + shared.EmptyNetworkFilterFactory +} + +func (*dataAppenderFactory) Create(handle shared.NetworkFilterHandle) shared.NetworkFilter { + return &dataAppenderFilter{handle: handle} +} + +type dataAppenderFilter struct { + shared.EmptyNetworkFilter + handle shared.NetworkFilterHandle + appended bool +} + +func (f *dataAppenderFilter) OnRead(buf shared.NetworkBuffer, _ bool) shared.NetworkFilterStatus { + // Append once on the first OnRead so the upstream observes the modification. + // Subsequent reads pass through unchanged so we don't keep appending forever. + if !f.appended { + buf.Append([]byte("|appended")) + f.appended = true + } + return shared.NetworkFilterStatusContinue +} diff --git a/test/extensions/dynamic_modules/test_data/go/test_data.bzl b/test/extensions/dynamic_modules/test_data/go/test_data.bzl index 7d4d9beb4aa8a..5040bd2c2d09d 100644 --- a/test/extensions/dynamic_modules/test_data/go/test_data.bzl +++ b/test/extensions/dynamic_modules/test_data/go/test_data.bzl @@ -1,7 +1,14 @@ load("@io_bazel_rules_go//go:def.bzl", "go_binary") -# This declares a cc_library target that is used to build a shared library. -# name + ".c" is the source file that is compiled to create the shared library. +# Builds a Go-based test_data dynamic module as a c-shared .so. +# +# The Go SDK's `abi/*.go` files reference all of Envoy's per-surface +# `envoy_dynamic_module_callback_*` symbols. When a test binary loads the .so but only +# links a subset of the matching `abi_impl` C++ libraries, the unresolved references +# would break dlopen on Linux. The `-Wl,--unresolved-symbols=ignore-all` linker flag +# tells the dynamic linker to allow these unresolved symbols at load time; they're +# resolved lazily when the module actually invokes them, and any genuinely missing +# symbol surfaces as a clear error at first call rather than at dlopen. def test_program(name): go_binary( name = name, @@ -9,6 +16,15 @@ def test_program(name): out = "lib{}.so".format(name), cgo = True, linkmode = "c-shared", + gc_linkopts = select({ + "@platforms//os:linux": [ + "-extldflags=-Wl,--unresolved-symbols=ignore-all", + ], + "@platforms//os:macos": [ + "-extldflags=-Wl,-undefined,dynamic_lookup", + ], + "//conditions:default": [], + }), visibility = ["//visibility:public"], deps = [ "@envoy//source/extensions/dynamic_modules:go_sdk", diff --git a/test/extensions/dynamic_modules/test_data/go/tracer_integration_test/tracer_integration_test.go b/test/extensions/dynamic_modules/test_data/go/tracer_integration_test/tracer_integration_test.go new file mode 100644 index 0000000000000..e094160c4cc67 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/tracer_integration_test/tracer_integration_test.go @@ -0,0 +1,55 @@ +// Tracer integration test module. +// +// Registers a "test_tracer" that returns a recording TracerSpan from StartSpan. The span +// records started/finished counts in process-wide atomics for diagnostics; the test +// asserts the tracer was loaded successfully (request flows complete) and span dispatch +// runs without crashing. +package main + +import ( + "sync/atomic" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterTracerConfigFactories(map[string]shared.TracerConfigFactory{ + "test_tracer": &testTracerConfigFactory{}, + }) +} + +func main() {} //nolint:all + +var ( + startedSpans atomic.Uint32 + finishedSpans atomic.Uint32 +) + +type testTracerConfigFactory struct { + shared.EmptyTracerConfigFactory +} + +func (testTracerConfigFactory) Create(_ shared.TracerConfigHandle, _ []byte) (shared.Tracer, error) { + return &testTracer{}, nil +} + +type testTracer struct { + shared.EmptyTracer +} + +func (*testTracer) StartSpan(_ shared.TracerSpanContext, _ string, _ bool, _ shared.TraceReason) shared.TracerSpan { + startedSpans.Add(1) + return &testSpan{} +} + +// testSpan inherits all default no-ops from EmptyTracerSpan; override Finish to track +// completion. Envoy will call OnDestroy after Finish. +type testSpan struct { + shared.EmptyTracerSpan +} + +func (*testSpan) Finish() { + finishedSpans.Add(1) +} diff --git a/test/extensions/dynamic_modules/test_data/go/udp_integration_test/udp_integration_test.go b/test/extensions/dynamic_modules/test_data/go/udp_integration_test/udp_integration_test.go new file mode 100644 index 0000000000000..2ea8bda083ecf --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/udp_integration_test/udp_integration_test.go @@ -0,0 +1,85 @@ +// UDP listener filter integration test module. +// +// Two filters: +// +// "test_filter" — passthrough; returns Continue on every datagram. +// "stop_iteration" — drops every datagram by returning StopIteration. +// +// The integration driver sends UDP datagrams through Envoy's udp_proxy and asserts that +// passthrough datagrams reach the upstream while stop_iteration drops them. +package main + +import ( + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterUdpListenerFilterConfigFactories(map[string]shared.UdpListenerFilterConfigFactory{ + "test_filter": &passthroughConfigFactory{}, + "stop_iteration": &stopIterationConfigFactory{}, + }) +} + +func main() {} //nolint:all + +// ============================================================================= +// passthrough — Continue +// ============================================================================= + +type passthroughConfigFactory struct { + shared.EmptyUdpListenerFilterConfigFactory +} + +func (passthroughConfigFactory) Create(_ shared.UdpListenerFilterConfigHandle, _ []byte) (shared.UdpListenerFilterFactory, error) { + return &passthroughFactory{}, nil +} + +type passthroughFactory struct { + shared.EmptyUdpListenerFilterFactory +} + +func (*passthroughFactory) Create(_ shared.UdpListenerFilterHandle) shared.UdpListenerFilter { + return &passthroughFilter{} +} + +type passthroughFilter struct { + shared.EmptyUdpListenerFilter +} + +func (*passthroughFilter) OnData(handle shared.UdpListenerFilterHandle) shared.UdpListenerFilterStatus { + // Exercise a couple of read-only handle accessors so they're hit at runtime — these + // methods would crash on bad cgo handling rather than return wrong data. + _ = handle.GetDatagramSize() + _, _, _ = handle.GetPeerAddress() + return shared.UdpListenerFilterStatusContinue +} + +// ============================================================================= +// stop_iteration — StopIteration +// ============================================================================= + +type stopIterationConfigFactory struct { + shared.EmptyUdpListenerFilterConfigFactory +} + +func (stopIterationConfigFactory) Create(_ shared.UdpListenerFilterConfigHandle, _ []byte) (shared.UdpListenerFilterFactory, error) { + return &stopIterationFactory{}, nil +} + +type stopIterationFactory struct { + shared.EmptyUdpListenerFilterFactory +} + +func (*stopIterationFactory) Create(_ shared.UdpListenerFilterHandle) shared.UdpListenerFilter { + return &stopIterationFilter{} +} + +type stopIterationFilter struct { + shared.EmptyUdpListenerFilter +} + +func (*stopIterationFilter) OnData(_ shared.UdpListenerFilterHandle) shared.UdpListenerFilterStatus { + return shared.UdpListenerFilterStatusStopIteration +} diff --git a/test/extensions/dynamic_modules/test_data/go/upstream_http_tcp_bridge/upstream_http_tcp_bridge.go b/test/extensions/dynamic_modules/test_data/go/upstream_http_tcp_bridge/upstream_http_tcp_bridge.go new file mode 100644 index 0000000000000..2cd36177971a9 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/go/upstream_http_tcp_bridge/upstream_http_tcp_bridge.go @@ -0,0 +1,110 @@ +// Upstream HTTP/TCP bridge test module. Mirrors test_data/rust/upstream_http_tcp_bridge.rs. +// +// Two modes selected by the config bytes: +// +// "local_reply" — short-circuits with a 403 local reply on encode_headers. +// anything else — streaming bridge: forwards request method as a "METHOD=X " prefix to +// the upstream, then forwards the request body, and converts upstream +// bytes back into HTTP response data. +// +// Loaded by test/extensions/upstreams/http/dynamic_modules/integration_test.cc which is +// parameterized over (rust, go). +package main + +import ( + "fmt" + + sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go" + _ "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/abi" + "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared" +) + +func init() { + sdk.RegisterUpstreamHttpTcpBridgeConfigFactories(map[string]shared.UpstreamHttpTcpBridgeConfigFactory{ + "test_bridge": &testBridgeConfigFactory{}, + }) +} + +func main() {} //nolint:all + +type bridgeMode int + +const ( + modeStreaming bridgeMode = iota + modeLocalReply +) + +type testBridgeConfigFactory struct{} + +func (testBridgeConfigFactory) Create(_ string, config []byte) (shared.UpstreamHttpTcpBridgeFactory, error) { + mode := modeStreaming + if string(config) == "local_reply" { + mode = modeLocalReply + } + return &testBridgeFactory{mode: mode}, nil +} + +type testBridgeFactory struct { + shared.EmptyUpstreamHttpTcpBridgeFactory + mode bridgeMode +} + +func (f *testBridgeFactory) Create(_ shared.UpstreamHttpTcpBridgeHandle) shared.UpstreamHttpTcpBridge { + return &testBridge{mode: f.mode} +} + +type testBridge struct { + shared.EmptyUpstreamHttpTcpBridge + mode bridgeMode + responseHeadersSent bool +} + +func (b *testBridge) EncodeHeaders(handle shared.UpstreamHttpTcpBridgeHandle, _ bool) { + switch b.mode { + case modeStreaming: + method, _, ok := handle.GetRequestHeader(":method", 0) + if ok { + prefix := fmt.Sprintf("METHOD=%s ", method.ToUnsafeString()) + handle.SendUpstreamData([]byte(prefix), false) + } + case modeLocalReply: + handle.SendResponse(403, nil, []byte("access denied")) + } +} + +func (b *testBridge) EncodeData(handle shared.UpstreamHttpTcpBridgeHandle, endOfStream bool) { + chunks := handle.GetRequestBuffer() + if len(chunks) == 0 { + if endOfStream { + handle.SendUpstreamData(nil, true) + } + return + } + for i, chunk := range chunks { + isLast := endOfStream && i == len(chunks)-1 + handle.SendUpstreamData(chunk.ToUnsafeBytes(), isLast) + } +} + +func (b *testBridge) EncodeTrailers(handle shared.UpstreamHttpTcpBridgeHandle) { + handle.SendUpstreamData(nil, true) +} + +func (b *testBridge) OnUpstreamData(handle shared.UpstreamHttpTcpBridgeHandle, endOfStream bool) { + if !b.responseHeadersSent { + handle.SendResponseHeaders(200, + [][2]string{{"x-bridge-mode", "dynamic_module"}}, false) + b.responseHeadersSent = true + } + chunks := handle.GetResponseBuffer() + if len(chunks) == 0 { + if endOfStream { + handle.SendResponseData(nil, true) + } + return + } + for i, chunk := range chunks { + isLast := endOfStream && i == len(chunks)-1 + handle.SendResponseData(chunk.ToUnsafeBytes(), isLast) + } +} diff --git a/test/extensions/dynamic_modules/test_data/rust/BUILD b/test/extensions/dynamic_modules/test_data/rust/BUILD index 9185767f5fcef..9d03daaacb057 100644 --- a/test/extensions/dynamic_modules/test_data/rust/BUILD +++ b/test/extensions/dynamic_modules/test_data/rust/BUILD @@ -8,7 +8,13 @@ package(default_visibility = [ "//test/extensions/dynamic_modules:__pkg__", "//test/extensions/dynamic_modules/bootstrap:__pkg__", "//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/extensions/load_balancing_policies/dynamic_modules:__pkg__", + "//test/extensions/matching/input_matchers/dynamic_modules:__pkg__", + "//test/extensions/tracers/dynamic_modules:__pkg__", + "//test/extensions/transport_sockets/tls/cert_validator/dynamic_modules:__pkg__", "//test/extensions/upstreams/http/dynamic_modules:__pkg__", ]) @@ -59,3 +65,15 @@ test_program(name = "access_log_integration_test") test_program(name = "upstream_http_tcp_bridge") test_program(name = "cluster_integration_test") + +test_program(name = "udp_integration_test") + +test_program(name = "matcher_integration_test") + +test_program(name = "listener_integration_test") + +test_program(name = "tracer_integration_test") + +test_program(name = "load_balancer_integration_test") + +test_program(name = "cert_validator_test") diff --git a/test/extensions/dynamic_modules/test_data/rust/Cargo.toml b/test/extensions/dynamic_modules/test_data/rust/Cargo.toml index 06b0bfe459caa..b64b2a3aef11f 100644 --- a/test/extensions/dynamic_modules/test_data/rust/Cargo.toml +++ b/test/extensions/dynamic_modules/test_data/rust/Cargo.toml @@ -85,3 +85,39 @@ name = "network_integration_test" path = "network_integration_test.rs" crate-type = ["cdylib"] test = true + +[[example]] +name = "udp_integration_test" +path = "udp_integration_test.rs" +crate-type = ["cdylib"] +test = true + +[[example]] +name = "matcher_integration_test" +path = "matcher_integration_test.rs" +crate-type = ["cdylib"] +test = true + +[[example]] +name = "listener_integration_test" +path = "listener_integration_test.rs" +crate-type = ["cdylib"] +test = true + +[[example]] +name = "tracer_integration_test" +path = "tracer_integration_test.rs" +crate-type = ["cdylib"] +test = true + +[[example]] +name = "load_balancer_integration_test" +path = "load_balancer_integration_test.rs" +crate-type = ["cdylib"] +test = true + +[[example]] +name = "cert_validator_test" +path = "cert_validator_test.rs" +crate-type = ["cdylib"] +test = true diff --git a/test/extensions/dynamic_modules/test_data/rust/access_log_integration_test.rs b/test/extensions/dynamic_modules/test_data/rust/access_log_integration_test.rs index ce658feded87e..3fee429fa46ef 100644 --- a/test/extensions/dynamic_modules/test_data/rust/access_log_integration_test.rs +++ b/test/extensions/dynamic_modules/test_data/rust/access_log_integration_test.rs @@ -52,21 +52,62 @@ fn new_access_logger_config_fn( } } -/// Access logger configuration. +/// Access logger configuration. Holds counter/gauge handles defined at config-time so the +/// per-request `log` callback can record getter results into them — this lets the C++ test +/// driver assert correctness via /stats. struct TestAccessLoggerConfig { _name: String, log_counter: CounterHandle, + http2_counter: CounterHandle, + resp_200_counter: CounterHandle, + method_get_counter: CounterHandle, + path_test_counter: CounterHandle, + has_route_counter: CounterHandle, + resp_code_gauge: GaugeHandle, + bytes_sent_gauge: GaugeHandle, + hdr_count_gauge: GaugeHandle, } impl AccessLoggerConfig for TestAccessLoggerConfig { fn new(ctx: &ConfigContext, name: &str, _config: &[u8]) -> Result { - // Define a counter metric during configuration. let log_counter = ctx .define_counter("test_log_count") - .ok_or("Failed to define counter")?; + .ok_or("Failed to define test_log_count")?; + let http2_counter = ctx + .define_counter("test_request_protocol_http2") + .ok_or("Failed to define test_request_protocol_http2")?; + let resp_200_counter = ctx + .define_counter("test_response_code_200") + .ok_or("Failed to define test_response_code_200")?; + let method_get_counter = ctx + .define_counter("test_method_get") + .ok_or("Failed to define test_method_get")?; + let path_test_counter = ctx + .define_counter("test_path_test") + .ok_or("Failed to define test_path_test")?; + let has_route_counter = ctx + .define_counter("test_has_route_name") + .ok_or("Failed to define test_has_route_name")?; + let resp_code_gauge = ctx + .define_gauge("test_response_code_last") + .ok_or("Failed to define test_response_code_last")?; + let bytes_sent_gauge = ctx + .define_gauge("test_bytes_sent_last") + .ok_or("Failed to define test_bytes_sent_last")?; + let hdr_count_gauge = ctx + .define_gauge("test_request_headers_count") + .ok_or("Failed to define test_request_headers_count")?; Ok(Self { _name: name.to_string(), log_counter, + http2_counter, + resp_200_counter, + method_get_counter, + path_test_counter, + has_route_counter, + resp_code_gauge, + bytes_sent_gauge, + hdr_count_gauge, }) } @@ -81,6 +122,14 @@ impl AccessLoggerConfig for TestAccessLoggerConfig { Box::new(TestAccessLogger { pending_logs: 0, log_counter: self.log_counter, + http2_counter: self.http2_counter, + resp_200_counter: self.resp_200_counter, + method_get_counter: self.method_get_counter, + path_test_counter: self.path_test_counter, + has_route_counter: self.has_route_counter, + resp_code_gauge: self.resp_code_gauge, + bytes_sent_gauge: self.bytes_sent_gauge, + hdr_count_gauge: self.hdr_count_gauge, metrics, }) } @@ -90,6 +139,14 @@ impl AccessLoggerConfig for TestAccessLoggerConfig { struct TestAccessLogger { pending_logs: u32, log_counter: CounterHandle, + http2_counter: CounterHandle, + resp_200_counter: CounterHandle, + method_get_counter: CounterHandle, + path_test_counter: CounterHandle, + has_route_counter: CounterHandle, + resp_code_gauge: GaugeHandle, + bytes_sent_gauge: GaugeHandle, + hdr_count_gauge: GaugeHandle, metrics: MetricsContext, } @@ -203,18 +260,55 @@ impl AccessLogger for TestAccessLogger { let _response_trailers = ctx.get_all_headers(abi::envoy_dynamic_module_type_http_header_type::ResponseTrailer); - // Test generic attribute accessors. - let _attr_protocol = - ctx.get_attribute_string(abi::envoy_dynamic_module_type_attribute_id::RequestProtocol); - let _attr_route = - ctx.get_attribute_string(abi::envoy_dynamic_module_type_attribute_id::XdsRouteName); - let _attr_resp_code = - ctx.get_attribute_int(abi::envoy_dynamic_module_type_attribute_id::ResponseCode); + // Test generic attribute accessors. Record select results into counters/gauges so + // the C++ driver can verify via /stats. + if let Some(proto) = + ctx.get_attribute_string(abi::envoy_dynamic_module_type_attribute_id::RequestProtocol) + { + if proto.as_slice() == b"HTTP/2" { + self.metrics.increment_counter(self.http2_counter, 1); + } + } + if ctx + .get_attribute_string(abi::envoy_dynamic_module_type_attribute_id::XdsRouteName) + .is_some() + { + self.metrics.increment_counter(self.has_route_counter, 1); + } + if let Some(code) = + ctx.get_attribute_int(abi::envoy_dynamic_module_type_attribute_id::ResponseCode) + { + self.metrics.set_gauge(self.resp_code_gauge, code); + if code == 200 { + self.metrics.increment_counter(self.resp_200_counter, 1); + } + } let _attr_conn_id = ctx.get_attribute_int(abi::envoy_dynamic_module_type_attribute_id::ConnectionId); let _attr_mtls = ctx.get_attribute_bool(abi::envoy_dynamic_module_type_attribute_id::ConnectionMtls); + // Header-based counters: bump if :method == GET, :path == /test. + if let Some(method) = ctx.get_request_header(":method") { + if method.as_slice() == b"GET" { + self.metrics.increment_counter(self.method_get_counter, 1); + } + } + if let Some(path) = ctx.get_request_header(":path") { + if path.as_slice() == b"/test" { + self.metrics.increment_counter(self.path_test_counter, 1); + } + } + // Set request header count gauge. + let req_hdr_count = + ctx.get_headers_count(abi::envoy_dynamic_module_type_http_header_type::RequestHeader); + self + .metrics + .set_gauge(self.hdr_count_gauge, req_hdr_count as u64); + // Set bytes_sent gauge from BytesInfo. + let bi = ctx.bytes_info(); + self.metrics.set_gauge(self.bytes_sent_gauge, bi.bytes_sent); + // Test access log type. let log_type = ctx.log_type(); assert_eq!(log_type.as_str(), "DownstreamEnd"); diff --git a/test/extensions/dynamic_modules/test_data/rust/cert_validator_test.rs b/test/extensions/dynamic_modules/test_data/rust/cert_validator_test.rs new file mode 100644 index 0000000000000..ded02826b42e7 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/rust/cert_validator_test.rs @@ -0,0 +1,39 @@ +//! Cert validator test module. Mirror of +//! test_data/go/cert_validator_test/cert_validator_test.go. +//! +//! Registers a "test" cert validator that always returns Successful for any chain. + +use envoy_proxy_dynamic_modules_rust_sdk::cert_validator::*; +use envoy_proxy_dynamic_modules_rust_sdk::declare_cert_validator_init_functions; + +declare_cert_validator_init_functions!(init, new_cert_validator_config); + +fn init() -> bool { + true +} + +fn new_cert_validator_config(_name: &str, _config: &[u8]) -> Option> { + Some(Box::new(NoOpValidator {})) +} + +struct NoOpValidator {} + +impl CertValidatorConfig for NoOpValidator { + fn do_verify_cert_chain( + &self, + _envoy_cert_validator: &EnvoyCertValidator, + _certs: &[&[u8]], + _host_name: &str, + _is_server: bool, + ) -> ValidationResult { + ValidationResult::successful() + } + + fn get_ssl_verify_mode(&self, _handshaker_provides_certificates: bool) -> i32 { + 0 + } + + fn update_digest(&self) -> &[u8] { + b"noop_cert_validator" + } +} diff --git a/test/extensions/dynamic_modules/test_data/rust/cluster_integration_test.rs b/test/extensions/dynamic_modules/test_data/rust/cluster_integration_test.rs index 8a3b3148d3c8d..c7a3f397a2ecc 100644 --- a/test/extensions/dynamic_modules/test_data/rust/cluster_integration_test.rs +++ b/test/extensions/dynamic_modules/test_data/rust/cluster_integration_test.rs @@ -26,10 +26,14 @@ fn new_cluster_config( ) -> Option> { let config_str = std::str::from_utf8(config).unwrap_or(""); match name { - "sync_host_selection" => Some(Box::new(SyncHostSelectionClusterConfig { - upstream_address: config_str.to_string(), - metrics: envoy_cluster_metrics, - })), + "sync_host_selection" => { + let counter_id = envoy_cluster_metrics.define_counter("requests_routed").ok(); + Some(Box::new(SyncHostSelectionClusterConfig { + upstream_address: config_str.to_string(), + counter_id, + metrics: envoy_cluster_metrics, + })) + }, "async_host_selection" => Some(Box::new(AsyncHostSelectionClusterConfig { upstream_address: config_str.to_string(), })), @@ -49,16 +53,16 @@ fn new_cluster_config( struct SyncHostSelectionClusterConfig { upstream_address: String, + counter_id: Option, metrics: Arc, } impl ClusterConfig for SyncHostSelectionClusterConfig { fn new_cluster(&self, _envoy_cluster: &dyn EnvoyCluster) -> Box { - let counter_id = self.metrics.define_counter("requests_routed").ok(); Box::new(SyncHostSelectionCluster { upstream_address: self.upstream_address.clone(), hosts: Arc::new(Mutex::new(HostList(Vec::new()))), - counter_id, + counter_id: self.counter_id, metrics: self.metrics.clone(), }) } diff --git a/test/extensions/dynamic_modules/test_data/rust/http_integration_test.rs b/test/extensions/dynamic_modules/test_data/rust/http_integration_test.rs index a97caca92c16a..7eb3191ea5ea1 100644 --- a/test/extensions/dynamic_modules/test_data/rust/http_integration_test.rs +++ b/test/extensions/dynamic_modules/test_data/rust/http_integration_test.rs @@ -136,6 +136,15 @@ fn new_http_filter_config_fn( Some(Box::new(ConfigStreamConfig { stream_done })) }, "list_metadata_callbacks" => Some(Box::new(ListMetadataCallbacksFilterConfig {})), + "dynamic_metadata" => Some(Box::new(DynamicMetadataConfig {})), + "filter_state_writer" => Some(Box::new(FilterStateWriterConfig { + value: if config.is_empty() { + "default_value".to_string() + } else { + String::from_utf8_lossy(config).to_string() + }, + })), + "filter_state_reader" => Some(Box::new(FilterStateReaderConfig {})), _ => panic!("Unknown filter name: {name}"), } } @@ -1929,3 +1938,149 @@ impl HttpFilter for ListMetadataCallbacksFilter { envoy_dynamic_module_type_on_http_filter_response_headers_status::Continue } } + +// ============================================================================= +// DynamicMetadata: scalar Get/Set on dynamic metadata. +// ============================================================================= + +struct DynamicMetadataConfig {} + +impl HttpFilterConfig for DynamicMetadataConfig { + fn new_http_filter(&self, _envoy: &mut EHF) -> Box> { + Box::new(DynamicMetadataFilter {}) + } +} + +struct DynamicMetadataFilter {} + +impl HttpFilter for DynamicMetadataFilter { + fn on_request_headers( + &mut self, + envoy_filter: &mut EHF, + _end_of_stream: bool, + ) -> abi::envoy_dynamic_module_type_on_http_filter_request_headers_status { + use abi::envoy_dynamic_module_type_metadata_source::Route; + + // Get-then-set pattern: the Get* methods return a reference borrowing envoy_filter + // immutably; subsequent Set* calls need a mutable borrow. Materialize values into + // owned forms first so the immutable borrow ends before the mutating calls. + let str_val: Option = envoy_filter + .get_metadata_string(Route, "test_ns", "string_key") + .map(|b| String::from_utf8_lossy(b.as_slice()).to_string()); + let num_val: Option = envoy_filter.get_metadata_number(Route, "test_ns", "number_key"); + let bool_val: Option = envoy_filter.get_metadata_bool(Route, "test_ns", "bool_key"); + + if let Some(s) = str_val { + envoy_filter.set_dynamic_metadata_string("dm_test", "string_key", &s); + } + if let Some(n) = num_val { + envoy_filter.set_dynamic_metadata_number("dm_test", "number_key", n); + } + if let Some(b) = bool_val { + envoy_filter.set_dynamic_metadata_bool("dm_test", "bool_key", b); + } + abi::envoy_dynamic_module_type_on_http_filter_request_headers_status::Continue + } + + fn on_response_headers( + &mut self, + envoy_filter: &mut EHF, + _end_of_stream: bool, + ) -> abi::envoy_dynamic_module_type_on_http_filter_response_headers_status { + use abi::envoy_dynamic_module_type_metadata_source::Dynamic; + + // The metadata getters return references into `envoy_filter`, holding an immutable + // borrow. We must materialize Vec copies before calling the mutating + // `set_response_header` so the borrow checker is satisfied. + let str_val: Option> = envoy_filter + .get_metadata_string(Dynamic, "dm_test", "string_key") + .map(|b| b.as_slice().to_vec()); + let num_val: Option = envoy_filter.get_metadata_number(Dynamic, "dm_test", "number_key"); + let bool_val: Option = envoy_filter.get_metadata_bool(Dynamic, "dm_test", "bool_key"); + let key_count = envoy_filter + .get_metadata_keys(Dynamic, "dm_test") + .map(|v| v.len()) + .unwrap_or(0); + + if let Some(s) = str_val { + envoy_filter.set_response_header("x-dm-string", &s); + } + if let Some(n) = num_val { + envoy_filter.set_response_header("x-dm-number", n.to_string().as_bytes()); + } + if let Some(b) = bool_val { + envoy_filter.set_response_header("x-dm-bool", b.to_string().as_bytes()); + } + envoy_filter.set_response_header("x-dm-key-count", key_count.to_string().as_bytes()); + + abi::envoy_dynamic_module_type_on_http_filter_response_headers_status::Continue + } +} + +// ============================================================================= +// FilterState: round-trip writer + reader. +// ============================================================================= + +struct FilterStateWriterConfig { + value: String, +} + +impl HttpFilterConfig for FilterStateWriterConfig { + fn new_http_filter(&self, _envoy: &mut EHF) -> Box> { + Box::new(FilterStateWriterFilter { + value: self.value.clone(), + }) + } +} + +struct FilterStateWriterFilter { + value: String, +} + +impl HttpFilter for FilterStateWriterFilter { + fn on_request_headers( + &mut self, + envoy_filter: &mut EHF, + _end_of_stream: bool, + ) -> abi::envoy_dynamic_module_type_on_http_filter_request_headers_status { + envoy_filter.set_filter_state_bytes(b"test_filter_state_key", self.value.as_bytes()); + abi::envoy_dynamic_module_type_on_http_filter_request_headers_status::Continue + } +} + +struct FilterStateReaderConfig {} + +impl HttpFilterConfig for FilterStateReaderConfig { + fn new_http_filter(&self, _envoy: &mut EHF) -> Box> { + Box::new(FilterStateReaderFilter { captured: None }) + } +} + +struct FilterStateReaderFilter { + captured: Option>, +} + +impl HttpFilter for FilterStateReaderFilter { + fn on_request_headers( + &mut self, + envoy_filter: &mut EHF, + _end_of_stream: bool, + ) -> abi::envoy_dynamic_module_type_on_http_filter_request_headers_status { + if let Some(buf) = envoy_filter.get_filter_state_bytes(b"test_filter_state_key") { + self.captured = Some(buf.as_slice().to_vec()); + } + abi::envoy_dynamic_module_type_on_http_filter_request_headers_status::Continue + } + + fn on_response_headers( + &mut self, + envoy_filter: &mut EHF, + _end_of_stream: bool, + ) -> abi::envoy_dynamic_module_type_on_http_filter_response_headers_status { + match &self.captured { + Some(v) => envoy_filter.set_response_header("x-filter-state-value", v), + None => envoy_filter.set_response_header("x-filter-state-value", b""), + }; + abi::envoy_dynamic_module_type_on_http_filter_response_headers_status::Continue + } +} diff --git a/test/extensions/dynamic_modules/test_data/rust/listener_integration_test.rs b/test/extensions/dynamic_modules/test_data/rust/listener_integration_test.rs new file mode 100644 index 0000000000000..13658afa0efb3 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/rust/listener_integration_test.rs @@ -0,0 +1,53 @@ +//! Listener filter integration test module. Mirror of +//! test_data/go/listener_integration_test/listener_integration_test.go. +//! +//! Registers a single filter "test_filter" that returns Continue from on_accept and +//! exercises a couple of read-only handle accessors. + +use envoy_proxy_dynamic_modules_rust_sdk::*; + +// Using declare_all_init_functions! with the `listener:` category instead of +// declare_listener_filter_init_functions!. The dedicated single-surface macro takes a +// server_factory_context_ptr argument that references a type not yet defined in the C +// ABI (envoy_dynamic_module_type_server_factory_context_envoy_ptr); declare_all_init_functions! +// avoids that signature mismatch. +declare_all_init_functions!( + init, + listener: new_filter_config, +); + +fn init() -> bool { + true +} + +fn new_filter_config( + _envoy: &mut EC, + name: &str, + _config: &[u8], +) -> Option>> { + match name { + "test_filter" => Some(Box::new(PassthroughConfig {})), + _ => None, + } +} + +struct PassthroughConfig {} + +impl ListenerFilterConfig for PassthroughConfig { + fn new_listener_filter(&self, _envoy: &mut ELF) -> Box> { + Box::new(PassthroughFilter {}) + } +} + +struct PassthroughFilter {} + +impl ListenerFilter for PassthroughFilter { + fn on_accept( + &mut self, + envoy: &mut ELF, + ) -> abi::envoy_dynamic_module_type_on_listener_filter_status { + let _ = envoy.get_remote_address(); + let _ = envoy.get_local_address(); + abi::envoy_dynamic_module_type_on_listener_filter_status::Continue + } +} diff --git a/test/extensions/dynamic_modules/test_data/rust/load_balancer_integration_test.rs b/test/extensions/dynamic_modules/test_data/rust/load_balancer_integration_test.rs new file mode 100644 index 0000000000000..c5c10d3648c85 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/rust/load_balancer_integration_test.rs @@ -0,0 +1,43 @@ +//! Load balancer integration test module. Mirror of +//! test_data/go/load_balancer_integration_test/load_balancer_integration_test.go. +//! +//! Registers a "first_host_lb" load balancer that always picks priority 0, index 0. + +use envoy_proxy_dynamic_modules_rust_sdk::*; +use std::sync::Arc; + +declare_load_balancer_init_functions!(init, new_lb_config); + +fn init() -> bool { + true +} + +fn new_lb_config( + _name: &str, + _config: &[u8], + _envoy_lb_config: Arc, +) -> Option> { + Some(Box::new(FirstHostConfig {})) +} + +struct FirstHostConfig {} + +impl LoadBalancerConfig for FirstHostConfig { + fn new_load_balancer(&self, _envoy_lb: &dyn EnvoyLoadBalancer) -> Box { + Box::new(FirstHostLB {}) + } +} + +struct FirstHostLB {} + +impl LoadBalancer for FirstHostLB { + fn choose_host(&mut self, envoy_lb: &dyn EnvoyLoadBalancer) -> Option { + if envoy_lb.get_hosts_count(0) == 0 { + return None; + } + Some(HostSelection { + priority: 0, + index: 0, + }) + } +} diff --git a/test/extensions/dynamic_modules/test_data/rust/matcher_integration_test.rs b/test/extensions/dynamic_modules/test_data/rust/matcher_integration_test.rs new file mode 100644 index 0000000000000..38f7e834cb0fe --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/rust/matcher_integration_test.rs @@ -0,0 +1,40 @@ +//! Input matcher integration test module. Mirror of the Go module at +//! test_data/go/matcher_integration_test/matcher_integration_test.go and the C fake at +//! test_data/c/matcher_check_headers.c. +//! +//! Registers a matcher named "header_check" that takes the header name to inspect via +//! matcher_config bytes. on_matcher_match returns true iff the named request header is +//! present with value exactly "match". + +use envoy_proxy_dynamic_modules_rust_sdk::abi; +use envoy_proxy_dynamic_modules_rust_sdk::declare_matcher; +use envoy_proxy_dynamic_modules_rust_sdk::matcher::*; + +// The declare_matcher! macro emits the matcher-specific entry points but NOT +// envoy_dynamic_module_on_program_init, which Envoy requires for ABI version +// negotiation. We provide it here manually. +#[no_mangle] +pub extern "C" fn envoy_dynamic_module_on_program_init() -> *const std::os::raw::c_char { + abi::envoy_dynamic_modules_abi_version.as_ptr() as *const std::os::raw::c_char +} + +struct HeaderCheckConfig { + header_name: String, +} + +impl MatcherConfig for HeaderCheckConfig { + fn new(_name: &str, config: &[u8]) -> Result { + Ok(Self { + header_name: String::from_utf8_lossy(config).to_string(), + }) + } + + fn on_matcher_match(&self, ctx: &MatchContext) -> bool { + match ctx.get_request_header(&self.header_name) { + Some(value) => value == b"match", + None => false, + } + } +} + +declare_matcher!(HeaderCheckConfig); diff --git a/test/extensions/dynamic_modules/test_data/rust/network_integration_test.rs b/test/extensions/dynamic_modules/test_data/rust/network_integration_test.rs index 50f1afbe597cb..6e409af622668 100644 --- a/test/extensions/dynamic_modules/test_data/rust/network_integration_test.rs +++ b/test/extensions/dynamic_modules/test_data/rust/network_integration_test.rs @@ -20,6 +20,8 @@ fn new_network_filter_config_fn Some(Box::new(ConnectionStateFilterConfig)), "half_close" => Some(Box::new(HalfCloseFilterConfig)), "buffer_limits" => Some(Box::new(BufferLimitsFilterConfig)), + "pause_resume" => Some(Box::new(PauseResumeFilterConfig)), + "data_appender" => Some(Box::new(DataAppenderFilterConfig)), _ => panic!("unknown filter name: {name}"), } } @@ -303,3 +305,74 @@ impl NetworkFilter for BufferLimitsFilter { BELOW_LOW_WATERMARK_CALLED.store(true, Ordering::SeqCst); } } + +// ============================================================================= +// pause_resume — exercises StopIteration + ContinueReading via the scheduler. +// ============================================================================= + +struct PauseResumeFilterConfig; + +impl NetworkFilterConfig for PauseResumeFilterConfig { + fn new_network_filter(&self, _envoy: &mut ENF) -> Box> { + Box::new(PauseResumeFilter { paused: false }) + } +} + +struct PauseResumeFilter { + paused: bool, +} + +impl NetworkFilter for PauseResumeFilter { + fn on_read( + &mut self, + envoy_filter: &mut ENF, + _data_length: usize, + _end_stream: bool, + ) -> abi::envoy_dynamic_module_type_on_network_filter_data_status { + if !self.paused { + self.paused = true; + // Schedule a continue_reading via the filter scheduler. on_scheduled fires on + // the same worker thread once dispatcher iteration unwinds back. + let scheduler = envoy_filter.new_scheduler(); + scheduler.commit(1); + return abi::envoy_dynamic_module_type_on_network_filter_data_status::StopIteration; + } + abi::envoy_dynamic_module_type_on_network_filter_data_status::Continue + } + + fn on_scheduled(&mut self, envoy_filter: &mut ENF, _event_id: u64) { + envoy_filter.continue_reading(); + } +} + +// ============================================================================= +// data_appender — append a fixed suffix to the read buffer so the upstream +// observes the modification. +// ============================================================================= + +struct DataAppenderFilterConfig; + +impl NetworkFilterConfig for DataAppenderFilterConfig { + fn new_network_filter(&self, _envoy: &mut ENF) -> Box> { + Box::new(DataAppenderFilter { appended: false }) + } +} + +struct DataAppenderFilter { + appended: bool, +} + +impl NetworkFilter for DataAppenderFilter { + fn on_read( + &mut self, + envoy_filter: &mut ENF, + _data_length: usize, + _end_stream: bool, + ) -> abi::envoy_dynamic_module_type_on_network_filter_data_status { + if !self.appended { + envoy_filter.append_read_buffer(b"|appended"); + self.appended = true; + } + abi::envoy_dynamic_module_type_on_network_filter_data_status::Continue + } +} diff --git a/test/extensions/dynamic_modules/test_data/rust/tracer_integration_test.rs b/test/extensions/dynamic_modules/test_data/rust/tracer_integration_test.rs new file mode 100644 index 0000000000000..c786da8f10549 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/rust/tracer_integration_test.rs @@ -0,0 +1,46 @@ +//! Tracer integration test module. Mirror of +//! test_data/go/tracer_integration_test/tracer_integration_test.go. + +use envoy_proxy_dynamic_modules_rust_sdk::*; + +declare_tracer_init_functions!(init, new_tracer_config); + +fn init() -> bool { + true +} + +fn new_tracer_config( + _ctx: TracerConfigContext, + _name: &str, + _config: &[u8], +) -> Option> { + Some(Box::new(TestTracer {})) +} + +struct TestTracer {} + +impl TracerConfig for TestTracer { + fn start_span( + &self, + _envoy_span: &dyn EnvoyTracerSpan, + _operation_name: &str, + _traced: bool, + _reason: TraceReason, + ) -> Option> { + Some(Box::new(TestSpan {})) + } +} + +struct TestSpan {} + +impl TracerSpan for TestSpan { + fn set_operation(&mut self, _operation: &str) {} + fn set_tag(&mut self, _key: &str, _value: &str) {} + fn log(&mut self, _timestamp_ns: i64, _event: &str) {} + fn finish(&mut self) {} + fn inject_context(&mut self, _envoy_span: &dyn EnvoyTracerSpan) {} + fn spawn_child(&mut self, _operation: &str, _start_time_ns: i64) -> Option> { + None + } + fn set_sampled(&mut self, _sampled: bool) {} +} diff --git a/test/extensions/dynamic_modules/test_data/rust/udp_integration_test.rs b/test/extensions/dynamic_modules/test_data/rust/udp_integration_test.rs new file mode 100644 index 0000000000000..82478d7b95241 --- /dev/null +++ b/test/extensions/dynamic_modules/test_data/rust/udp_integration_test.rs @@ -0,0 +1,75 @@ +//! UDP listener filter integration test module. Mirror of +//! test_data/go/udp_integration_test/udp_integration_test.go. +//! +//! Two filters: +//! "test_filter" — passthrough; returns Continue on every datagram. +//! "stop_iteration" — drops every datagram by returning StopIteration. + +use envoy_proxy_dynamic_modules_rust_sdk::*; + +declare_udp_listener_filter_init_functions!(init, new_filter_config); + +fn init() -> bool { + true +} + +fn new_filter_config( + _envoy: &mut EC, + name: &str, + _config: &[u8], +) -> Option>> { + match name { + "test_filter" => Some(Box::new(PassthroughConfig {})), + "stop_iteration" => Some(Box::new(StopIterationConfig {})), + _ => None, + } +} + +// ----------------------------------------------------------------------------- +// passthrough +// ----------------------------------------------------------------------------- + +struct PassthroughConfig {} + +impl UdpListenerFilterConfig for PassthroughConfig { + fn new_udp_listener_filter(&self, _envoy: &mut ELF) -> Box> { + Box::new(PassthroughFilter {}) + } +} + +struct PassthroughFilter {} + +impl UdpListenerFilter for PassthroughFilter { + fn on_data( + &mut self, + envoy: &mut ELF, + ) -> abi::envoy_dynamic_module_type_on_udp_listener_filter_status { + // Exercise a couple of read-only accessors so they're hit at runtime. + let _ = envoy.get_datagram_data(); + let _ = envoy.get_peer_address(); + abi::envoy_dynamic_module_type_on_udp_listener_filter_status::Continue + } +} + +// ----------------------------------------------------------------------------- +// stop_iteration +// ----------------------------------------------------------------------------- + +struct StopIterationConfig {} + +impl UdpListenerFilterConfig for StopIterationConfig { + fn new_udp_listener_filter(&self, _envoy: &mut ELF) -> Box> { + Box::new(StopIterationFilter {}) + } +} + +struct StopIterationFilter {} + +impl UdpListenerFilter for StopIterationFilter { + fn on_data( + &mut self, + _envoy: &mut ELF, + ) -> abi::envoy_dynamic_module_type_on_udp_listener_filter_status { + abi::envoy_dynamic_module_type_on_udp_listener_filter_status::StopIteration + } +} diff --git a/test/extensions/dynamic_modules/udp/BUILD b/test/extensions/dynamic_modules/udp/BUILD index 85a2ad5adc2c4..4acbc974e0bdc 100644 --- a/test/extensions/dynamic_modules/udp/BUILD +++ b/test/extensions/dynamic_modules/udp/BUILD @@ -69,8 +69,14 @@ envoy_cc_test( data = [ "//test/extensions/dynamic_modules/test_data/c:udp_no_op", "//test/extensions/dynamic_modules/test_data/c:udp_stop_iteration", + "//test/extensions/dynamic_modules/test_data/go:udp_integration_test", + "//test/extensions/dynamic_modules/test_data/rust:udp_integration_test", ], + env = {"GODEBUG": "cgocheck=0"}, deps = [ + # See //test/extensions/dynamic_modules/http:filter_test for why every test + # binary that loads a Go test_data .so depends on all_abi_impls. + "//source/extensions/dynamic_modules:all_abi_impls", "//source/extensions/filters/udp/dynamic_modules:config", "//source/extensions/filters/udp/udp_proxy:config", "//test/extensions/dynamic_modules:util", 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 index c34e91a477f99..4213f68d7e901 100644 --- a/test/extensions/dynamic_modules/udp/udp_dynamic_modules_integration_test.cc +++ b/test/extensions/dynamic_modules/udp/udp_dynamic_modules_integration_test.cc @@ -15,26 +15,55 @@ namespace UdpFilters { namespace DynamicModules { namespace { -class UdpDynamicModulesIntegrationTest : public testing::TestWithParam, +// Parameterized over (language, ip_version). The "c" language uses pre-existing C fakes +// (udp_no_op / udp_stop_iteration); "rust" and "go" use the udp_integration_test module +// each language ships, which registers two filter names: "test_filter" (passthrough) +// and "stop_iteration" (drop). +struct UdpDynamicModulesParam { + std::string language; + Network::Address::IpVersion ip_version; +}; + +class UdpDynamicModulesIntegrationTest : public testing::TestWithParam, public BaseIntegrationTest { public: UdpDynamicModulesIntegrationTest() - : BaseIntegrationTest(GetParam(), ConfigHelper::baseUdpListenerConfig()) {} + : BaseIntegrationTest(GetParam().ip_version, ConfigHelper::baseUdpListenerConfig()) {} + + // Returns (module_name, filter_name) for the given test variant. The C fakes have + // hard-coded module-level behavior (one module per behavior); the SDK languages have a + // single module exposing two filter names chosen via filter_name. + std::pair moduleAndFilter(const std::string& variant) { + if (GetParam().language == "c") { + // The C-fake module name encodes the behavior; the filter name is unused by the C + // fake but must be set to something. + if (variant == "passthrough") { + return {"udp_no_op", "test_filter"}; + } + return {"udp_stop_iteration", "test_filter"}; + } + // For Rust/Go we share a single module name and select via filter_name. + if (variant == "passthrough") { + return {"udp_integration_test", "test_filter"}; + } + return {"udp_integration_test", "stop_iteration"}; + } 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); + TestEnvironment::setEnvVar( + "ENVOY_DYNAMIC_MODULES_SEARCH_PATH", + TestEnvironment::substitute("{{ test_rundir }}/test/extensions/dynamic_modules/test_data/" + + GetParam().language), + 1); + TestEnvironment::setEnvVar("GODEBUG", "cgocheck=0", 1); } - void setup(const std::string& module_name = "udp_no_op") { + void setup(const std::string& variant = "passthrough") { FakeUpstreamConfig::UdpConfig config; setUdpFakeUpstream(config); + auto [module_name, filter_name] = moduleAndFilter(variant); + const std::string filter_config = fmt::format(R"EOF( name: envoy.filters.udp_listener.dynamic_modules typed_config: @@ -42,12 +71,12 @@ name: envoy.filters.udp_listener.dynamic_modules dynamic_module_config: name: "{}" do_not_close: true - filter_name: "test_filter" + filter_name: "{}" filter_config: "@type": type.googleapis.com/google.protobuf.StringValue value: "some_config" )EOF", - module_name); + module_name, filter_name); config_helper_.addListenerFilter(filter_config); @@ -69,19 +98,35 @@ name: envoy.filters.udp_listener.udp_proxy } }; -INSTANTIATE_TEST_SUITE_P(IpVersions, UdpDynamicModulesIntegrationTest, - testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), - TestUtility::ipTestParamsToString); +namespace { +std::vector getUdpTestParams() { + std::vector params; + for (const auto& language : {"c", "rust", "go"}) { + for (const auto ip : TestEnvironment::getIpVersionsForTest()) { + params.push_back({language, ip}); + } + } + return params; +} + +std::string udpParamName(const testing::TestParamInfo& info) { + return info.param.language + "_" + + (info.param.ip_version == Network::Address::IpVersion::v4 ? "IPv4" : "IPv6"); +} +} // namespace + +INSTANTIATE_TEST_SUITE_P(LanguagesAndIpVersions, UdpDynamicModulesIntegrationTest, + testing::ValuesIn(getUdpTestParams()), udpParamName); 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)); + const auto listener_address = *Network::Utility::resolveUrl(fmt::format( + "tcp://{}:{}", Network::Test::getLoopbackAddressUrlString(GetParam().ip_version), port)); std::string request = "hello"; - Network::Test::UdpSyncPeer client(GetParam()); + Network::Test::UdpSyncPeer client(GetParam().ip_version); client.write(request, *listener_address); Network::UdpRecvData request_datagram; @@ -90,14 +135,14 @@ TEST_P(UdpDynamicModulesIntegrationTest, BasicDataFlow) { } TEST_P(UdpDynamicModulesIntegrationTest, StopIteration) { - setup("udp_stop_iteration"); + setup("stop_iteration"); const uint32_t port = lookupPort("listener_0"); - const auto listener_address = *Network::Utility::resolveUrl( - fmt::format("tcp://{}:{}", Network::Test::getLoopbackAddressUrlString(GetParam()), port)); + const auto listener_address = *Network::Utility::resolveUrl(fmt::format( + "tcp://{}:{}", Network::Test::getLoopbackAddressUrlString(GetParam().ip_version), port)); std::string request = "should be blocked"; - Network::Test::UdpSyncPeer client(GetParam()); + Network::Test::UdpSyncPeer client(GetParam().ip_version); client.write(request, *listener_address); Network::UdpRecvData request_datagram; @@ -112,12 +157,12 @@ 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)); + const auto listener_address = *Network::Utility::resolveUrl(fmt::format( + "tcp://{}:{}", Network::Test::getLoopbackAddressUrlString(GetParam().ip_version), port)); // Use a conservative payload size to avoid platform-specific UDP limits. std::string large_request(512, 'x'); - Network::Test::UdpSyncPeer client(GetParam()); + Network::Test::UdpSyncPeer client(GetParam().ip_version); client.write(large_request, *listener_address); Network::UdpRecvData request_datagram; @@ -129,10 +174,10 @@ 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)); + const auto listener_address = *Network::Utility::resolveUrl(fmt::format( + "tcp://{}:{}", Network::Test::getLoopbackAddressUrlString(GetParam().ip_version), port)); - Network::Test::UdpSyncPeer client(GetParam()); + Network::Test::UdpSyncPeer client(GetParam().ip_version); // Send multiple datagrams. for (int i = 0; i < 5; i++) { diff --git a/test/extensions/load_balancing_policies/dynamic_modules/BUILD b/test/extensions/load_balancing_policies/dynamic_modules/BUILD index df8769440d786..c2f2772386a19 100644 --- a/test/extensions/load_balancing_policies/dynamic_modules/BUILD +++ b/test/extensions/load_balancing_policies/dynamic_modules/BUILD @@ -37,3 +37,25 @@ envoy_cc_test( "@envoy_api//envoy/extensions/load_balancing_policies/dynamic_modules/v3:pkg_cc_proto", ], ) + +envoy_cc_test( + name = "integration_test", + srcs = ["integration_test.cc"], + data = [ + "//test/extensions/dynamic_modules/test_data/go:load_balancer_integration_test", + "//test/extensions/dynamic_modules/test_data/rust:load_balancer_integration_test", + ], + env = {"GODEBUG": "cgocheck=0"}, + rbe_pool = "6gig", + deps = [ + # See //test/extensions/dynamic_modules/http:filter_test for why every test + # binary that loads a Go test_data .so depends on all_abi_impls. + "//source/extensions/dynamic_modules:all_abi_impls", + "//source/extensions/load_balancing_policies/dynamic_modules:config", + "//test/extensions/dynamic_modules:util", + "//test/integration:http_integration_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/load_balancing_policies/dynamic_modules/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/load_balancing_policies/dynamic_modules/integration_test.cc b/test/extensions/load_balancing_policies/dynamic_modules/integration_test.cc new file mode 100644 index 0000000000000..361aae5fd1de7 --- /dev/null +++ b/test/extensions/load_balancing_policies/dynamic_modules/integration_test.cc @@ -0,0 +1,95 @@ +#include "envoy/config/cluster/v3/cluster.pb.h" +#include "envoy/extensions/load_balancing_policies/dynamic_modules/v3/dynamic_modules.pb.h" + +#include "test/extensions/dynamic_modules/util.h" +#include "test/integration/http_integration.h" + +namespace Envoy { +namespace Extensions { +namespace LoadBalancingPolicies { +namespace DynamicModules { +namespace { + +// Parameterized over (language, ip_version). Each language ships a +// load_balancer_integration_test module with a "first_host_lb" LB that selects priority +// 0, index 0. The default test cluster has exactly one host so the trivial selection +// always succeeds; the test asserts requests route through Envoy with the +// dynamic-module LB attached, exercising the per-worker LB Create + ChooseHost path. +struct LoadBalancerIntegrationParam { + std::string language; + Network::Address::IpVersion ip_version; +}; + +class DynamicModuleLoadBalancerIntegrationTest + : public testing::TestWithParam, + public HttpIntegrationTest { +public: + DynamicModuleLoadBalancerIntegrationTest() + : HttpIntegrationTest(Http::CodecType::HTTP1, GetParam().ip_version) {} + + void SetUp() override { + TestEnvironment::setEnvVar( + "ENVOY_DYNAMIC_MODULES_SEARCH_PATH", + TestEnvironment::substitute("{{ test_rundir }}/test/extensions/dynamic_modules/test_data/" + + GetParam().language), + 1); + TestEnvironment::setEnvVar("GODEBUG", "cgocheck=0", 1); + } + + void initializeWithLoadBalancer() { + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* cluster = bootstrap.mutable_static_resources()->mutable_clusters(0); + cluster->set_lb_policy(envoy::config::cluster::v3::Cluster::LOAD_BALANCING_POLICY_CONFIG); + + auto* policy = cluster->mutable_load_balancing_policy()->add_policies(); + policy->mutable_typed_extension_config()->set_name( + "envoy.load_balancing_policies.dynamic_modules"); + + envoy::extensions::load_balancing_policies::dynamic_modules::v3:: + DynamicModulesLoadBalancerConfig lb_config; + lb_config.mutable_dynamic_module_config()->set_name("load_balancer_integration_test"); + lb_config.set_lb_policy_name("first_host_lb"); + policy->mutable_typed_extension_config()->mutable_typed_config()->PackFrom(lb_config); + }); + initialize(); + } +}; + +namespace { +std::vector getLoadBalancerTestParams() { + std::vector params; + for (const auto& language : {"rust", "go"}) { + for (const auto ip : TestEnvironment::getIpVersionsForTest()) { + params.push_back({language, ip}); + } + } + return params; +} + +std::string lbParamName(const testing::TestParamInfo& info) { + return info.param.language + "_" + + (info.param.ip_version == Network::Address::IpVersion::v4 ? "IPv4" : "IPv6"); +} +} // namespace + +INSTANTIATE_TEST_SUITE_P(LanguagesAndIpVersions, DynamicModuleLoadBalancerIntegrationTest, + testing::ValuesIn(getLoadBalancerTestParams()), lbParamName); + +TEST_P(DynamicModuleLoadBalancerIntegrationTest, RoutesViaDynamicModuleLB) { + initializeWithLoadBalancer(); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/test"}, {":scheme", "http"}, {":authority", "host"}}; + + auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); +} + +} // namespace +} // namespace DynamicModules +} // namespace LoadBalancingPolicies +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/matching/input_matchers/dynamic_modules/BUILD b/test/extensions/matching/input_matchers/dynamic_modules/BUILD index ae7d1e74b4f25..91bf01e02c3e4 100644 --- a/test/extensions/matching/input_matchers/dynamic_modules/BUILD +++ b/test/extensions/matching/input_matchers/dynamic_modules/BUILD @@ -56,8 +56,14 @@ envoy_cc_test( srcs = ["integration_test.cc"], data = [ "//test/extensions/dynamic_modules/test_data/c:matcher_check_headers", + "//test/extensions/dynamic_modules/test_data/go:matcher_integration_test", + "//test/extensions/dynamic_modules/test_data/rust:matcher_integration_test", ], + env = {"GODEBUG": "cgocheck=0"}, deps = [ + # See //test/extensions/dynamic_modules/http:filter_test for why every test + # binary that loads a Go test_data .so depends on all_abi_impls. + "//source/extensions/dynamic_modules:all_abi_impls", "//source/extensions/matching/http/dynamic_modules:data_input_lib", "//source/extensions/matching/input_matchers/dynamic_modules:config", "//test/extensions/dynamic_modules:util", diff --git a/test/extensions/matching/input_matchers/dynamic_modules/integration_test.cc b/test/extensions/matching/input_matchers/dynamic_modules/integration_test.cc index 88acd9dba1665..abe3231d9f212 100644 --- a/test/extensions/matching/input_matchers/dynamic_modules/integration_test.cc +++ b/test/extensions/matching/input_matchers/dynamic_modules/integration_test.cc @@ -5,30 +5,50 @@ #include "test/integration/http_integration.h" namespace Envoy { +namespace { + +// Parameterized over (language, ip_version). The C variant uses the matcher_check_headers +// fake; rust and go each ship a matcher_integration_test module that registers the same +// "header_check" matcher (header name from config; matches when value equals "match"). +struct MatcherIntegrationParam { + std::string language; + Network::Address::IpVersion ip_version; +}; -class DynamicModuleMatcherIntegrationTest - : public testing::TestWithParam, - public HttpIntegrationTest { +class DynamicModuleMatcherIntegrationTest : public testing::TestWithParam, + public HttpIntegrationTest { public: - DynamicModuleMatcherIntegrationTest() : HttpIntegrationTest(Http::CodecType::HTTP2, GetParam()) { + DynamicModuleMatcherIntegrationTest() + : HttpIntegrationTest(Http::CodecType::HTTP2, GetParam().ip_version) { setUpstreamProtocol(Http::CodecType::HTTP2); } + // Returns (module_name, matcher_name) for the active language. + std::pair moduleAndMatcher() { + if (GetParam().language == "c") { + return {"matcher_check_headers", "header_check"}; + } + return {"matcher_integration_test", "header_check"}; + } + void initializeWithMatcher() { - std::string shared_object_path = - Extensions::DynamicModules::testSharedObjectPath("matcher_check_headers", "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); + TestEnvironment::setEnvVar( + "ENVOY_DYNAMIC_MODULES_SEARCH_PATH", + TestEnvironment::substitute("{{ test_rundir }}/test/extensions/dynamic_modules/test_data/" + + GetParam().language), + 1); + TestEnvironment::setEnvVar("GODEBUG", "cgocheck=0", 1); + + auto [module_name, matcher_name] = moduleAndMatcher(); config_helper_.addConfigModifier( - [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& - hcm) { + [module_name, matcher_name]( + envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) { auto* route_config = hcm.mutable_route_config(); route_config->clear_virtual_hosts(); - // Use the matcher tree API in the virtual host. - constexpr auto vhost_yaml = R"EOF( + const std::string vhost_yaml = fmt::format(R"EOF( name: matcher_vhost domains: ["*"] matcher: @@ -45,9 +65,9 @@ domains: ["*"] typed_config: "@type": type.googleapis.com/envoy.extensions.matching.input_matchers.dynamic_modules.v3.DynamicModuleMatcher dynamic_module_config: - name: matcher_check_headers + name: {} do_not_close: true - matcher_name: header_check + matcher_name: {} matcher_config: "@type": type.googleapis.com/google.protobuf.StringValue value: x-match-header @@ -60,7 +80,8 @@ domains: ["*"] prefix: / route: cluster: cluster_0 -)EOF"; +)EOF", + module_name, matcher_name); envoy::config::route::v3::VirtualHost virtual_host; TestUtility::loadFromYaml(vhost_yaml, virtual_host); @@ -71,9 +92,25 @@ domains: ["*"] } }; -INSTANTIATE_TEST_SUITE_P(IpVersions, DynamicModuleMatcherIntegrationTest, - testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), - TestUtility::ipTestParamsToString); +namespace { +std::vector getMatcherTestParams() { + std::vector params; + for (const auto& language : {"c", "rust", "go"}) { + for (const auto ip : TestEnvironment::getIpVersionsForTest()) { + params.push_back({language, ip}); + } + } + return params; +} + +std::string matcherParamName(const testing::TestParamInfo& info) { + return info.param.language + "_" + + (info.param.ip_version == Network::Address::IpVersion::v4 ? "IPv4" : "IPv6"); +} +} // namespace + +INSTANTIATE_TEST_SUITE_P(LanguagesAndIpVersions, DynamicModuleMatcherIntegrationTest, + testing::ValuesIn(getMatcherTestParams()), matcherParamName); TEST_P(DynamicModuleMatcherIntegrationTest, MatchingHeaderRoutes) { initializeWithMatcher(); @@ -98,7 +135,6 @@ TEST_P(DynamicModuleMatcherIntegrationTest, NonMatchingHeaderValue) { codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); - // Request with wrong header value should not match and return 404. Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, {":path", "/test"}, {":scheme", "http"}, @@ -115,7 +151,6 @@ TEST_P(DynamicModuleMatcherIntegrationTest, MissingHeader) { codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); - // Request without the header should not match and return 404. Http::TestRequestHeaderMapImpl request_headers{ {":method", "GET"}, {":path", "/test"}, {":scheme", "http"}, {":authority", "host"}}; @@ -124,4 +159,5 @@ TEST_P(DynamicModuleMatcherIntegrationTest, MissingHeader) { EXPECT_EQ("404", response->headers().Status()->value().getStringView()); } +} // namespace } // namespace Envoy diff --git a/test/extensions/tracers/dynamic_modules/BUILD b/test/extensions/tracers/dynamic_modules/BUILD index 4bd3b256e1a8f..83ae1d2f715fc 100644 --- a/test/extensions/tracers/dynamic_modules/BUILD +++ b/test/extensions/tracers/dynamic_modules/BUILD @@ -60,3 +60,23 @@ envoy_cc_test( "//test/test_common:utility_lib", ], ) + +envoy_cc_test( + name = "integration_test", + srcs = ["integration_test.cc"], + data = [ + "//test/extensions/dynamic_modules/test_data/go:tracer_integration_test", + "//test/extensions/dynamic_modules/test_data/rust:tracer_integration_test", + ], + env = {"GODEBUG": "cgocheck=0"}, + deps = [ + # See //test/extensions/dynamic_modules/http:filter_test for why every test + # binary that loads a Go test_data .so depends on all_abi_impls. + "//source/extensions/dynamic_modules:all_abi_impls", + "//source/extensions/tracers/dynamic_modules:config", + "//test/extensions/dynamic_modules:util", + "//test/integration:http_integration_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/tracers/dynamic_modules/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/tracers/dynamic_modules/integration_test.cc b/test/extensions/tracers/dynamic_modules/integration_test.cc new file mode 100644 index 0000000000000..5892cb5d5bd10 --- /dev/null +++ b/test/extensions/tracers/dynamic_modules/integration_test.cc @@ -0,0 +1,104 @@ +#include "envoy/extensions/tracers/dynamic_modules/v3/dynamic_modules.pb.h" + +#include "test/extensions/dynamic_modules/util.h" +#include "test/integration/http_integration.h" + +namespace Envoy { +namespace { + +// Parameterized over (language, ip_version). Each language ships a tracer_integration_test +// module exposing a "test_tracer" tracer that returns recording spans. The test asserts +// that requests flow through HCM with the dynamic-module tracer attached and don't +// crash — i.e., span creation, tag/log dispatch, and span finish all work via cgo. +struct TracerIntegrationParam { + std::string language; + Network::Address::IpVersion ip_version; +}; + +class DynamicModuleTracerIntegrationTest : public testing::TestWithParam, + public HttpIntegrationTest { +public: + DynamicModuleTracerIntegrationTest() + : HttpIntegrationTest(Http::CodecType::HTTP1, GetParam().ip_version) {} + + void SetUp() override { + TestEnvironment::setEnvVar( + "ENVOY_DYNAMIC_MODULES_SEARCH_PATH", + TestEnvironment::substitute("{{ test_rundir }}/test/extensions/dynamic_modules/test_data/" + + GetParam().language), + 1); + TestEnvironment::setEnvVar("GODEBUG", "cgocheck=0", 1); + } + + void initializeWithTracer() { + config_helper_.addConfigModifier( + [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) { + auto* tracing = hcm.mutable_tracing(); + // Force sampling so spans are actually created. + tracing->mutable_random_sampling()->set_value(100.0); + + auto* provider = tracing->mutable_provider(); + provider->set_name("envoy.tracers.dynamic_modules"); + + envoy::extensions::tracers::dynamic_modules::v3::DynamicModuleTracer tracer_proto; + tracer_proto.mutable_dynamic_module_config()->set_name("tracer_integration_test"); + tracer_proto.set_tracer_name("test_tracer"); + Protobuf::StringValue value; + value.set_value("test_config"); + tracer_proto.mutable_tracer_config()->PackFrom(value); + provider->mutable_typed_config()->PackFrom(tracer_proto); + }); + initialize(); + } +}; + +namespace { +std::vector getTracerTestParams() { + std::vector params; + for (const auto& language : {"rust", "go"}) { + for (const auto ip : TestEnvironment::getIpVersionsForTest()) { + params.push_back({language, ip}); + } + } + return params; +} + +std::string tracerParamName(const testing::TestParamInfo& info) { + return info.param.language + "_" + + (info.param.ip_version == Network::Address::IpVersion::v4 ? "IPv4" : "IPv6"); +} +} // namespace + +INSTANTIATE_TEST_SUITE_P(LanguagesAndIpVersions, DynamicModuleTracerIntegrationTest, + testing::ValuesIn(getTracerTestParams()), tracerParamName); + +TEST_P(DynamicModuleTracerIntegrationTest, BasicTracingFlow) { + initializeWithTracer(); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/test"}, {":scheme", "http"}, {":authority", "host"}}; + + auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); +} + +TEST_P(DynamicModuleTracerIntegrationTest, MultipleRequests) { + initializeWithTracer(); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + + for (int i = 0; i < 3; ++i) { + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/test"}, {":scheme", "http"}, {":authority", "host"}}; + + auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); + } +} + +} // namespace +} // namespace Envoy diff --git a/test/extensions/transport_sockets/tls/cert_validator/dynamic_modules/BUILD b/test/extensions/transport_sockets/tls/cert_validator/dynamic_modules/BUILD index a2fe3cd7eef66..c53d91defe3b1 100644 --- a/test/extensions/transport_sockets/tls/cert_validator/dynamic_modules/BUILD +++ b/test/extensions/transport_sockets/tls/cert_validator/dynamic_modules/BUILD @@ -8,6 +8,34 @@ licenses(["notice"]) # Apache 2 envoy_package() +envoy_cc_test( + name = "dynamic_modules_cert_validator_integration_test", + size = "large", + srcs = ["dynamic_modules_cert_validator_integration_test.cc"], + data = [ + "//test/common/tls/test_data:certs", + "//test/config/integration/certs", + "//test/extensions/dynamic_modules/test_data/c:cert_validator_filter_state", + "//test/extensions/dynamic_modules/test_data/c:cert_validator_no_op", + "//test/extensions/dynamic_modules/test_data/go:cert_validator_test", + "//test/extensions/dynamic_modules/test_data/rust:cert_validator_test", + ], + env = {"GODEBUG": "cgocheck=0"}, + rbe_pool = "6gig", + deps = [ + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/transport_sockets/tls/cert_validator/dynamic_modules/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/transport_sockets/tls/v3:pkg_cc_proto", + # See //test/extensions/dynamic_modules/http:filter_test for why every test + # binary that loads a Go test_data .so depends on all_abi_impls. + "//source/extensions/dynamic_modules:all_abi_impls", + "//source/extensions/transport_sockets/tls/cert_validator/dynamic_modules:config", + "//test/integration:http_integration_lib", + "//test/test_common:environment_lib", + "//test/test_common:utility_lib", + ], +) + envoy_cc_test( name = "dynamic_modules_cert_validator_test", srcs = ["dynamic_modules_cert_validator_test.cc"], @@ -17,12 +45,23 @@ envoy_cc_test( "//test/extensions/dynamic_modules/test_data/c:cert_validator_empty_digest", "//test/extensions/dynamic_modules/test_data/c:cert_validator_fail", "//test/extensions/dynamic_modules/test_data/c:cert_validator_filter_state", + "//test/extensions/dynamic_modules/test_data/c:cert_validator_missing_config_destroy", + "//test/extensions/dynamic_modules/test_data/c:cert_validator_missing_do_verify", + "//test/extensions/dynamic_modules/test_data/c:cert_validator_missing_get_verify_mode", + "//test/extensions/dynamic_modules/test_data/c:cert_validator_missing_update_digest", "//test/extensions/dynamic_modules/test_data/c:cert_validator_no_client_cert", "//test/extensions/dynamic_modules/test_data/c:cert_validator_no_op", "//test/extensions/dynamic_modules/test_data/c:cert_validator_not_validated", + "//test/extensions/dynamic_modules/test_data/c:cert_validator_unknown_status", "//test/extensions/dynamic_modules/test_data/c:no_op", ], rbe_pool = "6gig", + # Intentionally does NOT depend on //source/extensions/dynamic_modules:all_abi_impls. + # That meta-target pulls in dynamic_modules:abi_impl, which carries the WEAK fallbacks + # for the cert-validator host callbacks. With both weak and strong (from :config) in + # the same link unit, direct C++ calls in this test resolve to the WEAK version, leaving + # the strong code paths in config.cc uncovered. Cross-language tests that need + # all_abi_impls (because they load Go .so files) live in the language_test target below. deps = [ "//source/common/router:string_accessor_lib", "//source/extensions/transport_sockets/tls/cert_validator/dynamic_modules:config", @@ -35,3 +74,25 @@ envoy_cc_test( "//test/test_common:utility_lib", ], ) + +envoy_cc_test( + name = "dynamic_modules_cert_validator_language_test", + srcs = ["dynamic_modules_cert_validator_language_test.cc"], + data = [ + "//test/common/tls/test_data:certs", + "//test/extensions/dynamic_modules/test_data/go:cert_validator_test", + "//test/extensions/dynamic_modules/test_data/rust:cert_validator_test", + ], + env = {"GODEBUG": "cgocheck=0"}, + rbe_pool = "6gig", + deps = [ + # See //test/extensions/dynamic_modules/http:filter_test for why every test + # binary that loads a Go test_data .so depends on all_abi_impls. + "//source/extensions/dynamic_modules:all_abi_impls", + "//source/extensions/transport_sockets/tls/cert_validator/dynamic_modules:config", + "//test/common/stats:stat_test_utility_lib", + "//test/common/tls:ssl_test_utils", + "//test/test_common:environment_lib", + "//test/test_common:utility_lib", + ], +) diff --git a/test/extensions/transport_sockets/tls/cert_validator/dynamic_modules/dynamic_modules_cert_validator_integration_test.cc b/test/extensions/transport_sockets/tls/cert_validator/dynamic_modules/dynamic_modules_cert_validator_integration_test.cc new file mode 100644 index 0000000000000..c0a2ec2612075 --- /dev/null +++ b/test/extensions/transport_sockets/tls/cert_validator/dynamic_modules/dynamic_modules_cert_validator_integration_test.cc @@ -0,0 +1,228 @@ +// Integration tests for the dynamic_modules cert validator. These tests run a real Envoy +// server with a TLS listener whose validator is provided by a dynamically loaded module, and +// drive a TLS handshake from a client cert. Unlike the unit test in this same directory, the +// validator's extern "C" callbacks (set_error_details, set_filter_state, get_filter_state) +// are reached through a dlopen'd .so calling into the host — i.e. through the dynamic linker +// rather than a direct C++ call. That dynamic-linker path is what exercises the strong +// definitions in config.cc end-to-end. +// +// Two test classes: +// +// * DynamicModulesCertValidatorClientIntegrationTest mounts the validator on the *client* +// side (validating the server's certificate). The server presents its certificate as +// part of the TLS handshake, so the client validator's do_verify_cert_chain runs without +// extra plumbing. This is the simpler wiring and covers the doVerifyCertChain code path. +// +// * DynamicModulesCertValidatorServerIntegrationTest mounts the validator on the *server* +// side. This covers addClientValidationContext (which builds the server's TLS +// CertificateRequest CA list from the configured trusted_ca) and exercises the three +// extern "C" filter-state / error-details callbacks, which the cert_validator_filter_state +// module invokes inside do_verify_cert_chain. + +#include "envoy/config/core/v3/extension.pb.h" +#include "envoy/extensions/transport_sockets/tls/cert_validator/dynamic_modules/v3/dynamic_modules.pb.h" +#include "envoy/extensions/transport_sockets/tls/v3/tls.pb.h" + +#include "test/integration/http_integration.h" +#include "test/integration/ssl_utility.h" +#include "test/test_common/environment.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { +namespace DynamicModules { +namespace { + +struct CertValidatorIntegrationParam { + std::string language; + Network::Address::IpVersion ip_version; +}; + +// ============================================================================= +// Client-side validator: validates the server's certificate. The C/Go/Rust modules +// always return Successful, so the handshake completes if our doVerifyCertChain +// strong code path ran end-to-end. +// ============================================================================= + +class DynamicModulesCertValidatorClientIntegrationTest + : public testing::TestWithParam, + public HttpIntegrationTest { +public: + DynamicModulesCertValidatorClientIntegrationTest() + : HttpIntegrationTest(Http::CodecType::HTTP1, GetParam().ip_version) {} + + void SetUp() override { + TestEnvironment::setEnvVar( + "ENVOY_DYNAMIC_MODULES_SEARCH_PATH", + TestEnvironment::substitute("{{ test_rundir }}/test/extensions/dynamic_modules/test_data/" + + GetParam().language), + 1); + TestEnvironment::setEnvVar("GODEBUG", "cgocheck=0", 1); + } + + void TearDown() override { + HttpIntegrationTest::cleanupUpstreamAndDownstream(); + codec_client_.reset(); + } + + void initialize() override { + config_helper_.addSslConfig( + ConfigHelper::ServerSslOptions().setRsaCert(true).setTlsV13(true).setRsaCertOcspStaple( + false)); + HttpIntegrationTest::initialize(); + } + + Network::ClientConnectionPtr makeSslClient() { + validator_config_storage_ = std::make_unique(); + const std::string module_name = + (GetParam().language == "c") ? "cert_validator_no_op" : "cert_validator_test"; + TestUtility::loadFromYaml(fmt::format(R"EOF( +name: envoy.tls.cert_validator.dynamic_modules +typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.cert_validator.dynamic_modules.v3.DynamicModuleCertValidatorConfig + dynamic_module_config: + name: {} + validator_name: test +)EOF", + module_name), + *validator_config_storage_); + + Ssl::ClientSslTransportOptions options; + options.setCustomCertValidatorConfig(validator_config_storage_.get()); + + Network::Address::InstanceConstSharedPtr address = + Ssl::getSslAddress(version_, lookupPort("http")); + auto factory = Ssl::createClientSslTransportSocketFactory(options, context_manager_, *api_); + return dispatcher_->createClientConnection(address, Network::Address::InstanceConstSharedPtr(), + factory->createTransportSocket(nullptr, nullptr), + nullptr, nullptr); + } + + std::unique_ptr validator_config_storage_; +}; + +namespace { +std::vector getTestParams() { + std::vector params; + for (const auto& language : {"c", "go", "rust"}) { + for (const auto ip : TestEnvironment::getIpVersionsForTest()) { + params.push_back({language, ip}); + } + } + return params; +} + +std::string testParamName(const testing::TestParamInfo& info) { + return info.param.language + "_" + + (info.param.ip_version == Network::Address::IpVersion::v4 ? "IPv4" : "IPv6"); +} +} // namespace + +INSTANTIATE_TEST_SUITE_P(LanguagesAndIpVersions, DynamicModulesCertValidatorClientIntegrationTest, + testing::ValuesIn(getTestParams()), testParamName); + +TEST_P(DynamicModulesCertValidatorClientIntegrationTest, ValidatorAccepts) { + ConnectionCreationFunction creator = [&]() -> Network::ClientConnectionPtr { + return makeSslClient(); + }; + testRouterRequestAndResponseWithBody(1024, 512, false, false, &creator); +} + +// ============================================================================= +// Server-side validator: validates the client's certificate. This exercises +// addClientValidationContext (which builds the CA list from trusted_ca) and the +// three extern "C" callbacks invoked from inside the filter_state module's +// do_verify_cert_chain. The validator returns Successful on the round-trip. +// ============================================================================= + +class DynamicModulesCertValidatorServerIntegrationTest + : public testing::TestWithParam, + public HttpIntegrationTest { +public: + DynamicModulesCertValidatorServerIntegrationTest() + : HttpIntegrationTest(Http::CodecType::HTTP1, GetParam()) {} + + void SetUp() override { + TestEnvironment::setEnvVar("ENVOY_DYNAMIC_MODULES_SEARCH_PATH", + TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/dynamic_modules/test_data/c"), + 1); + } + + void TearDown() override { + HttpIntegrationTest::cleanupUpstreamAndDownstream(); + codec_client_.reset(); + } + + void initialize() override { + auto* validator_config = new envoy::config::core::v3::TypedExtensionConfig(); + TestUtility::loadFromYaml(R"EOF( +name: envoy.tls.cert_validator.dynamic_modules +typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.cert_validator.dynamic_modules.v3.DynamicModuleCertValidatorConfig + dynamic_module_config: + name: cert_validator_filter_state + validator_name: test +)EOF", + *validator_config); + + config_helper_.addSslConfig(ConfigHelper::ServerSslOptions() + .setRsaCert(true) + .setTlsV13(true) + .setRsaCertOcspStaple(false) + .setCustomValidatorConfig(validator_config)); + + // ConfigHelper::initializeTls treats custom_validator_config and trusted_ca as + // either-or — it skips trusted_ca when a custom validator is set. We add it back via + // a config modifier so the dynamic_modules validator's addClientValidationContext can + // populate the server's TLS CertificateRequest CA list. Without that list, BoringSSL + // clients refuse to send a certificate. Chain validation is still delegated to the + // dynamic module via the custom verify callback; the CA list is only an advertisement. + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* filter_chain = + bootstrap.mutable_static_resources()->mutable_listeners(0)->mutable_filter_chains(0); + auto* transport_socket = filter_chain->mutable_transport_socket(); + envoy::extensions::transport_sockets::tls::v3::DownstreamTlsContext tls_context; + transport_socket->mutable_typed_config()->UnpackTo(&tls_context); + tls_context.mutable_common_tls_context() + ->mutable_validation_context() + ->mutable_trusted_ca() + ->set_filename(TestEnvironment::runfilesPath("test/config/integration/certs/cacert.pem")); + transport_socket->mutable_typed_config()->PackFrom(tls_context); + }); + + HttpIntegrationTest::initialize(); + } + + Network::ClientConnectionPtr makeSslClient() { + Network::Address::InstanceConstSharedPtr address = + Ssl::getSslAddress(version_, lookupPort("http")); + auto factory = Ssl::createClientSslTransportSocketFactory(Ssl::ClientSslTransportOptions(), + context_manager_, *api_); + return dispatcher_->createClientConnection(address, Network::Address::InstanceConstSharedPtr(), + factory->createTransportSocket(nullptr, nullptr), + nullptr, nullptr); + } +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, DynamicModulesCertValidatorServerIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +TEST_P(DynamicModulesCertValidatorServerIntegrationTest, FilterStateCallbacksRoundTrip) { + ConnectionCreationFunction creator = [&]() -> Network::ClientConnectionPtr { + return makeSslClient(); + }; + testRouterRequestAndResponseWithBody(1024, 512, false, false, &creator); +} + +} // namespace +} // namespace DynamicModules +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/transport_sockets/tls/cert_validator/dynamic_modules/dynamic_modules_cert_validator_language_test.cc b/test/extensions/transport_sockets/tls/cert_validator/dynamic_modules/dynamic_modules_cert_validator_language_test.cc new file mode 100644 index 0000000000000..7e6e4cd970049 --- /dev/null +++ b/test/extensions/transport_sockets/tls/cert_validator/dynamic_modules/dynamic_modules_cert_validator_language_test.cc @@ -0,0 +1,89 @@ +// Cross-language tests for the dynamic_modules cert validator. Parameterized over the SDK +// language. Each language ships a "cert_validator_test" module exposing a "test" cert +// validator that always returns Successful. The C counterpart (cert_validator_no_op) is +// exercised by the TEST_F suite in dynamic_modules_cert_validator_test.cc. +// +// This is split from the unit test file because loading Go test_data .so files at runtime +// requires the test binary to depend on //source/extensions/dynamic_modules:all_abi_impls +// to satisfy unresolved Go SDK symbols. That meta-dep also pulls in the WEAK fallbacks for +// cert-validator host callbacks (set_filter_state / get_filter_state / set_error_details). +// When both weak and strong are in the same link unit, the linker resolves direct C++ calls +// to the WEAK version, leaving the strong code paths in config.cc uncovered. Keeping these +// cross-language tests in their own binary lets the unit test binary depend only on +// //source/extensions/transport_sockets/tls/cert_validator/dynamic_modules:config, so direct +// C++ calls there resolve to the strong version and get coverage. + +#include "source/common/tls/cert_validator/cert_validator.h" +#include "source/common/tls/stats.h" +#include "source/extensions/transport_sockets/tls/cert_validator/dynamic_modules/config.h" + +#include "test/common/stats/stat_test_utility.h" +#include "test/common/tls/ssl_test_utility.h" +#include "test/test_common/environment.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" +#include "openssl/ssl.h" + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { +namespace DynamicModules { +namespace { + +class DynamicModuleCertValidatorLanguageTest : public testing::TestWithParam { +protected: + DynamicModuleCertValidatorLanguageTest() + : api_(Api::createApiForTest()), stats_(generateSslStats(*store_.rootScope())) { + TestEnvironment::setEnvVar( + "ENVOY_DYNAMIC_MODULES_SEARCH_PATH", + TestEnvironment::substitute("{{ test_rundir }}/test/extensions/dynamic_modules/test_data/" + + GetParam()), + 1); + TestEnvironment::setEnvVar("GODEBUG", "cgocheck=0", 1); + } + + Api::ApiPtr api_; + Stats::TestUtil::TestStore store_; + SslStats stats_; +}; + +INSTANTIATE_TEST_SUITE_P(SdkLanguages, DynamicModuleCertValidatorLanguageTest, + testing::Values("rust", "go")); + +TEST_P(DynamicModuleCertValidatorLanguageTest, ConfigNewSuccess) { + auto module = Envoy::Extensions::DynamicModules::newDynamicModuleByName("cert_validator_test", + false, false); + ASSERT_TRUE(module.ok()); + auto config_or_error = newDynamicModuleCertValidatorConfig("test", "", std::move(module.value())); + ASSERT_TRUE(config_or_error.ok()); + EXPECT_NE(config_or_error.value()->in_module_config_, nullptr); +} + +TEST_P(DynamicModuleCertValidatorLanguageTest, VerifyCertChainSuccess) { + auto module = Envoy::Extensions::DynamicModules::newDynamicModuleByName("cert_validator_test", + false, false); + ASSERT_TRUE(module.ok()); + auto config_or_error = newDynamicModuleCertValidatorConfig("test", "", std::move(module.value())); + ASSERT_TRUE(config_or_error.ok()); + + DynamicModuleCertValidator validator(config_or_error.value(), stats_); + + bssl::UniquePtr cert = readCertFromFile( + TestEnvironment::substitute("{{ test_rundir }}/test/common/tls/test_data/san_dns_cert.pem")); + bssl::UniquePtr cert_chain(sk_X509_new_null()); + sk_X509_push(cert_chain.get(), cert.release()); + + CSmartPtr ssl_ctx(SSL_CTX_new(TLS_method())); + auto results = validator.doVerifyCertChain(*cert_chain, nullptr, nullptr, *ssl_ctx, {}, false, + "example.com"); + EXPECT_EQ(ValidationResults::ValidationStatus::Successful, results.status); +} + +} // namespace +} // namespace DynamicModules +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/transport_sockets/tls/cert_validator/dynamic_modules/dynamic_modules_cert_validator_test.cc b/test/extensions/transport_sockets/tls/cert_validator/dynamic_modules/dynamic_modules_cert_validator_test.cc index 939bc31b334f3..124fe19472c90 100644 --- a/test/extensions/transport_sockets/tls/cert_validator/dynamic_modules/dynamic_modules_cert_validator_test.cc +++ b/test/extensions/transport_sockets/tls/cert_validator/dynamic_modules/dynamic_modules_cert_validator_test.cc @@ -75,11 +75,36 @@ TEST_F(DynamicModuleCertValidatorTest, ConfigNewModuleNotFound) { } TEST_F(DynamicModuleCertValidatorTest, ConfigNewMissingSymbol) { - // The "no_op" module does not implement cert validator functions. + // The "no_op" module does not implement cert validator functions, so the + // very first symbol lookup (config_new) fails. auto config_or_error = createConfig("no_op"); ASSERT_FALSE(config_or_error.ok()); } +// The next four tests cover the per-symbol fallback paths in +// newDynamicModuleCertValidatorConfig. Each fake defines all cert-validator +// symbols except one, forcing exactly that resolution to fail. + +TEST_F(DynamicModuleCertValidatorTest, ConfigNewMissingConfigDestroy) { + auto config_or_error = createConfig("cert_validator_missing_config_destroy"); + ASSERT_FALSE(config_or_error.ok()); +} + +TEST_F(DynamicModuleCertValidatorTest, ConfigNewMissingDoVerify) { + auto config_or_error = createConfig("cert_validator_missing_do_verify"); + ASSERT_FALSE(config_or_error.ok()); +} + +TEST_F(DynamicModuleCertValidatorTest, ConfigNewMissingGetVerifyMode) { + auto config_or_error = createConfig("cert_validator_missing_get_verify_mode"); + ASSERT_FALSE(config_or_error.ok()); +} + +TEST_F(DynamicModuleCertValidatorTest, ConfigNewMissingUpdateDigest) { + auto config_or_error = createConfig("cert_validator_missing_update_digest"); + ASSERT_FALSE(config_or_error.ok()); +} + // ============================================================================= // doVerifyCertChain tests. // ============================================================================= @@ -209,6 +234,28 @@ TEST_F(DynamicModuleCertValidatorTest, VerifyCertChainNotValidatedDefaultStatus) EXPECT_EQ(Envoy::Ssl::ClientValidationStatus::NotValidated, results.detailed_status); } +// Exercises the default branch of the detailed_status switch in doVerifyCertChain. +// The fake returns an out-of-range enum value, which the validator must translate to +// NotValidated. +TEST_F(DynamicModuleCertValidatorTest, VerifyCertChainUnknownDetailedStatus) { + auto config_or_error = createConfig("cert_validator_unknown_status"); + ASSERT_TRUE(config_or_error.ok()); + + DynamicModuleCertValidator validator(config_or_error.value(), stats_); + + bssl::UniquePtr cert = readCertFromFile( + TestEnvironment::substitute("{{ test_rundir }}/test/common/tls/test_data/san_dns_cert.pem")); + + bssl::UniquePtr cert_chain(sk_X509_new_null()); + sk_X509_push(cert_chain.get(), cert.release()); + + CSmartPtr ssl_ctx(SSL_CTX_new(TLS_method())); + auto results = validator.doVerifyCertChain(*cert_chain, nullptr, nullptr, *ssl_ctx, {}, false, + "example.com"); + EXPECT_EQ(ValidationResults::ValidationStatus::Successful, results.status); + EXPECT_EQ(Envoy::Ssl::ClientValidationStatus::NotValidated, results.detailed_status); +} + TEST_F(DynamicModuleCertValidatorTest, VerifyCertChainMultipleCerts) { auto config_or_error = createConfig("cert_validator_no_op"); ASSERT_TRUE(config_or_error.ok()); @@ -573,6 +620,33 @@ TEST_F(DynamicModuleCertValidatorTest, FilterStateSetNullValue) { config->current_callbacks_ = nullptr; } +// Directly exercises the set_error_details callback to cover its no-op branches when the +// caller passes a null buffer or zero length. The cert_validator_fail fake covers the +// happy path; this test covers the early-return paths. +TEST_F(DynamicModuleCertValidatorTest, SetErrorDetailsNullAndEmptyAreNoOps) { + auto config_or_error = createConfig("cert_validator_no_op"); + ASSERT_TRUE(config_or_error.ok()); + auto& config = config_or_error.value(); + + // Null pointer: should leave last_error_details_ unset. + envoy_dynamic_module_callback_cert_validator_set_error_details(static_cast(config.get()), + {nullptr, 0}); + EXPECT_FALSE(config->last_error_details_.has_value()); + + // Non-null pointer with zero length: still treated as a no-op. + const char placeholder = 'x'; + envoy_dynamic_module_callback_cert_validator_set_error_details( + static_cast(config.get()), {const_cast(&placeholder), 0}); + EXPECT_FALSE(config->last_error_details_.has_value()); + + // Sanity: non-empty buffer goes through and stores the value. + const std::string err = "module says no"; + envoy_dynamic_module_callback_cert_validator_set_error_details( + static_cast(config.get()), {const_cast(err.data()), err.size()}); + ASSERT_TRUE(config->last_error_details_.has_value()); + EXPECT_EQ(err, config->last_error_details_.value()); +} + TEST_F(DynamicModuleCertValidatorTest, FilterStateGetNullKey) { auto config_or_error = createConfig("cert_validator_no_op"); ASSERT_TRUE(config_or_error.ok()); diff --git a/test/extensions/upstreams/http/dynamic_modules/BUILD b/test/extensions/upstreams/http/dynamic_modules/BUILD index f007b47e65276..a6b66e7ffcde6 100644 --- a/test/extensions/upstreams/http/dynamic_modules/BUILD +++ b/test/extensions/upstreams/http/dynamic_modules/BUILD @@ -19,6 +19,7 @@ envoy_cc_test( "//test/extensions/dynamic_modules/test_data/c:upstream_bridge_headers_end_stream_no_body", "//test/extensions/dynamic_modules/test_data/c:upstream_bridge_new_fail", "//test/extensions/dynamic_modules/test_data/c:upstream_bridge_no_op", + "//test/extensions/dynamic_modules/test_data/c:upstream_bridge_send_upstream", "//test/extensions/dynamic_modules/test_data/c:upstream_bridge_stop_and_buffer", ], rbe_pool = "6gig", @@ -43,14 +44,19 @@ envoy_cc_test( size = "large", srcs = ["integration_test.cc"], data = [ + "//test/extensions/dynamic_modules/test_data/go:upstream_http_tcp_bridge", "//test/extensions/dynamic_modules/test_data/rust:upstream_http_tcp_bridge", ], + env = {"GODEBUG": "cgocheck=0"}, rbe_pool = "2core", shard_count = 4, tags = [ "cpu:3", ], deps = [ + # See //test/extensions/dynamic_modules/http:filter_test for why every test + # binary that loads a Go test_data .so depends on all_abi_impls. + "//source/extensions/dynamic_modules:all_abi_impls", "//source/extensions/upstreams/http/dynamic_modules:config", "//test/integration:http_integration_lib", "//test/integration:http_protocol_integration_lib", diff --git a/test/extensions/upstreams/http/dynamic_modules/integration_test.cc b/test/extensions/upstreams/http/dynamic_modules/integration_test.cc index 219e4c5a0b349..7434ba2bdba4f 100644 --- a/test/extensions/upstreams/http/dynamic_modules/integration_test.cc +++ b/test/extensions/upstreams/http/dynamic_modules/integration_test.cc @@ -7,9 +7,22 @@ namespace Envoy { namespace { -class DynamicModuleBridgeIntegrationTest : public HttpProtocolIntegrationTest { +// Parameterized over (HttpProtocolTestParams, language). language selects which +// test_data subdirectory (rust, go) the upstream_http_tcp_bridge module is loaded from. Both +// languages ship a "test_bridge" config factory that supports the same two modes: +// "streaming" (the default) and "local_reply". +struct DynamicModuleBridgeParam { + HttpProtocolTestParams protocol; + std::string language; +}; + +class DynamicModuleBridgeIntegrationTest : public testing::TestWithParam, + public HttpIntegrationTest { public: - DynamicModuleBridgeIntegrationTest() { enableHalfClose(true); } + DynamicModuleBridgeIntegrationTest() + : HttpIntegrationTest(GetParam().protocol.downstream_protocol, GetParam().protocol.version) { + enableHalfClose(true); + } void initialize() override { config_helper_.addConfigModifier( @@ -29,9 +42,12 @@ class DynamicModuleBridgeIntegrationTest : public HttpProtocolIntegrationTest { void initializeWithBridgeConfig(const std::string& bridge_mode) { TestEnvironment::setEnvVar( "ENVOY_DYNAMIC_MODULES_SEARCH_PATH", - TestEnvironment::substitute( - "{{ test_rundir }}/test/extensions/dynamic_modules/test_data/rust"), + TestEnvironment::substitute("{{ test_rundir }}/test/extensions/dynamic_modules/test_data/" + + GetParam().language), 1); + // The Go module relies on the cgo runtime; cgocheck must be relaxed for the + // module's internal cgo calls. + TestEnvironment::setEnvVar("GODEBUG", "cgocheck=0", 1); config_helper_.addConfigModifier( [bridge_mode](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { @@ -74,10 +90,25 @@ class DynamicModuleBridgeIntegrationTest : public HttpProtocolIntegrationTest { IntegrationStreamDecoderPtr response_; }; -INSTANTIATE_TEST_SUITE_P(HttpAndIpVersions, DynamicModuleBridgeIntegrationTest, - testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams( - {Http::CodecType::HTTP1}, {Http::CodecType::HTTP1})), - HttpProtocolIntegrationTest::protocolTestParamsToString); +std::vector getBridgeTestParams() { + std::vector params; + for (const auto& language : {"rust", "go"}) { + for (const auto& protocol : HttpProtocolIntegrationTest::getProtocolTestParams( + {Http::CodecType::HTTP1}, {Http::CodecType::HTTP1})) { + params.push_back({protocol, language}); + } + } + return params; +} + +std::string bridgeParamName(const testing::TestParamInfo& info) { + return info.param.language + "_" + + HttpProtocolIntegrationTest::protocolTestParamsToString( + ::testing::TestParamInfo(info.param.protocol, 0)); +} + +INSTANTIATE_TEST_SUITE_P(SdkLanguagesAndProtocols, DynamicModuleBridgeIntegrationTest, + testing::ValuesIn(getBridgeTestParams()), bridgeParamName); TEST_P(DynamicModuleBridgeIntegrationTest, StreamingMode) { initializeWithBridgeConfig("streaming"); diff --git a/test/extensions/upstreams/http/dynamic_modules/upstream_request_test.cc b/test/extensions/upstreams/http/dynamic_modules/upstream_request_test.cc index 8b189049d5d83..8b5d1e6b7c3b8 100644 --- a/test/extensions/upstreams/http/dynamic_modules/upstream_request_test.cc +++ b/test/extensions/upstreams/http/dynamic_modules/upstream_request_test.cc @@ -406,6 +406,45 @@ TEST_F(HttpTcpBridgeAbiEdgeCasesTest, EdgeCaseCallbacksExercised) { EXPECT_TRUE(status.ok()); } +// ============================================================================= +// HttpTcpBridge tests for send_upstream_data and send_response_trailers ABI +// callbacks. The "send_upstream" fake calls send_upstream_data (with data and +// then with empty/end_stream) from encode_headers, and calls +// send_response_headers + send_response_trailers (both with a vector and with +// NULL) from on_upstream_data. +// ============================================================================= + +class HttpTcpBridgeSendUpstreamTest : public HttpTcpBridgeTest { +public: + HttpTcpBridgeSendUpstreamTest() { createBridge("upstream_bridge_send_upstream"); } +}; + +TEST_F(HttpTcpBridgeSendUpstreamTest, EncodeHeadersWritesUpstream) { + // First send_upstream_data call writes "hello-upstream" with end_stream=true. The bridge + // calls enableHalfClose(true) and writes the buffer. The second call has empty data with + // end_stream=true; sendUpstreamData still half-closes and writes a zero-length buffer. + EXPECT_CALL(mock_connection_, enableHalfClose(true)).Times(2); + EXPECT_CALL(mock_connection_, write(_, true)).Times(2); + + Envoy::Http::TestRequestHeaderMapImpl headers{{":method", "POST"}, {":path", "/test"}}; + auto status = bridge_->encodeHeaders(headers, false); + EXPECT_TRUE(status.ok()); +} + +TEST_F(HttpTcpBridgeSendUpstreamTest, OnUpstreamDataSendsResponseTrailers) { + // First the bridge sends response headers (decodeHeaders) with end_stream=false, then a + // trailer map with two entries (decodeTrailers), then a second send_response_trailers with + // a NULL vector (skips the loop, decodeTrailers is still invoked with empty map). + EXPECT_CALL(mock_upstream_to_downstream_, decodeHeaders(_, false)); + EXPECT_CALL(mock_upstream_to_downstream_, decodeTrailers(_)).Times(2); + + Envoy::Http::TestRequestHeaderMapImpl headers{{":method", "POST"}, {":path", "/test"}}; + EXPECT_TRUE(bridge_->encodeHeaders(headers, false).ok()); + + Buffer::OwnedImpl data("upstream-data"); + bridge_->onUpstreamData(data, false); +} + // ============================================================================= // Edge case tests for connection and event handling. // ============================================================================= diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index c1125d30480e5..60c9275f5d3e6 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -1709,3 +1709,10 @@ IWYU CRTP bitrate kbps +# Words used by dynamic_modules tests/SDKs. +aarch +cgo +cgocheck +dynamicmodulescustom +strconv +FormatFloat