diff --git a/Cargo.lock b/Cargo.lock index 91e7b75279..255a22a74f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -588,6 +588,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" +dependencies = [ + "hybrid-array", +] + [[package]] name = "borsh" version = "1.6.0" @@ -708,6 +717,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.3.0", +] + [[package]] name = "chrono" version = "0.4.42" @@ -722,6 +742,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64727038c8c5e2bb503a15b9f5b9df50a1da9a33e83e1f93067d914f2c6604a5" +dependencies = [ + "block-buffer 0.11.0", + "crypto-common 0.2.0", + "inout", +] + [[package]] name = "circular-buffer" version = "0.1.9" @@ -1188,6 +1219,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1237,6 +1277,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "211f05e03c7d03754740fd9e585de910a095d6b99f8bcfffdef8319fa02a8331" +dependencies = [ + "hybrid-array", +] + [[package]] name = "darling" version = "0.21.3" @@ -1438,8 +1487,8 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", "subtle", ] @@ -2258,6 +2307,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +[[package]] +name = "hybrid-array" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b229d73f5803b562cc26e4da0396c8610a4ee209f4fac8fa4f8d709166dc45" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.8.1" @@ -2536,6 +2594,15 @@ dependencies = [ "libc", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + [[package]] name = "insta" version = "1.46.1" @@ -3568,9 +3635,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", "ucd-trie", @@ -3578,9 +3645,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ "pest", "pest_generator", @@ -3588,9 +3655,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ "pest", "pest_meta", @@ -3601,9 +3668,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.5" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", "sha2", @@ -3719,9 +3786,9 @@ checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "postgres-protocol" -version = "0.6.9" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbef655056b916eb868048276cfd5d6a7dea4f81560dfd047f97c8c6fe3fcfd4" +checksum = "3ee9dd5fe15055d2b6806f4736aa0c9637217074e224bbec46d4041b91bb9491" dependencies = [ "base64", "byteorder", @@ -4043,6 +4110,7 @@ dependencies = [ "base64-serde", "bytes", "cfg-if", + "chacha20", "clap", "crossbeam-utils", "dashmap", @@ -5040,7 +5108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -5051,7 +5119,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] diff --git a/Cargo.toml b/Cargo.toml index d69791ea31..751a0d344e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ base64.workspace = true base64-serde = "0.8.0" bytes.workspace = true cfg-if = "1.0" +chacha20 = { version = "0.10", default-features = false, features = ["cipher"] } crossbeam-utils = { version = "0.8", optional = true } clap = { version = "4.5.54", features = ["cargo", "derive", "env"] } dashmap = { version = "6.1", features = ["serde"] } diff --git a/crates/proto-gen/gen.rs b/crates/proto-gen/gen.rs index a5b0601466..1e69754ed7 100644 --- a/crates/proto-gen/gen.rs +++ b/crates/proto-gen/gen.rs @@ -222,6 +222,7 @@ fn execute(which: &str) { "config/v1alpha1/config", "filters/capture/v1alpha1/capture", "filters/concatenate/v1alpha1/concatenate", + "filters/decryptor/v1alpha1/decryptor", "filters/debug/v1alpha1/debug", "filters/drop/v1alpha1/drop", "filters/firewall/v1alpha1/firewall", diff --git a/crates/quilkin-proto/src/generated/quilkin/filters.rs b/crates/quilkin-proto/src/generated/quilkin/filters.rs index 1583618832..5af21ad33c 100644 --- a/crates/quilkin-proto/src/generated/quilkin/filters.rs +++ b/crates/quilkin-proto/src/generated/quilkin/filters.rs @@ -1,6 +1,7 @@ pub mod capture; pub mod concatenate; pub mod debug; +pub mod decryptor; pub mod drop; pub mod firewall; pub mod load_balancer; diff --git a/crates/quilkin-proto/src/generated/quilkin/filters/decryptor.rs b/crates/quilkin-proto/src/generated/quilkin/filters/decryptor.rs new file mode 100644 index 0000000000..32a5a9d4fd --- /dev/null +++ b/crates/quilkin-proto/src/generated/quilkin/filters/decryptor.rs @@ -0,0 +1 @@ +pub mod v1alpha1; diff --git a/crates/quilkin-proto/src/generated/quilkin/filters/decryptor/v1alpha1.rs b/crates/quilkin-proto/src/generated/quilkin/filters/decryptor/v1alpha1.rs new file mode 100644 index 0000000000..1e3359b14a --- /dev/null +++ b/crates/quilkin-proto/src/generated/quilkin/filters/decryptor/v1alpha1.rs @@ -0,0 +1,38 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct Decryptor { + #[prost(bytes = "vec", tag = "1")] + pub key: ::prost::alloc::vec::Vec, + #[prost(enumeration = "decryptor::Mode", tag = "2")] + pub mode: i32, + #[prost(message, optional, tag = "3")] + pub data_key: ::core::option::Option<::prost::alloc::string::String>, + #[prost(message, optional, tag = "4")] + pub nonce_key: ::core::option::Option<::prost::alloc::string::String>, +} +/// Nested message and enum types in `Decryptor`. +pub mod decryptor { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[repr(i32)] + pub enum Mode { + Destination = 0, + } + impl Mode { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Destination => "Destination", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "Destination" => Some(Self::Destination), + _ => None, + } + } + } +} diff --git a/deny.toml b/deny.toml index 8441d71451..ba58104a9c 100644 --- a/deny.toml +++ b/deny.toml @@ -54,6 +54,7 @@ skip = [ skip-tree = [ { crate = "thiserror@1.0.69", reason = "many crates use this old version" }, { crate = "hashbrown@0.15.5", reason = "many crates this old version" }, + { crate = "sha2@0.10.9", reason = "old version that uses multiple other old crates" }, ] [[bans.features]] @@ -82,5 +83,4 @@ exceptions = [ # This license should not really be used for code, but here we are { crate = "notify", allow = ["CC0-1.0"] }, { crate = "webpki-roots", allow = ["CDLA-Permissive-2.0"] }, - { crate = "ar_archive_writer", allow = ["Apache-2.0 WITH LLVM-exception"] } ] diff --git a/docs/src/filters/decryptor.md b/docs/src/filters/decryptor.md new file mode 100644 index 0000000000..32d4f5e39b --- /dev/null +++ b/docs/src/filters/decryptor.md @@ -0,0 +1,47 @@ +# Decryptor + +The `Decryptor` filter's job is to decrypt a portion of a client (downstream) packet as an IPv4 or IPv6 address and port to forward the packet to. + +## Filter name + +```text +quilkin.filters.decryptor.v1alpha1.Decryptor +``` + +## Configuration Examples + +```rust +# let yaml = " +version: v1alpha1 +filters: + - name: quilkin.filters.capture.v1alpha1.Capture + config: + suffix: + size: 24 + remove: true + - name: quilkin.filters.decryptor.v1alpha1.Decryptor + config: + # the (binary) decryption key + key: keygoeshere + # the decryption mode, currently only `Destination` is supported, which + # will be interpreted as either an IPv4 or IPv6 address and a port which + # will be used as the destination address of the packet + mode: Destination + # the name of the metadata key to retrieve the data being decrypted. + # defaults to `quilkin.dev/capture` unless otherwise specified + data_key: quilkin.dev/capture + # the name of the metadata key to retrieve the nonce key from + nonce_key: quilkin.dev/nonce +clusters: + - endpoints: + - address: 127.0.0.1:7001 +# "; +# let config = quilkin::config::Config::from_reader(yaml.as_bytes()).unwrap(); +# assert_eq!(config.filters.load().len(), 2); +``` + +## Configuration Options ([Rust Doc](../../api/quilkin/filters/decryptor/struct.Decryptor.html)) + +```yaml +{{#include ../../../target/quilkin.filters.decryptor.v1alpha1.yaml}} +``` diff --git a/proto/quilkin/filters/decryptor/v1alpha1/decryptor.proto b/proto/quilkin/filters/decryptor/v1alpha1/decryptor.proto new file mode 100644 index 0000000000..b8dae62093 --- /dev/null +++ b/proto/quilkin/filters/decryptor/v1alpha1/decryptor.proto @@ -0,0 +1,32 @@ +/* + * Copyright 2024 Google LLC + * + * 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 quilkin.filters.decryptor.v1alpha1; + +import "google/protobuf/wrappers.proto"; + +message Decryptor { + enum Mode { + Destination = 0; + } + + bytes key = 1; + Mode mode = 2; + google.protobuf.StringValue data_key = 3; + google.protobuf.StringValue nonce_key = 4; +} diff --git a/src/filters.rs b/src/filters.rs index 163d461f43..7623b22fad 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -27,6 +27,7 @@ mod write; pub mod capture; pub mod concatenate; pub mod debug; +pub mod decryptor; pub mod drop; pub mod firewall; pub mod load_balancer; @@ -53,6 +54,7 @@ pub use self::{ chain::FilterChain, concatenate::Concatenate, debug::Debug, + decryptor::Decryptor, drop::Drop, error::{ConvertProtoConfigError, CreationError, FilterError}, factory::{CreateFilterArgs, DynFilterFactory, FilterFactory, FilterInstance}, @@ -81,6 +83,7 @@ pub enum FilterKind { Capture, Concatenate, Debug, + Decryptor, Drop, Firewall, LoadBalancer, diff --git a/src/filters/decryptor.rs b/src/filters/decryptor.rs new file mode 100644 index 0000000000..bc0defdabb --- /dev/null +++ b/src/filters/decryptor.rs @@ -0,0 +1,319 @@ +/* + * Copyright 2024 Google LLC + * + * 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 chacha20::cipher::*; +use serde::{Deserialize, Serialize}; + +use crate::{ + filters::{capture::CAPTURED_BYTES, prelude::*}, + net::endpoint::metadata, +}; + +use quilkin_xds::generated::quilkin::filters::decryptor::v1alpha1 as proto; + +/// The default key under which the [`Decryptor`] filter reads the nonce. +/// - **Type** `Vec` +pub const NONCE_KEY: &str = "quilkin.dev/nonce"; + +/// Filter that decrypts the destination IP and port from the packet +pub struct Decryptor { + config: Config, +} + +const DESTINATION_MIN: usize = 6; +const DESTINATION_MAX: usize = 18; + +impl Decryptor { + #[inline] + fn decode_chacha20(&self, nonce: [u8; 12], data: &mut [u8]) { + let mut cipher = chacha20::ChaCha20::new(&self.config.key.into(), &nonce.into()); + cipher.apply_keystream(data); + } + + #[inline] + fn decode_destination(data: &[u8]) -> std::net::SocketAddr { + use std::net; + + let port = (data[data.len() - 2] as u16) << 8 | data[data.len() - 1] as u16; + + match data.len() { + DESTINATION_MIN => net::SocketAddr::V4(net::SocketAddrV4::new( + net::Ipv4Addr::from_octets(data[..4].try_into().unwrap()), + port, + )), + DESTINATION_MAX => net::SocketAddr::V6(net::SocketAddrV6::new( + net::Ipv6Addr::from_octets(data[..16].try_into().unwrap()), + port, + 0, + 0, + )), + _ => unreachable!(), + } + } +} + +impl StaticFilter for Decryptor { + const NAME: &'static str = "quilkin.filters.decryptor.v1alpha1.Decryptor"; + type Configuration = Config; + type BinaryConfiguration = proto::Decryptor; + + fn try_from_config(config: Option) -> Result { + Ok(Self { + config: Self::ensure_config_exists(config)?, + }) + } +} + +impl Filter for Decryptor { + fn read(&self, ctx: &mut ReadContext<'_, P>) -> Result<(), FilterError> { + match ( + ctx.metadata.get(&self.config.data_key), + ctx.metadata.get(&self.config.nonce_key), + ) { + (Some(metadata::Value::Bytes(data)), Some(metadata::Value::Bytes(nonce))) => { + let nonce = <[u8; 12]>::try_from(&**nonce) + .map_err(|_e| FilterError::Custom("Expected 12 byte nonce"))?; + + match self.config.mode { + Mode::Destination => { + // We can avoid a heap allocation since we know the maximum size of the encrypted payload + let mut edata = [0u8; DESTINATION_MAX]; + + let edata = match data.len() { + DESTINATION_MIN => { + edata[..DESTINATION_MIN].copy_from_slice(data); + &mut edata[..DESTINATION_MIN] + } + DESTINATION_MAX => { + edata[..DESTINATION_MAX].copy_from_slice(data); + &mut edata[..DESTINATION_MAX] + } + _ => { + return Err(FilterError::Custom( + "Invalid decoded data length, must be `6` or `18` bytes.", + )); + } + }; + + self.decode_chacha20(nonce, edata); + ctx.destinations + .push(Self::decode_destination(edata).into()); + Ok(()) + } + } + } + (Some(metadata::Value::Bytes(_)), Some(_)) => { + Err(FilterError::Custom("expected `bytes` value in nonce key")) + } + (Some(_), Some(metadata::Value::Bytes(_))) => { + Err(FilterError::Custom("expected `bytes` value in data key")) + } + (Some(_), Some(_)) => Err(FilterError::Custom( + "expected `bytes` value in data and nonce key", + )), + (Some(_), None) => Err(FilterError::Custom("Nonce key is missing")), + (None, Some(_)) => Err(FilterError::Custom("Data key is missing")), + (None, None) => Err(FilterError::Custom("Nonce and data key is missing")), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, schemars::JsonSchema)] +pub struct Config { + #[serde(deserialize_with = "deserialize", serialize_with = "serialize")] + #[schemars(with = "String")] + pub key: [u8; 32], + /// the key to use when retrieving the data from the Filter's dynamic metadata + #[serde(rename = "dataKey", default = "default_data_key")] + pub data_key: metadata::Key, + #[serde(rename = "nonceKey", default = "default_nonce_key")] + pub nonce_key: metadata::Key, + pub mode: Mode, +} + +/// Default value for [`Config::data_key`] +fn default_data_key() -> metadata::Key { + metadata::Key::from_static(CAPTURED_BYTES) +} + +/// Default value for [`Config::nonce_key`] +fn default_nonce_key() -> metadata::Key { + metadata::Key::from_static(NONCE_KEY) +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, schemars::JsonSchema)] +pub enum Mode { + /// Value is expected to be a IP:port pair to be used for setting the destination. + Destination, +} + +impl From for proto::decryptor::Mode { + fn from(mode: Mode) -> Self { + match mode { + Mode::Destination => Self::Destination, + } + } +} + +impl From for Mode { + fn from(mode: proto::decryptor::Mode) -> Self { + match mode { + proto::decryptor::Mode::Destination => Self::Destination, + } + } +} + +impl TryFrom for Mode { + type Error = ConvertProtoConfigError; + fn try_from(mode: i32) -> Result { + match mode { + 0 => Ok(Self::Destination), + _ => Err(ConvertProtoConfigError::missing_field("mode")), + } + } +} + +fn deserialize<'de, D>(de: D) -> Result<[u8; 32], D::Error> +where + D: serde::Deserializer<'de>, +{ + let string = String::deserialize(de)?; + + crate::codec::base64::decode(string) + .map_err(serde::de::Error::custom)? + .try_into() + .map_err(|_e| serde::de::Error::custom("invalid key, expected 32 bytes")) +} + +fn serialize(value: &[u8; 32], ser: S) -> Result +where + S: serde::Serializer, +{ + crate::codec::base64::encode(value).serialize(ser) +} + +impl From for proto::Decryptor { + fn from(config: Config) -> Self { + Self { + key: config.key.into(), + mode: proto::decryptor::Mode::from(config.mode).into(), + data_key: Some(config.data_key.to_string()), + nonce_key: Some(config.nonce_key.to_string()), + } + } +} + +impl TryFrom for Config { + type Error = ConvertProtoConfigError; + + fn try_from(p: proto::Decryptor) -> Result { + Ok(Self { + key: p.key.try_into().map_err(|_e| { + ConvertProtoConfigError::new( + "invalid key, expected 32 bytes", + Some("private_key".into()), + ) + })?, + mode: p.mode.try_into()?, + data_key: p.data_key.map_or_else(default_data_key, metadata::Key::new), + nonce_key: p + .nonce_key + .map_or_else(default_nonce_key, metadata::Key::new), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::alloc_buffer; + + #[test] + fn decryptor() { + let endpoints = crate::net::cluster::ClusterMap::default(); + let mut dest = Vec::new(); + let mut ctx = ReadContext::new( + &endpoints, + "0.0.0.0:0".parse().unwrap(), + alloc_buffer(b"hello"), + &mut dest, + ); + + let key = [0x42u8; 32]; + let nonce = [0x22u8; 12]; + + ctx.metadata.insert( + NONCE_KEY.into(), + bytes::Bytes::from(Vec::from(nonce)).into(), + ); + + let config = Config { + data_key: CAPTURED_BYTES.into(), + nonce_key: NONCE_KEY.into(), + key, + mode: Mode::Destination, + }; + + let filter = Decryptor::from_config(config.into()); + + // ipv4 + { + let expected = std::net::SocketAddrV4::new(std::net::Ipv4Addr::new(127, 0, 0, 1), 8080); + + let mut data = Vec::new(); + data.extend(expected.ip().to_bits().to_be_bytes()); + data.extend(expected.port().to_be_bytes()); + + let mut cipher = chacha20::ChaCha20::new(&key.into(), &nonce.into()); + cipher.apply_keystream(&mut data); + + ctx.metadata + .insert(CAPTURED_BYTES.into(), bytes::Bytes::from(data).into()); + + filter.read(&mut ctx).unwrap(); + assert_eq!( + std::net::SocketAddr::from(expected), + ctx.destinations.pop().unwrap().to_socket_addr().unwrap() + ); + } + + // ipv6 + { + let expected = std::net::SocketAddrV6::new( + std::net::Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 8), + 45000, + 0, + 0, + ); + + let mut data = Vec::new(); + data.extend(expected.ip().octets()); + data.extend(expected.port().to_be_bytes()); + + let mut cipher = chacha20::ChaCha20::new(&key.into(), &nonce.into()); + cipher.apply_keystream(&mut data); + + ctx.metadata + .insert(CAPTURED_BYTES.into(), bytes::Bytes::from(data).into()); + + filter.read(&mut ctx).unwrap(); + assert_eq!( + std::net::SocketAddr::from(expected), + ctx.destinations.pop().unwrap().to_socket_addr().unwrap() + ); + } + } +}