diff --git a/Cargo.lock b/Cargo.lock index 26d29ec8..6a9bea9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2122,6 +2122,7 @@ dependencies = [ "orion-error", "orion-format", "orion-interner", + "prost", "regex", "serde", "serde_json", diff --git a/README.md b/README.md index f5d1e801..16935da1 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,29 @@ docker rm -f backend1 backend2 orion-proxy For detailed Docker configuration options, see [docker/README.md](docker/README.md). +## Examples and Demos + +### TLV Listener Filter Demo + +Orion includes a comprehensive TLV (Type-Length-Value) listener filter demo compatible with the Kmesh project for service mesh integration. This demo provides end-to-end testing of the TLV filter functionality. + +To test the TLV filter: + +```bash +cd examples/tlv-filter-demo +./test_tlv_config.sh +``` + +This demo will: +- Start Orion with TLV filter configuration matching Kmesh format +- Load the TLV listener filter using TypedStruct configuration +- Send actual TLV packets to test the filter functionality +- Extract and verify original destination information from TLV data +- Show debug logs confirming successful TLV processing +- Verify compatibility with Kmesh TLV configuration format + + +For detailed information, see [examples/tlv-filter-demo/README.md](examples/tlv-filter-demo/README.md). diff --git a/envoy-data-plane-api/build.rs b/envoy-data-plane-api/build.rs index deb523bc..eaa1871b 100644 --- a/envoy-data-plane-api/build.rs +++ b/envoy-data-plane-api/build.rs @@ -5,7 +5,13 @@ use glob::glob; /// std::env::set_var("PROTOC", The Path of Protoc); fn main() -> std::io::Result<()> { let descriptor_path = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("proto_descriptor.bin"); - let protos: Vec = glob("data-plane-api/envoy/**/v3/*.proto").unwrap().filter_map(Result::ok).collect(); + let mut protos: Vec = glob("data-plane-api/envoy/**/v3/*.proto").unwrap().filter_map(Result::ok).collect(); + + let udpa_protos: Vec = glob("xds/udpa/**/*.proto").unwrap().filter_map(Result::ok).collect(); + protos.extend(udpa_protos); + + let custom_protos: Vec = glob("../proto/**/*.proto").unwrap().filter_map(Result::ok).collect(); + protos.extend(custom_protos); let include_paths = [ "data-plane-api/", @@ -17,6 +23,7 @@ fn main() -> std::io::Result<()> { "prometheus-client-model/", "cel-spec/proto", "protobuf/src/", + "../proto/", ]; let mut config = prost_build::Config::new(); diff --git a/examples/tlv-filter-demo/README.md b/examples/tlv-filter-demo/README.md new file mode 100644 index 00000000..75b9705a --- /dev/null +++ b/examples/tlv-filter-demo/README.md @@ -0,0 +1,85 @@ +# TLV Filter Demo + +This demo showcases Orion's TLV (Type-Length-Value) listener filter implementation, compatible with Kmesh configurations. + +## Quick Test + +Run the automated test script: + +```bash +./test_tlv_config.sh +``` + +This script: +1. Validates Orion configuration loading +2. Tests TLV filter integration +3. Optionally tests end-to-end packet processing (if client is available) + +## Configuration + +The TLV filter is configured in `orion-config.yaml`: + +```yaml +listeners: + - name: tlv_demo_listener + address: 0.0.0.0:9000 + filter_chains: + - filters: + - name: envoy.listener.kmesh_tlv + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.listener.kmesh_tlv.v3.KmeshTlv +``` + +## Manual Testing + +### Start Orion +```bash +../../target/debug/orion -c orion-config.yaml +``` + +### Send TLV Packets +```bash +rustc send_tlv.rs -o send_tlv +./send_tlv +``` + +The client constructs TLV packets containing original destination information and sends them to the Orion listener for testing the filter's TLV processing capabilities. + +## TLV Client + +The included Rust client (`send_tlv.rs`) is used for testing the TLV filter: + +- **Purpose**: Constructs and sends TLV packets to test the filter's processing +- **Functionality**: Encodes original destination IP/port in TLV format and sends to Orion listener +- **Usage**: Compile with `rustc send_tlv.rs -o send_tlv` then run `./send_tlv ` +- **Protocol**: Supports both IPv4 and IPv6 addresses in TLV packets + +### Protocol Buffer +```protobuf +syntax = "proto3"; +package envoy.extensions.filters.listener.kmesh_tlv.v3; + +message KmeshTlv {} +``` + +### Configuration Parameters +- **Filter Name**: `envoy.listener.kmesh_tlv` +- **Type URL**: `type.googleapis.com/envoy.extensions.filters.listener.kmesh_tlv.v3.KmeshTlv` + +### TLV Protocol Support +- **TLV_TYPE_SERVICE_ADDRESS (0x1)**: Service address information +- **TLV_TYPE_ENDING (0xfe)**: End marker +- **Maximum TLV Length**: 256 bytes + +## Architecture + +``` +Client → Orion Listener → TLV Filter → Filter Chains → Backend + ↓ + TLV Processing + (Extract original destination) +``` + +## Kmesh Compatibility + +Fully compatible with Kmesh project configurations using identical protobuf package names and TypedStruct configuration pattern. diff --git a/examples/tlv-filter-demo/orion-config.yaml b/examples/tlv-filter-demo/orion-config.yaml new file mode 100644 index 00000000..c3375940 --- /dev/null +++ b/examples/tlv-filter-demo/orion-config.yaml @@ -0,0 +1,67 @@ +# Orion Configuration with TLV Listener Filter Enabled + +runtime: + num_cpus: 1 + num_runtimes: 1 + +logging: + log_level: "debug" + +envoy_bootstrap: + admin: + address: + socket_address: + address: "127.0.0.1" + port_value: 9901 + + static_resources: + listeners: + - name: "tlv_demo_listener" + address: + socket_address: + address: "0.0.0.0" + port_value: 10000 + listener_filters: + - name: "envoy.listener.kmesh_tlv" + typed_config: + "@type": "type.googleapis.com/udpa.type.v1.TypedStruct" + "type_url": "type.googleapis.com/envoy.extensions.filters.listener.kmesh_tlv.v3.KmeshTlv" + "value": {} + filter_chains: + - name: "default_filter_chain" + filters: + - name: "envoy.filters.network.http_connection_manager" + typedConfig: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + codec_type: AUTO + stat_prefix: tlv_demo + httpFilters: + - name: "envoy.filters.http.router" + typedConfig: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + routeConfig: + name: "local_route" + virtualHosts: + - name: "demo" + domains: ["*"] + routes: + - match: + prefix: "/" + direct_response: + status: 200 + body: + inline_string: "TLV Filter Test - Success!" + + clusters: + - name: "dummy_cluster" + connect_timeout: 0.25s + type: STATIC + lb_policy: ROUND_ROBIN + load_assignment: + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: "127.0.0.1" + port_value: 8080 diff --git a/examples/tlv-filter-demo/send_tlv.rs b/examples/tlv-filter-demo/send_tlv.rs new file mode 100644 index 00000000..9fd99150 --- /dev/null +++ b/examples/tlv-filter-demo/send_tlv.rs @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: © 2025 kmesh authors +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright 2025 kmesh authors +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// + +use std::env; +use std::io::Write; +use std::net::{TcpStream, IpAddr}; + +fn construct_tlv_packet(ip: &str, port: u16) -> Result, Box> { + // Parse IP address + let ip_addr: IpAddr = ip.parse()?; + let (ip_bytes, content_len) = match ip_addr { + IpAddr::V4(ipv4) => (ipv4.octets().to_vec(), 6u32), // 4 bytes IP + 2 bytes port + IpAddr::V6(ipv6) => (ipv6.octets().to_vec(), 18u32), // 16 bytes IP + 2 bytes port + }; + + // Pack port as big-endian + let port_bytes = port.to_be_bytes(); + + // TLV structure: + // Type: 0x01 (service address) + // Length: 4 bytes (big-endian) + // Content: IP + Port + // End: 0xfe + + let content = [ip_bytes, port_bytes.to_vec()].concat(); + let length = content_len.to_be_bytes(); + + let mut tlv_packet = vec![0x01]; // Type + tlv_packet.extend_from_slice(&length); // Length + tlv_packet.extend_from_slice(&content); // Content + + // End marker: Type 0xfe, Length 0 + tlv_packet.push(0xfe); // End type + tlv_packet.extend_from_slice(&0u32.to_be_bytes()); // End length (0) + + Ok(tlv_packet) +} + +fn send_tlv_packet(ip: &str, port: u16, listener_host: &str, listener_port: u16) -> Result<(), Box> { + // Create TLV packet + let tlv_data = construct_tlv_packet(ip, port)?; + println!("Constructed TLV packet: {:02x?}", tlv_data); + + // Create HTTP request to follow + let http_request = b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; + + // Combine TLV + HTTP + let mut packet = tlv_data; + packet.extend_from_slice(http_request); + + // Send to listener + let mut stream = TcpStream::connect((listener_host, listener_port))?; + stream.write_all(&packet)?; + + println!("✅ Sent TLV packet with original destination {}:{}", ip, port); + Ok(()) +} + +fn main() -> Result<(), Box> { + let args: Vec = env::args().collect(); + + if args.len() != 3 { + eprintln!("Usage: {} ", args[0]); + std::process::exit(1); + } + + let ip = &args[1]; + let port: u16 = args[2].parse()?; + + match send_tlv_packet(ip, port, "127.0.0.1", 10000) { + Ok(()) => { + std::process::exit(0); + } + Err(e) => { + eprintln!("❌ Failed to send TLV packet: {}", e); + std::process::exit(1); + } + } +} diff --git a/examples/tlv-filter-demo/test_tlv_config.sh b/examples/tlv-filter-demo/test_tlv_config.sh new file mode 100755 index 00000000..b824e8d9 --- /dev/null +++ b/examples/tlv-filter-demo/test_tlv_config.sh @@ -0,0 +1,153 @@ +#!/bin/bash + +echo "🧪 TLV Filter Test" +echo "==================" + +# Check if client is available +if [ -f "./send_tlv" ]; then + HAS_CLIENT=true + echo "✅ TLV client found - will run end-to-end tests" +else + HAS_CLIENT=false + echo "⚠️ TLV client not found - will run configuration tests only" + echo " To enable full testing, compile the client:" + echo " rustc send_tlv.rs -o send_tlv" +fi + +# Function to send TLV packet and check response +test_tlv_packet() { + local ip="$1" + local port="$2" + local listener_port="${3:-10000}" + + echo "Testing TLV packet with original destination: $ip:$port" + + # Send TLV packet using Rust binary + if ./send_tlv "$ip" "$port"; then + echo "✅ TLV packet sent to listener" + return 0 + else + echo "❌ Failed to send TLV packet" + return 1 + fi +} + +echo "Testing TLV filter configuration loading..." + +# Start Orion in background +../../target/debug/orion -c orion-config.yaml > orion.log 2>&1 & +ORION_PID=$! + +# Wait for Orion to start +sleep 3 + +if ! kill -0 $ORION_PID 2>/dev/null; then + echo "❌ Orion failed to start" + cat orion.log + exit 1 +fi + +echo "✅ Orion started (PID: $ORION_PID)" + +# Check configuration loading +if grep -q "TLV listener filter is true" orion.log; then + echo "✅ TLV listener filter loaded and activated" + TLV_LOADED=true +else + echo "❌ TLV listener filter not found" + TLV_LOADED=false +fi + +if grep -q "Listener tlv_demo_listener" orion.log; then + echo "✅ TLV demo listener configuration parsed" + CONFIG_PARSED=true +else + echo "❌ TLV demo listener configuration failed" + CONFIG_PARSED=false +fi + +if grep -q "failed to decode TypedStruct" orion.log; then + echo "❌ TypedStruct parsing failed" + TYPED_STRUCT_OK=false +else + echo "✅ TypedStruct configuration processed" + TYPED_STRUCT_OK=true +fi + +echo "" +echo "Configuration Test Results:" +echo "TLV Filter: $([ "$TLV_LOADED" = true ] && echo "✅ PASS" || echo "❌ FAIL")" +echo "Config: $([ "$CONFIG_PARSED" = true ] && echo "✅ PASS" || echo "❌ FAIL")" +echo "TypedStruct: $([ "$TYPED_STRUCT_OK" = true ] && echo "✅ PASS" || echo "❌ FAIL")" + +# Test TLV packet processing (only if client is available) +if [ "$HAS_CLIENT" = true ]; then + echo "" + echo "Testing TLV packet processing..." + + # Clear previous logs + > orion.log + + # Send test TLV packet + test_tlv_packet "192.168.1.100" "8080" + + # Wait a moment for processing + sleep 1 + + # Check if TLV was processed + if grep -q "Extracted original destination" orion.log; then + echo "✅ TLV packet processed successfully" + TLV_PROCESSED=true + else + echo "❌ TLV packet processing failed" + TLV_PROCESSED=false + fi + + if grep -q "192.168.1.100:8080" orion.log; then + echo "✅ Original destination extracted correctly" + ORIGINAL_DEST_EXTRACTED=true + else + echo "❌ Original destination extraction failed" + ORIGINAL_DEST_EXTRACTED=false + fi +else + echo "" + echo "Skipping TLV packet processing test (client not available)" + TLV_PROCESSED=true # Not applicable + ORIGINAL_DEST_EXTRACTED=true # Not applicable +fi + +echo "" +if [ "$HAS_CLIENT" = true ]; then + echo "TLV Processing Test Results:" + echo "TLV Processed: $([ "$TLV_PROCESSED" = true ] && echo "✅ PASS" || echo "❌ FAIL")" + echo "Original Dest: $([ "$ORIGINAL_DEST_EXTRACTED" = true ] && echo "✅ PASS" || echo "❌ FAIL")" +else + echo "TLV Processing Test Results: ⚠️ SKIPPED (client not available)" +fi + +# Cleanup +kill $ORION_PID 2>/dev/null +wait $ORION_PID 2>/dev/null + +echo "" +if [ "$HAS_CLIENT" = true ]; then + # Full end-to-end test + if [ "$TLV_LOADED" = true ] && [ "$CONFIG_PARSED" = true ] && [ "$TYPED_STRUCT_OK" = true ] && [ "$TLV_PROCESSED" = true ] && [ "$ORIGINAL_DEST_EXTRACTED" = true ]; then + echo "🎉 TLV Filter End-to-End Test: ALL TESTS PASSED!" + exit 0 + else + echo "⚠️ Some tests failed. Check orion.log for details." + exit 1 + fi +else + # Configuration-only test + if [ "$TLV_LOADED" = true ] && [ "$CONFIG_PARSED" = true ] && [ "$TYPED_STRUCT_OK" = true ]; then + echo "🎉 TLV Filter Configuration Test: PASSED!" + echo " Note: End-to-end testing skipped (compile client for full test)" + exit 0 + else + echo "⚠️ Configuration tests failed. Check orion.log for details." + exit 1 + fi +fi diff --git a/orion-configuration/Cargo.toml b/orion-configuration/Cargo.toml index b443e403..b0f16574 100644 --- a/orion-configuration/Cargo.toml +++ b/orion-configuration/Cargo.toml @@ -31,6 +31,7 @@ orion-data-plane-api = { workspace = true, optional = true } orion-error.workspace = true orion-format.workspace = true orion-interner.workspace = true +prost = "0.13.0" regex.workspace = true serde = { workspace = true, features = ["rc"] } serde_json.workspace = true @@ -52,6 +53,8 @@ tempfile = "3.8" default = ["envoy-conversions"] envoy-conversions = ["dep:orion-data-plane-api"] +[lib] +doctest = false [lints] workspace = true diff --git a/orion-configuration/src/config/listener.rs b/orion-configuration/src/config/listener.rs index 276771da..e2d1d667 100644 --- a/orion-configuration/src/config/listener.rs +++ b/orion-configuration/src/config/listener.rs @@ -52,6 +52,10 @@ pub struct Listener { pub with_tls_inspector: bool, #[serde(skip_serializing_if = "Option::is_none", default = "Default::default")] pub proxy_protocol_config: Option, + #[serde(skip_serializing_if = "std::ops::Not::not", default)] + pub with_tlv_listener_filter: bool, + #[serde(skip_serializing_if = "Option::is_none", default = "Default::default")] + pub tlv_listener_filter_config: Option, } impl Listener { @@ -440,6 +444,8 @@ mod envoy_conversions { let listener_filters: Vec = convert_vec!(listener_filters)?; let mut with_tls_inspector = false; let mut proxy_protocol_config = None; + let mut with_tlv_listener_filter = false; + let mut tlv_listener_filter_config = None; for filter in listener_filters { match filter.config { @@ -457,6 +463,14 @@ mod envoy_conversions { } proxy_protocol_config = Some(config); }, + ListenerFilterConfig::TlvListenerFilter(config) => { + if with_tlv_listener_filter { + return Err(GenericError::from_msg("duplicate TLV listener filter")) + .with_node("listener_filters"); + } + with_tlv_listener_filter = true; + tlv_listener_filter_config = Some(config); + }, } } let bind_device = convert_vec!(socket_options)?; @@ -465,7 +479,16 @@ mod envoy_conversions { .with_node("socket_options"); } let bind_device = bind_device.into_iter().next(); - Ok(Self { name, address, filter_chains, bind_device, with_tls_inspector, proxy_protocol_config }) + Ok(Self { + name, + address, + filter_chains, + bind_device, + with_tls_inspector, + proxy_protocol_config, + with_tlv_listener_filter, + tlv_listener_filter_config, + }) }()) .with_name(name) } diff --git a/orion-configuration/src/config/listener_filters.rs b/orion-configuration/src/config/listener_filters.rs index 6ad259ba..40d89304 100644 --- a/orion-configuration/src/config/listener_filters.rs +++ b/orion-configuration/src/config/listener_filters.rs @@ -30,8 +30,12 @@ pub struct ListenerFilter { pub enum ListenerFilterConfig { TlsInspector, ProxyProtocol(DownstreamProxyProtocolConfig), + TlvListenerFilter(TlvListenerFilterConfig), } +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Default)] +pub struct TlvListenerFilterConfig {} + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Default)] pub struct DownstreamProxyProtocolConfig { #[serde(default)] @@ -47,7 +51,7 @@ pub struct DownstreamProxyProtocolConfig { #[cfg(feature = "envoy-conversions")] mod envoy_conversions { #![allow(deprecated)] - use super::{DownstreamProxyProtocolConfig, ListenerFilter, ListenerFilterConfig}; + use super::{DownstreamProxyProtocolConfig, ListenerFilter, ListenerFilterConfig, TlvListenerFilterConfig}; use crate::config::{ common::{ProxyProtocolVersion, *}, transport::ProxyProtocolPassThroughTlvs, @@ -59,22 +63,42 @@ mod envoy_conversions { listener_filter::ConfigType as EnvoyListenerFilterConfigType, ListenerFilter as EnvoyListenerFilter, }, extensions::filters::listener::{ - proxy_protocol::v3::ProxyProtocol as EnvoyProxyProtocol, + kmesh_tlv::v3::KmeshTlv as EnvoyKmeshTlv, proxy_protocol::v3::ProxyProtocol as EnvoyProxyProtocol, tls_inspector::v3::TlsInspector as EnvoyTlsInspector, }, }, google::protobuf::Any, prost::Message, + udpa::r#type::v1::TypedStruct, }; #[derive(Debug, Clone)] enum SupportedEnvoyListenerFilter { TlsInspector(EnvoyTlsInspector), ProxyProtocol(EnvoyProxyProtocol), + KmeshTlv(EnvoyKmeshTlv), } impl TryFrom for SupportedEnvoyListenerFilter { type Error = GenericError; fn try_from(typed_config: Any) -> Result { + if typed_config.type_url == "type.googleapis.com/udpa.type.v1.TypedStruct" { + let typed_struct = TypedStruct::decode(typed_config.value.as_slice()) + .map_err(|e| GenericError::from_msg_with_cause("failed to decode TypedStruct", e))?; + + match typed_struct.type_url.as_str() { + "type.googleapis.com/envoy.extensions.filters.listener.kmesh_tlv.v3.KmeshTlv" => { + let config = EnvoyKmeshTlv {}; + return Ok(Self::KmeshTlv(config)); + }, + _ => { + return Err(GenericError::unsupported_variant(format!( + "unsupported TypedStruct type_url: {}", + typed_struct.type_url + ))); + }, + } + } + match typed_config.type_url.as_str() { "type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector" => { EnvoyTlsInspector::decode(typed_config.value.as_slice()).map(Self::TlsInspector) @@ -82,6 +106,11 @@ mod envoy_conversions { "type.googleapis.com/envoy.extensions.filters.listener.proxy_protocol.v3.ProxyProtocol" => { EnvoyProxyProtocol::decode(typed_config.value.as_slice()).map(Self::ProxyProtocol) }, + "type.googleapis.com/envoy.extensions.filters.listener.kmesh_tlv.v3.KmeshTlv" => { + let config = EnvoyKmeshTlv::decode(typed_config.value.as_slice()) + .map_err(|e| GenericError::from_msg_with_cause("failed to decode KmeshTlv protobuf", e))?; + Ok(Self::KmeshTlv(config)) + }, _ => { return Err(GenericError::unsupported_variant(typed_config.type_url)); }, @@ -145,6 +174,10 @@ mod envoy_conversions { let config = DownstreamProxyProtocolConfig::try_from(envoy_proxy_protocol)?; Ok(Self::ProxyProtocol(config)) }, + SupportedEnvoyListenerFilter::KmeshTlv(config) => { + let tlv_config = TlvListenerFilterConfig::try_from(config)?; + Ok(Self::TlvListenerFilter(tlv_config)) + }, } } } @@ -174,4 +207,11 @@ mod envoy_conversions { Ok(Self { allow_requests_without_proxy_protocol, stat_prefix, disallowed_versions, pass_through_tlvs }) } } + + impl TryFrom for TlvListenerFilterConfig { + type Error = GenericError; + fn try_from(_value: EnvoyKmeshTlv) -> Result { + Ok(Self::default()) + } + } } diff --git a/orion-lib/src/listeners/filter_state.rs b/orion-lib/src/listeners/filter_state.rs index 9c66ea6d..1daa781c 100644 --- a/orion-lib/src/listeners/filter_state.rs +++ b/orion-lib/src/listeners/filter_state.rs @@ -35,6 +35,12 @@ pub enum DownstreamConnectionMetadata { proxy_peer_address: SocketAddr, proxy_local_address: SocketAddr, }, + FromTlv { + original_destination_address: SocketAddr, + tlv_data: HashMap>, + proxy_peer_address: SocketAddr, + proxy_local_address: SocketAddr, + }, } impl DownstreamConnectionMetadata { @@ -42,12 +48,14 @@ impl DownstreamConnectionMetadata { match self { Self::FromSocket { peer_address, .. } => *peer_address, Self::FromProxyProtocol { original_peer_address, .. } => *original_peer_address, + Self::FromTlv { proxy_peer_address, .. } => *proxy_peer_address, } } pub fn local_address(&self) -> SocketAddr { match self { Self::FromSocket { local_address, .. } => *local_address, Self::FromProxyProtocol { original_destination_address, .. } => *original_destination_address, + Self::FromTlv { original_destination_address, .. } => *original_destination_address, } } } diff --git a/orion-lib/src/listeners/listener.rs b/orion-lib/src/listeners/listener.rs index 12eda9c3..666ca371 100644 --- a/orion-lib/src/listeners/listener.rs +++ b/orion-lib/src/listeners/listener.rs @@ -25,7 +25,7 @@ use super::{ use crate::{ listeners::filter_state::DownstreamConnectionMetadata, secrets::{TlsConfigurator, WantsToBuildServer}, - transport::{bind_device::BindDevice, tls_inspector, AsyncStream, ProxyProtocolReader}, + transport::{bind_device::BindDevice, tls_inspector, AsyncStream, ProxyProtocolReader, TlvListenerFilter}, ConversionContext, Error, Result, RouteConfigurationChange, }; use opentelemetry::KeyValue; @@ -63,6 +63,7 @@ struct PartialListener { filter_chains: HashMap, with_tls_inspector: bool, proxy_protocol_config: Option, + with_tlv_listener_filter: bool, } #[derive(Debug, Clone)] pub struct ListenerFactory { @@ -77,7 +78,9 @@ impl TryFrom> for PartialListener { let addr = listener.address; let with_tls_inspector = listener.with_tls_inspector; let proxy_protocol_config = listener.proxy_protocol_config; + let with_tlv_listener_filter = listener.with_tlv_listener_filter; debug!("Listener {name} :TLS Inspector is {with_tls_inspector}"); + debug!("Listener {name} :TLV listener filter is {with_tlv_listener_filter}"); let filter_chains: HashMap = listener .filter_chains @@ -102,6 +105,7 @@ impl TryFrom> for PartialListener { filter_chains, with_tls_inspector, proxy_protocol_config, + with_tlv_listener_filter, }) } } @@ -119,6 +123,7 @@ impl ListenerFactory { filter_chains, with_tls_inspector, proxy_protocol_config, + with_tlv_listener_filter, } = self.listener; let filter_chains = filter_chains @@ -133,6 +138,7 @@ impl ListenerFactory { filter_chains, with_tls_inspector, proxy_protocol_config, + with_tlv_listener_filter, route_updates_receiver, secret_updates_receiver, }) @@ -155,6 +161,7 @@ pub struct Listener { pub filter_chains: HashMap, with_tls_inspector: bool, proxy_protocol_config: Option, + with_tlv_listener_filter: bool, route_updates_receiver: broadcast::Receiver, secret_updates_receiver: broadcast::Receiver, } @@ -174,6 +181,7 @@ impl Listener { filter_chains: HashMap::new(), with_tls_inspector: false, proxy_protocol_config: None, + with_tlv_listener_filter: false, route_updates_receiver: route_rx, secret_updates_receiver: secret_rx, } @@ -194,6 +202,7 @@ impl Listener { filter_chains, with_tls_inspector, proxy_protocol_config, + with_tlv_listener_filter, mut route_updates_receiver, mut secret_updates_receiver, } = self; @@ -230,7 +239,7 @@ impl Listener { // we could optimize a little here by either splitting up the filter_chain selection and rbac into the parts that can run // before we have the ClientHello and the ones after. since we might already have enough info to decide to drop the connection // or pick a specific filter_chain to run, or we could simply if-else on the with_tls_inspector variable. - tokio::spawn(Self::process_listener_update(name, filter_chains, with_tls_inspector, proxy_protocol_config, local_address, peer_addr, Box::new(stream), start)); + tokio::spawn(Self::process_listener_update(name, filter_chains, with_tls_inspector, proxy_protocol_config, with_tlv_listener_filter, local_address, peer_addr, Box::new(stream), start)); }, Err(e) => {warn!("failed to accept tcp connection: {e}");} } @@ -351,6 +360,7 @@ impl Listener { filter_chains: Arc>, with_tls_inspector: bool, proxy_protocol_config: Option>, + with_tlv_listener_filter: bool, local_address: SocketAddr, peer_addr: SocketAddr, mut stream: AsyncStream, @@ -378,6 +388,16 @@ impl Listener { } else { DownstreamConnectionMetadata::FromSocket { peer_address: peer_addr, local_address } }; + + let downstream_metadata = if with_tlv_listener_filter { + let mut tlv_filter = TlvListenerFilter::default(); + let (new_stream, tlv_metadata) = tlv_filter.process_stream(stream, local_address, peer_addr).await?; + stream = new_stream; + tlv_metadata + } else { + downstream_metadata + }; + let downstream_metadata = Arc::new(downstream_metadata); let server_name = if with_tls_inspector { diff --git a/orion-lib/src/listeners/listeners_manager.rs b/orion-lib/src/listeners/listeners_manager.rs index 1acf5a71..393c78b8 100644 --- a/orion-lib/src/listeners/listeners_manager.rs +++ b/orion-lib/src/listeners/listeners_manager.rs @@ -21,7 +21,9 @@ use std::collections::BTreeMap; use tokio::sync::{broadcast, mpsc}; -use tracing::{debug, info, warn}; +#[cfg(debug_assertions)] +use tracing::debug; +use tracing::{info, warn}; use orion_configuration::config::{ network_filters::http_connection_manager::RouteConfiguration, Listener as ListenerConfig, @@ -184,6 +186,8 @@ mod tests { bind_device: None, with_tls_inspector: false, proxy_protocol_config: None, + with_tlv_listener_filter: false, + tlv_listener_filter_config: None, }; man.start_listener(l1, l1_info.clone()).unwrap(); assert!(routeb_tx1.send(RouteConfigurationChange::Removed("n/a".into())).is_ok()); @@ -223,6 +227,8 @@ mod tests { bind_device: None, with_tls_inspector: false, proxy_protocol_config: None, + with_tlv_listener_filter: false, + tlv_listener_filter_config: None, }; man.start_listener(l1, l1_info).unwrap(); diff --git a/orion-lib/src/transport/mod.rs b/orion-lib/src/transport/mod.rs index ab113b6d..92fb3725 100644 --- a/orion-lib/src/transport/mod.rs +++ b/orion-lib/src/transport/mod.rs @@ -30,6 +30,7 @@ pub use resolver::resolve; pub mod policy; pub mod proxy_protocol; pub mod tls_inspector; +pub mod tlv_listener_filter; pub mod transport_socket; pub use self::{ @@ -37,6 +38,7 @@ pub use self::{ http_channel::{HttpChannel, HttpChannelBuilder}, proxy_protocol::ProxyProtocolReader, tcp_channel::TcpChannelConnector, + tlv_listener_filter::TlvListenerFilter, transport_socket::UpstreamTransportSocketConfigurator, }; diff --git a/orion-lib/src/transport/tlv_listener_filter.rs b/orion-lib/src/transport/tlv_listener_filter.rs new file mode 100644 index 00000000..ed0a72e1 --- /dev/null +++ b/orion-lib/src/transport/tlv_listener_filter.rs @@ -0,0 +1,471 @@ +// SPDX-FileCopyrightText: © 2025 kmesh authors +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright 2025 kmesh authors +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// + +use crate::listeners::filter_state::DownstreamConnectionMetadata; +use crate::transport::AsyncStream; +use crate::{Error, Result}; +use std::io::{Error as IoError, ErrorKind}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use tokio::io::AsyncReadExt; +use tracing::{debug, error, trace}; + +const TLV_TYPE_LEN: usize = 1; +const TLV_LENGTH_LEN: usize = 4; + +const TLV_TYPE_SERVICE_ADDRESS: u8 = 0x1; +const TLV_TYPE_ENDING: u8 = 0xfe; + +const TLV_TYPE_SERVICE_ADDRESS_IPV4_LEN: u32 = 6; +const TLV_TYPE_SERVICE_ADDRESS_IPV6_LEN: u32 = 18; + +const MAX_TLV_LENGTH: usize = 256; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TlvParseState { + TypeAndLength, + Content, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ReadOrParseState { + Done, + Error, + SkipFilter, +} + +pub struct TlvListenerFilter { + state: TlvParseState, + expected_length: usize, + index: usize, + content_length: u32, + max_tlv_length: usize, + buffer: Vec, +} + +impl Default for TlvListenerFilter { + fn default() -> Self { + Self::new(MAX_TLV_LENGTH) + } +} + +impl TlvListenerFilter { + pub fn new(max_tlv_length: usize) -> Self { + Self { + state: TlvParseState::TypeAndLength, + expected_length: TLV_TYPE_LEN + TLV_LENGTH_LEN, + index: 0, + content_length: 0, + max_tlv_length, + buffer: Vec::new(), + } + } + + fn read_u32_be(&self, offset: usize) -> Result { + if offset + 4 > self.buffer.len() { + return Err(Error::from(IoError::new(ErrorKind::InvalidData, "Not enough data for u32"))); + } + Ok(u32::from_be_bytes([ + self.buffer[offset], + self.buffer[offset + 1], + self.buffer[offset + 2], + self.buffer[offset + 3], + ])) + } + + fn read_u16_be(&self, offset: usize) -> Result { + if offset + 2 > self.buffer.len() { + return Err(Error::from(IoError::new(ErrorKind::InvalidData, "Not enough data for u16"))); + } + Ok(u16::from_be_bytes([self.buffer[offset], self.buffer[offset + 1]])) + } + + fn validate_service_address_length(content_length: u32) -> bool { + content_length == TLV_TYPE_SERVICE_ADDRESS_IPV4_LEN || content_length == TLV_TYPE_SERVICE_ADDRESS_IPV6_LEN + } + + fn parse_ipv4_address(&self, content_start: usize) -> Result { + if content_start + 6 > self.buffer.len() { + return Err(Error::from(IoError::new(ErrorKind::InvalidData, "Not enough data for IPv4 address"))); + } + + let ip_bytes: [u8; 4] = self.buffer[content_start..content_start + 4] + .try_into() + .map_err(|_| IoError::new(ErrorKind::InvalidData, "Invalid IPv4 bytes"))?; + let port = self.read_u16_be(content_start + 4)?; + + let ip = IpAddr::V4(Ipv4Addr::from(ip_bytes)); + Ok(SocketAddr::new(ip, port)) + } + + fn parse_ipv6_address(&self, content_start: usize) -> Result { + if content_start + 18 > self.buffer.len() { + return Err(Error::from(IoError::new(ErrorKind::InvalidData, "Not enough data for IPv6 address"))); + } + + let mut ip_bytes = [0u8; 16]; + ip_bytes.copy_from_slice(&self.buffer[content_start..content_start + 16]); + let port = self.read_u16_be(content_start + 16)?; + + let ip = IpAddr::V6(Ipv6Addr::from(ip_bytes)); + Ok(SocketAddr::new(ip, port)) + } + + fn process_service_address_tlv(&mut self) -> ReadOrParseState { + if self.expected_length < self.index + TLV_TYPE_LEN + TLV_LENGTH_LEN { + self.expected_length = self.index + TLV_TYPE_LEN + TLV_LENGTH_LEN; + return ReadOrParseState::Done; + } + + trace!("Processing TLV_TYPE_SERVICE_ADDRESS"); + + let content_length = match self.read_u32_be(self.index + 1) { + Ok(len) => len, + Err(_) => return ReadOrParseState::Error, + }; + + trace!("TLV content length: {}", content_length); + + if !Self::validate_service_address_length(content_length) { + error!( + "Invalid TLV service address content length: {} (expected {} or {})", + content_length, TLV_TYPE_SERVICE_ADDRESS_IPV4_LEN, TLV_TYPE_SERVICE_ADDRESS_IPV6_LEN + ); + return ReadOrParseState::Error; + } + + self.expected_length = self.index + TLV_TYPE_LEN + TLV_LENGTH_LEN + content_length as usize; + if self.expected_length > self.max_tlv_length { + error!("TLV data exceeds maximum length: {} > {}", self.expected_length, self.max_tlv_length); + return ReadOrParseState::Error; + } + + self.content_length = content_length; + self.index += TLV_TYPE_LEN + TLV_LENGTH_LEN; + self.state = TlvParseState::Content; + ReadOrParseState::Done + } + + fn process_ending_tlv(&mut self) -> ReadOrParseState { + trace!("Processing TLV_TYPE_ENDING"); + + if self.expected_length < self.index + TLV_TYPE_LEN + TLV_LENGTH_LEN { + self.expected_length = self.index + TLV_TYPE_LEN + TLV_LENGTH_LEN; + return ReadOrParseState::Done; + } + + let end_length = match self.read_u32_be(self.index + 1) { + Ok(len) => len, + Err(_) => return ReadOrParseState::Error, + }; + + if end_length != 0 { + error!("Invalid TLV end marker length: {} (expected 0)", end_length); + return ReadOrParseState::Error; + } + + ReadOrParseState::Done + } + + fn process_tlv_content(&mut self) -> ReadOrParseState { + trace!("Parsing TLV content"); + self.expected_length += TLV_TYPE_LEN; + self.index += self.content_length as usize; + self.state = TlvParseState::TypeAndLength; + ReadOrParseState::Done + } + + async fn read_required_data(&mut self, stream: &mut AsyncStream) -> ReadOrParseState { + let mut temp_buf = vec![0u8; 512]; + + while self.buffer.len() < self.expected_length { + let bytes_needed = self.expected_length - self.buffer.len(); + let read_size = bytes_needed.min(temp_buf.len()); + + match stream.read(&mut temp_buf[..read_size]).await { + Ok(0) => { + if self.buffer.len() >= self.expected_length { + break; + } + trace!("Connection closed while reading TLV data, skipping filter"); + return ReadOrParseState::SkipFilter; + }, + Ok(n) => { + self.buffer.extend_from_slice(&temp_buf[..n]); + }, + Err(e) if e.kind() == ErrorKind::WouldBlock => { + trace!("No data available for TLV, skipping filter"); + return ReadOrParseState::SkipFilter; + }, + Err(e) => { + error!("Error reading TLV data: {}", e); + return ReadOrParseState::Error; + }, + } + } + + ReadOrParseState::Done + } + + pub async fn process_stream( + &mut self, + mut stream: AsyncStream, + local_address: SocketAddr, + peer_address: SocketAddr, + ) -> Result<(AsyncStream, DownstreamConnectionMetadata)> { + trace!("Starting TLV processing"); + + match self.read_and_parse_tlv(&mut stream).await { + ReadOrParseState::Done => { + trace!("TLV parsing completed successfully"); + + if let Some(original_dest) = self.extract_original_destination() { + debug!("Extracted original destination: {}", original_dest); + let metadata = DownstreamConnectionMetadata::FromTlv { + original_destination_address: original_dest, + tlv_data: std::collections::HashMap::new(), + proxy_peer_address: peer_address, + proxy_local_address: local_address, + }; + Ok((stream, metadata)) + } else { + let metadata = DownstreamConnectionMetadata::FromSocket { peer_address, local_address }; + Ok((stream, metadata)) + } + }, + ReadOrParseState::SkipFilter => { + trace!("Skipping TLV filter"); + let metadata = DownstreamConnectionMetadata::FromSocket { peer_address, local_address }; + Ok((stream, metadata)) + }, + ReadOrParseState::Error => { + error!("Error parsing TLV data"); + Err(Error::from(IoError::new(ErrorKind::InvalidData, "Failed to parse TLV data"))) + }, + } + } + + async fn read_and_parse_tlv(&mut self, stream: &mut AsyncStream) -> ReadOrParseState { + loop { + let read_result = self.read_required_data(stream).await; + match read_result { + ReadOrParseState::Done => {}, + other => return other, + } + + trace!("Buffer has {} bytes, expected {}", self.buffer.len(), self.expected_length); + + match self.state { + TlvParseState::TypeAndLength => { + trace!("Parsing TLV type and length"); + + let tlv_type = self.buffer[self.index]; + + let parse_result = match tlv_type { + TLV_TYPE_SERVICE_ADDRESS => self.process_service_address_tlv(), + TLV_TYPE_ENDING => return self.process_ending_tlv(), + _ => { + error!("Invalid TLV type: {:#x}", tlv_type); + ReadOrParseState::Error + }, + }; + + match parse_result { + ReadOrParseState::Done => {}, + other => return other, + } + }, + TlvParseState::Content => { + let parse_result = self.process_tlv_content(); + match parse_result { + ReadOrParseState::Done => {}, + other => return other, + } + }, + } + } + } + + fn extract_original_destination(&self) -> Option { + let mut current_index = 0; + + while current_index + TLV_TYPE_LEN + TLV_LENGTH_LEN <= self.buffer.len() { + let tlv_type = self.buffer[current_index]; + + if tlv_type == TLV_TYPE_ENDING { + break; + } + + let content_length = match self.read_u32_be(current_index + TLV_TYPE_LEN) { + Ok(len) => len, + Err(_) => return None, + }; + + let content_start = current_index + TLV_TYPE_LEN + TLV_LENGTH_LEN; + if content_start + content_length as usize > self.buffer.len() { + // Not enough data for content + return None; + } + + if tlv_type == TLV_TYPE_SERVICE_ADDRESS { + let socket_addr = match content_length { + TLV_TYPE_SERVICE_ADDRESS_IPV4_LEN => self.parse_ipv4_address(content_start).ok(), + TLV_TYPE_SERVICE_ADDRESS_IPV6_LEN => self.parse_ipv6_address(content_start).ok(), + _ => None, + }; + + if let Some(addr) = socket_addr { + return Some(addr); + } + } + + current_index += TLV_TYPE_LEN + TLV_LENGTH_LEN + content_length as usize; + } + + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + use tokio::io::AsyncRead; + + struct CursorAdapter { + cursor: Cursor>, + } + + impl AsyncRead for CursorAdapter { + fn poll_read( + mut self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + let n = match std::io::Read::read(&mut self.cursor, buf.initialize_unfilled()) { + Ok(n) => n, + Err(e) => return std::task::Poll::Ready(Err(e)), + }; + buf.advance(n); + std::task::Poll::Ready(Ok(())) + } + } + + impl tokio::io::AsyncWrite for CursorAdapter { + fn poll_write( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + _buf: &[u8], + ) -> std::task::Poll> { + std::task::Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::Unsupported, "write not supported"))) + } + + fn poll_flush( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::task::Poll::Ready(Ok(())) + } + + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::task::Poll::Ready(Ok(())) + } + } + + #[tokio::test] + async fn test_ipv4_tlv_parsing() { + let mut filter = TlvListenerFilter::new(256); + + let mut tlv_data = Vec::new(); + tlv_data.push(TLV_TYPE_SERVICE_ADDRESS); + tlv_data.extend_from_slice(&TLV_TYPE_SERVICE_ADDRESS_IPV4_LEN.to_be_bytes()); + tlv_data.extend_from_slice(&[192, 168, 1, 100]); + tlv_data.extend_from_slice(&8080u16.to_be_bytes()); + tlv_data.push(TLV_TYPE_ENDING); + tlv_data.extend_from_slice(&0u32.to_be_bytes()); + + let cursor_adapter = CursorAdapter { cursor: Cursor::new(tlv_data) }; + let stream = Box::new(cursor_adapter) as AsyncStream; + + let local_addr = "0.0.0.0:80".parse().unwrap(); + let peer_addr = "10.0.0.1:12345".parse().unwrap(); + + let (_stream, metadata) = filter.process_stream(stream, local_addr, peer_addr).await.unwrap(); + + match metadata { + DownstreamConnectionMetadata::FromTlv { original_destination_address, .. } => { + assert_eq!(original_destination_address, "192.168.1.100:8080".parse::().unwrap()); + }, + other => { + unreachable!("Expected FromTlv metadata, got: {:?}", other); + }, + } + } + + #[tokio::test] + async fn test_ipv6_tlv_parsing() { + let mut filter = TlvListenerFilter::new(256); + + let mut tlv_data = Vec::new(); + tlv_data.push(TLV_TYPE_SERVICE_ADDRESS); + tlv_data.extend_from_slice(&TLV_TYPE_SERVICE_ADDRESS_IPV6_LEN.to_be_bytes()); + + tlv_data.extend_from_slice(&[ + 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + ]); + tlv_data.extend_from_slice(&8080u16.to_be_bytes()); + tlv_data.push(TLV_TYPE_ENDING); + tlv_data.extend_from_slice(&0u32.to_be_bytes()); + + let cursor_adapter = CursorAdapter { cursor: Cursor::new(tlv_data) }; + let stream = Box::new(cursor_adapter) as AsyncStream; + + let local_addr = "[::]:80".parse().unwrap(); + let peer_addr = "[2001:db8::2]:12345".parse().unwrap(); + + let (_stream, metadata) = filter.process_stream(stream, local_addr, peer_addr).await.unwrap(); + + match metadata { + DownstreamConnectionMetadata::FromTlv { original_destination_address, .. } => { + assert_eq!(original_destination_address, "[2001:db8::1]:8080".parse::().unwrap()); + }, + other => { + unreachable!("Expected FromTlv metadata, got: {:?}", other); + }, + } + } + #[tokio::test] + async fn test_invalid_tlv_type() { + let mut filter = TlvListenerFilter::new(256); + + let tlv_data = vec![0x99, 0x00, 0x00, 0x00, 0x04, 0x01, 0x02, 0x03, 0x04]; + + let cursor_adapter = CursorAdapter { cursor: Cursor::new(tlv_data) }; + let stream = Box::new(cursor_adapter) as AsyncStream; + + let local_addr = "0.0.0.0:80".parse().unwrap(); + let peer_addr = "10.0.0.1:12345".parse().unwrap(); + + let result = filter.process_stream(stream, local_addr, peer_addr).await; + assert!(result.is_err()); + } +} diff --git a/orion-proxy/src/admin/config_dump.rs b/orion-proxy/src/admin/config_dump.rs index 5f12ce88..857f2505 100644 --- a/orion-proxy/src/admin/config_dump.rs +++ b/orion-proxy/src/admin/config_dump.rs @@ -307,6 +307,8 @@ mod config_dump_tests { bind_device: None, proxy_protocol_config: None, with_tls_inspector: false, + with_tlv_listener_filter: false, + tlv_listener_filter_config: None, }; let (configuration_senders, handle) = spawn_mock_listener_manager(Some(vec![listener])); let admin_state = AdminState { diff --git a/proto/extensions/filters/listener/kmesh_tlv/v3/kmesh_tlv.proto b/proto/extensions/filters/listener/kmesh_tlv/v3/kmesh_tlv.proto new file mode 100644 index 00000000..c732924d --- /dev/null +++ b/proto/extensions/filters/listener/kmesh_tlv/v3/kmesh_tlv.proto @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: © 2025 kmesh authors +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright 2025 kmesh authors +// +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// + +syntax = "proto3"; + +package envoy.extensions.filters.listener.kmesh_tlv.v3; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.listener.kmesh_tlv.v3"; +option java_outer_classname = "KmeshTlvProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/kmesh_tlv/v3;kmesh_tlvv3"; + +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Kmesh TLV listener filter] +// Kmesh TLV filter parses the TLV (Type-Length-Value) options in the TCP stream. + +message KmeshTlv { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.listener.kmesh_tlv.v2.KmeshTlv"; +}