diff --git a/rust/Cargo.lock b/rust/Cargo.lock index c3713de..fe873ff 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -17,6 +17,17 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "bindgen" version = "0.70.1" @@ -43,6 +54,12 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + [[package]] name = "cexpr" version = "0.6.0" @@ -69,6 +86,43 @@ dependencies = [ "libloading", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "downcast" version = "0.11.0" @@ -81,10 +135,22 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "envoy-proxy-dynamic-modules-rust-sdk" version = "0.1.0" -source = "git+https://github.com/envoyproxy/envoy?rev=6d9bb7d9a85d616b220d1f8fe67b61f82bbdb8d3#6d9bb7d9a85d616b220d1f8fe67b61f82bbdb8d3" +source = "git+https://github.com/envoyproxy/envoy?rev=f0e51db62b58196f012f93f20899d86ec81c63e6#f0e51db62b58196f012f93f20899d86ec81c63e6" dependencies = [ "bindgen", "mockall", @@ -94,9 +160,13 @@ dependencies = [ name = "envoy-proxy-dynamic-modules-rust-sdk-examples" version = "0.1.0" dependencies = [ + "dashmap", "envoy-proxy-dynamic-modules-rust-sdk", + "hickory-proto", "matchers", - "rand", + "once_cell", + "parking_lot", + "rand 0.9.1", "serde", "serde_json", "tempfile", @@ -118,12 +188,72 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fragile" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.3.3" @@ -133,7 +263,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -142,6 +272,150 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hickory-proto" +version = "0.24.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.8.5", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "itertools" version = "0.13.0" @@ -170,7 +444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.53.2", ] [[package]] @@ -179,6 +453,21 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.27" @@ -206,6 +495,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + [[package]] name = "mockall" version = "0.13.1" @@ -248,6 +548,56 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -317,14 +667,35 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ - "rand_chacha", - "rand_core", + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] @@ -334,7 +705,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -343,7 +723,16 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom", + "getrandom 0.3.3", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", ] [[package]] @@ -400,6 +789,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.219" @@ -438,6 +833,34 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "syn" version = "2.0.104" @@ -449,6 +872,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tempfile" version = "3.20.0" @@ -456,7 +890,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.59.0", @@ -468,12 +902,126 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" @@ -483,6 +1031,12 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.59.0" @@ -501,6 +1055,15 @@ dependencies = [ "windows-targets 0.53.2", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -638,6 +1201,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.26" @@ -657,3 +1249,57 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 7fc60b0..c964fac 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -7,11 +7,15 @@ repository = "https://github.com/envoyproxy/dynamic-modules-example" [dependencies] # The SDK version must match the Envoy version due to the strict compatibility requirements. -envoy-proxy-dynamic-modules-rust-sdk = { git = "https://github.com/envoyproxy/envoy", rev = "6d9bb7d9a85d616b220d1f8fe67b61f82bbdb8d3" } +envoy-proxy-dynamic-modules-rust-sdk = { git = "https://github.com/envoyproxy/envoy", rev = "f0e51db62b58196f012f93f20899d86ec81c63e6" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" rand = "0.9.0" matchers = "0.2.0" +dashmap = "6.1.0" +once_cell = "1.20.2" +hickory-proto = "0.24" +parking_lot = "0.12" [dev-dependencies] tempfile = "3.16.0" diff --git a/rust/src/egress_policies/README.md b/rust/src/egress_policies/README.md new file mode 100644 index 0000000..93c4d89 Binary files /dev/null and b/rust/src/egress_policies/README.md differ diff --git a/rust/src/egress_policies/diagram.png b/rust/src/egress_policies/diagram.png new file mode 100644 index 0000000..b2aa790 Binary files /dev/null and b/rust/src/egress_policies/diagram.png differ diff --git a/rust/src/egress_policies/dns_gateway.rs b/rust/src/egress_policies/dns_gateway.rs new file mode 100644 index 0000000..5234d73 --- /dev/null +++ b/rust/src/egress_policies/dns_gateway.rs @@ -0,0 +1,575 @@ +use envoy_proxy_dynamic_modules_rust_sdk::*; +use hickory_proto::op::{Message, MessageType, ResponseCode}; +use hickory_proto::rr::{Name, RData, Record, RecordType}; +use hickory_proto::serialize::binary::{BinDecodable, BinDecoder}; +use std::net::Ipv4Addr; + +use super::virtual_ip_cache::{get_cache, init_cache, EgressPolicy}; + +pub struct DnsGatewayFilterConfig { + policies: Vec, +} + +struct PolicyMatcher { + domain_pattern: String, + metadata: std::collections::HashMap, +} + +impl PolicyMatcher { + /// Matches a domain against this policy's pattern. + /// Supports wildcard patterns like "*.aws.com". + /// Wildcard only matches ONE level: *.aws.com matches api.aws.com but NOT sub.api.aws.com + fn matches(&self, domain: &str) -> bool { + if self.domain_pattern.starts_with("*.") { + let suffix = &self.domain_pattern[1..]; // Include the "." in suffix: ".aws.com" + + // Check if domain ends with suffix + if !domain.ends_with(suffix) || domain.len() <= suffix.len() { + return false; + } + + // Extract the prefix (the part before the suffix) + let prefix = &domain[..domain.len() - suffix.len()]; + + // Wildcard should only match ONE level, so prefix must not contain dots + !prefix.contains('.') + } else { + domain == self.domain_pattern // Exact match + } + } +} + +impl DnsGatewayFilterConfig { + pub fn new(config: &[u8]) -> Option { + // Parse config as JSON. The config arrives as a JSON-serialized google.protobuf.Any. + // Supported wrappers: + // - StringValue: {"@type":"...StringValue", "value":""} + // - Struct: {"@type":"...Struct", "value":{"base_ip":"...", ...}} + let config_str = std::str::from_utf8(config).ok()?; + let outer_json: serde_json::Value = serde_json::from_str(config_str).ok()?; + + let config_json: serde_json::Value = match &outer_json["value"] { + // StringValue: "value" is a JSON string that we parse again. + serde_json::Value::String(s) => serde_json::from_str(s).ok()?, + // Struct: "value" is already an object with our config fields. + serde_json::Value::Object(_) => outer_json["value"].clone(), + // Fallback: use the outer object directly. + _ => outer_json, + }; + + // Parse base_ip from config (required) + let base_ip_str = config_json["base_ip"] + .as_str() + .expect("base_ip is required for DNS gateway"); + + let base_ip: Ipv4Addr = base_ip_str.parse().ok()?; + let base_ip_u32 = u32::from(base_ip); + + // Parse prefix_len from config (required) + let prefix_len = config_json["prefix_len"] + .as_u64() + .expect("prefix_len is required for DNS gateway") as u8; + + if !(1..=32).contains(&prefix_len) { + panic!("prefix_len must be between 1 and 32, got {}", prefix_len); + } + + // Initialize the cache (first call wins, subsequent calls are ignored) + init_cache(base_ip_u32, prefix_len); + + // Parse policies + let policies_array = config_json["policies"].as_array()?; + let mut policies = Vec::new(); + + for policy_json in policies_array { + let domain_pattern = policy_json["domain"].as_str()?.to_string(); + let metadata_obj = policy_json["metadata"].as_object()?; + + let mut metadata = std::collections::HashMap::new(); + for (key, value) in metadata_obj { + if let Some(value_str) = value.as_str() { + metadata.insert(key.clone(), value_str.to_string()); + } + } + + policies.push(PolicyMatcher { + domain_pattern, + metadata, + }); + } + + envoy_log_info!("DnsGateway initialized with {} policies", policies.len()); + + Some(DnsGatewayFilterConfig { policies }) + } +} + +impl UdpListenerFilterConfig for DnsGatewayFilterConfig { + fn new_udp_listener_filter(&self, _envoy: &mut ELF) -> Box> { + Box::new(DnsGatewayFilter { + policies: self + .policies + .iter() + .map(|p| PolicyMatcher { + domain_pattern: p.domain_pattern.clone(), + metadata: p.metadata.clone(), + }) + .collect(), + }) + } +} + +struct DnsGatewayFilter { + policies: Vec, +} + +impl UdpListenerFilter for DnsGatewayFilter { + fn on_data( + &mut self, + envoy_filter: &mut ELF, + ) -> abi::envoy_dynamic_module_type_on_udp_listener_filter_status { + let (chunks, total_length) = envoy_filter.get_datagram_data(); + envoy_log_info!( + "dns_gateway: received UDP datagram, {} bytes, {} chunks", + total_length, + chunks.len() + ); + let mut data = Vec::new(); + for chunk in &chunks { + data.extend_from_slice(chunk.as_slice()); + } + + let peer = envoy_filter.get_peer_address(); + envoy_log_info!("dns_gateway: peer address: {:?}", peer); + + let mut decoder: BinDecoder<'_> = BinDecoder::new(&data); + let query_message = match Message::read(&mut decoder) { + Ok(msg) => msg, + Err(e) => { + envoy_log_warn!("dns_gateway: failed to parse DNS query: {}", e); + return abi::envoy_dynamic_module_type_on_udp_listener_filter_status::Continue; + } + }; + + envoy_log_info!( + "dns_gateway: parsed DNS message id={}, type={:?}, queries={}", + query_message.id(), + query_message.message_type(), + query_message.queries().len() + ); + + if query_message.message_type() != MessageType::Query { + envoy_log_warn!("dns_gateway: received non-query DNS message"); + return abi::envoy_dynamic_module_type_on_udp_listener_filter_status::Continue; + } + + let question = match query_message.queries().first() { + Some(q) => q, + None => { + envoy_log_warn!("dns_gateway: DNS query has no questions"); + return abi::envoy_dynamic_module_type_on_udp_listener_filter_status::Continue; + } + }; + + let domain_raw = question.name().to_utf8(); + // DNS names are fully qualified with a trailing dot (e.g. "api.aws.com."). + // Strip it so our wildcard patterns like "*.aws.com" match correctly. + let domain = domain_raw + .strip_suffix('.') + .unwrap_or(&domain_raw) + .to_string(); + + envoy_log_info!( + "dns_gateway: {:?} record query for domain: {} (raw: {})", + question.query_type(), + domain, + domain_raw + ); + + // Check if this domain matches any of our policies + let matched_policy = self.policies.iter().find(|p| p.matches(&domain)); + + if let Some(policy_matcher) = matched_policy { + envoy_log_info!( + "dns_gateway: matched policy pattern '{}' for domain '{}'", + policy_matcher.domain_pattern, + domain + ); + + // Craft the appropriate response based on query type + let response_bytes = match question.query_type() { + RecordType::A => { + // Allocate virtual IP for A record queries + let policy = EgressPolicy { + domain: domain.clone(), + metadata: policy_matcher.metadata.clone(), + }; + + let cache = get_cache(); + let virtual_ip = cache.allocate(policy); + + envoy_log_info!( + "dns_gateway: allocated virtual IP {} for domain {}", + virtual_ip, + domain + ); + + // Craft DNS A response with virtual IP + match build_dns_response(&query_message, question.name(), virtual_ip) { + Ok(bytes) => { + envoy_log_info!( + "dns_gateway: crafted DNS A response, {} bytes", + bytes.len() + ); + bytes + } + Err(e) => { + envoy_log_error!("dns_gateway: failed to craft DNS A response: {}", e); + return abi::envoy_dynamic_module_type_on_udp_listener_filter_status::Continue; + } + } + } + RecordType::AAAA => { + // Return NODATA response for AAAA queries (no IPv6 available) + envoy_log_info!( + "dns_gateway: returning NODATA for AAAA query (no IPv6 available)" + ); + + match build_nodata_response(&query_message) { + Ok(bytes) => { + envoy_log_info!( + "dns_gateway: crafted NODATA response, {} bytes", + bytes.len() + ); + bytes + } + Err(e) => { + envoy_log_error!("dns_gateway: failed to craft NODATA response: {}", e); + return abi::envoy_dynamic_module_type_on_udp_listener_filter_status::Continue; + } + } + } + // For other query types (MX, TXT, CNAME, etc.), return NODATA since we only have A records + _ => { + envoy_log_info!( + "dns_gateway: returning NODATA for {:?} query (only A records supported)", + question.query_type() + ); + + match build_nodata_response(&query_message) { + Ok(bytes) => { + envoy_log_info!( + "dns_gateway: crafted NODATA response, {} bytes", + bytes.len() + ); + bytes + } + Err(e) => { + envoy_log_error!("dns_gateway: failed to craft NODATA response: {}", e); + return abi::envoy_dynamic_module_type_on_udp_listener_filter_status::Continue; + } + } + } + }; + + if let Some((peer_addr, peer_port)) = peer { + envoy_log_info!( + "dns_gateway: sending {} byte response to {}:{}", + response_bytes.len(), + peer_addr, + peer_port + ); + if !envoy_filter.send_datagram(&response_bytes, &peer_addr, peer_port) { + envoy_log_error!( + "dns_gateway: failed to send datagram to {}:{}", + peer_addr, + peer_port + ); + } + } else { + envoy_log_error!("dns_gateway: no peer address available, cannot send response"); + } + + return abi::envoy_dynamic_module_type_on_udp_listener_filter_status::StopIteration; + } else { + envoy_log_info!("dns_gateway: no policy matched for domain: {}", domain); + } + + // No match — let the packet continue to the next filter or return empty response + abi::envoy_dynamic_module_type_on_udp_listener_filter_status::Continue + } +} + +fn build_dns_response( + query_message: &Message, + name: &Name, + ip: Ipv4Addr, +) -> Result, Box> { + // Clone the query and convert it to a response + let mut response = query_message.clone(); + + // Set response flags + response.set_message_type(MessageType::Response); + response.set_response_code(ResponseCode::NoError); + response.set_recursion_available(true); + + // Question section is already copied from the query via clone() + + // Add answer record + let mut record = Record::new(); + record.set_name(name.clone()); + record.set_record_type(RecordType::A); + record.set_ttl(600); // 10 min TTL + record.set_data(Some(RData::A(ip.into()))); + + response.add_answer(record); + + let bytes = response.to_vec()?; + Ok(bytes) +} + +// For now, no IPv6 support +fn build_nodata_response(query_message: &Message) -> Result, Box> { + // Clone the query and convert it to a NODATA response + let mut response = query_message.clone(); + + // Set response flags + response.set_message_type(MessageType::Response); + response.set_response_code(ResponseCode::NoError); + response.set_recursion_available(true); + + // Question section is already copied from the query via clone() + // No answer records - this is a NODATA response indicating the domain exists but has no records of this type + + let bytes = response.to_vec()?; + Ok(bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_policy_matcher_wildcard() { + let matcher = PolicyMatcher { + domain_pattern: "*.aws.com".to_string(), + metadata: std::collections::HashMap::new(), + }; + + // Should match + assert!(matcher.matches("api.aws.com")); + assert!(matcher.matches("s3.aws.com")); + assert!(matcher.matches("lambda.aws.com")); + + // Should NOT match + assert!(!matcher.matches("aws.com")); // Base domain + assert!(!matcher.matches("sub.api.aws.com")); // Too many levels + assert!(!matcher.matches("aws.com.evil.com")); // Different domain + assert!(!matcher.matches("api.aws.org")); // Different TLD + } + + #[test] + fn test_policy_matcher_exact() { + let matcher = PolicyMatcher { + domain_pattern: "api.example.com".to_string(), + metadata: std::collections::HashMap::new(), + }; + + // Should match + assert!(matcher.matches("api.example.com")); + + // Should NOT match + assert!(!matcher.matches("www.api.example.com")); + assert!(!matcher.matches("example.com")); + assert!(!matcher.matches("api.example.org")); + } + + #[test] + fn test_policy_matcher_wildcard_edge_cases() { + let matcher = PolicyMatcher { + domain_pattern: "*.com".to_string(), + metadata: std::collections::HashMap::new(), + }; + + assert!(matcher.matches("example.com")); + assert!(matcher.matches("test.com")); + assert!(!matcher.matches("com")); // Base TLD + assert!(!matcher.matches("sub.example.com")); // Too many levels + } + + #[test] + fn test_config_parsing_valid_stringvalue() { + let config = r#"{ + "@type": "type.googleapis.com/google.protobuf.StringValue", + "value": "{\"base_ip\": \"10.10.0.0\", \"prefix_len\": 24, \"policies\": [{\"domain\": \"*.example.com\", \"metadata\": {\"cluster\": \"test\"}}]}" + }"#; + + let result = DnsGatewayFilterConfig::new(config.as_bytes()); + assert!(result.is_some()); + let config = result.unwrap(); + assert_eq!(config.policies.len(), 1); + assert_eq!(config.policies[0].domain_pattern, "*.example.com"); + } + + #[test] + fn test_config_parsing_valid_struct() { + let config = r#"{ + "@type": "type.googleapis.com/google.protobuf.Struct", + "value": { + "base_ip": "10.10.0.0", + "prefix_len": 24, + "policies": [ + { + "domain": "*.aws.com", + "metadata": { + "cluster": "aws_cluster", + "region": "us-east-1" + } + } + ] + } + }"#; + + let result = DnsGatewayFilterConfig::new(config.as_bytes()); + assert!(result.is_some()); + let config = result.unwrap(); + assert_eq!(config.policies.len(), 1); + assert_eq!(config.policies[0].domain_pattern, "*.aws.com"); + assert_eq!( + config.policies[0].metadata.get("cluster").unwrap(), + "aws_cluster" + ); + assert_eq!( + config.policies[0].metadata.get("region").unwrap(), + "us-east-1" + ); + } + + #[test] + fn test_config_parsing_multiple_policies() { + let config = r#"{ + "base_ip": "10.10.0.0", + "prefix_len": 16, + "policies": [ + {"domain": "*.aws.com", "metadata": {"cluster": "aws"}}, + {"domain": "*.google.com", "metadata": {"cluster": "google"}}, + {"domain": "exact.example.com", "metadata": {"cluster": "exact"}} + ] + }"#; + + let result = DnsGatewayFilterConfig::new(config.as_bytes()); + assert!(result.is_some()); + let config = result.unwrap(); + assert_eq!(config.policies.len(), 3); + } + + #[test] + #[should_panic(expected = "base_ip is required")] + fn test_config_parsing_missing_base_ip() { + let config = r#"{ + "prefix_len": 24, + "policies": [] + }"#; + + DnsGatewayFilterConfig::new(config.as_bytes()); + } + + #[test] + #[should_panic(expected = "prefix_len is required")] + fn test_config_parsing_missing_prefix_len() { + let config = r#"{ + "base_ip": "10.10.0.0", + "policies": [] + }"#; + + DnsGatewayFilterConfig::new(config.as_bytes()); + } + + #[test] + #[should_panic(expected = "prefix_len must be between 1 and 32")] + fn test_config_parsing_invalid_prefix_len() { + let config = r#"{ + "base_ip": "10.10.0.0", + "prefix_len": 33, + "policies": [] + }"#; + + DnsGatewayFilterConfig::new(config.as_bytes()); + } + + #[test] + fn test_config_parsing_invalid_json() { + let config = b"invalid json {"; + let result = DnsGatewayFilterConfig::new(config); + assert!(result.is_none()); + } + + #[test] + fn test_domain_stripping_trailing_dot() { + let domain_raw = "api.aws.com."; + let domain = domain_raw.strip_suffix('.').unwrap_or(domain_raw); + assert_eq!(domain, "api.aws.com"); + } + + #[test] + fn test_domain_without_trailing_dot() { + let domain_raw = "api.aws.com"; + let domain = domain_raw.strip_suffix('.').unwrap_or(domain_raw); + assert_eq!(domain, "api.aws.com"); + } + + #[test] + fn test_dns_response_building() { + // Create a simple query message + let mut query = Message::new(); + query.set_id(12345); + query.set_message_type(MessageType::Query); + query.set_recursion_desired(true); + + let name = Name::from_utf8("test.example.com").unwrap(); + let ip = Ipv4Addr::new(10, 10, 0, 1); + + // Build response + let result = build_dns_response(&query, &name, ip); + assert!(result.is_ok()); + + let response_bytes = result.unwrap(); + assert!(!response_bytes.is_empty()); + + // Parse the response to verify structure + let mut decoder = BinDecoder::new(&response_bytes); + let response = Message::read(&mut decoder).unwrap(); + + assert_eq!(response.id(), 12345); + assert_eq!(response.message_type(), MessageType::Response); + assert_eq!(response.response_code(), ResponseCode::NoError); + assert!(response.recursion_available()); + assert_eq!(response.answers().len(), 1); + } + + #[test] + fn test_nodata_response_building() { + // Create a query for AAAA record + let mut query = Message::new(); + query.set_id(54321); + query.set_message_type(MessageType::Query); + query.set_recursion_desired(false); + + // Build NODATA response + let result = build_nodata_response(&query); + assert!(result.is_ok()); + + let response_bytes = result.unwrap(); + assert!(!response_bytes.is_empty()); + + // Parse the response to verify structure + let mut decoder = BinDecoder::new(&response_bytes); + let response = Message::read(&mut decoder).unwrap(); + + assert_eq!(response.id(), 54321); + assert_eq!(response.message_type(), MessageType::Response); + assert_eq!(response.response_code(), ResponseCode::NoError); + assert!(response.recursion_available()); + assert_eq!(response.answers().len(), 0); // NODATA = no answers + } +} diff --git a/rust/src/egress_policies/hostname_lookup.rs b/rust/src/egress_policies/hostname_lookup.rs new file mode 100644 index 0000000..440d243 --- /dev/null +++ b/rust/src/egress_policies/hostname_lookup.rs @@ -0,0 +1,192 @@ +//! Network filter for hostname lookup from virtual IP. + +use envoy_proxy_dynamic_modules_rust_sdk::*; +use std::net::Ipv4Addr; + +use super::virtual_ip_cache::get_cache; + +pub struct HostnameLookupFilterConfig {} + +impl HostnameLookupFilterConfig { + pub fn new(_config: &[u8]) -> Option { + envoy_log_info!("HostnameLookup filter initialized"); + Some(HostnameLookupFilterConfig {}) + } +} + +impl NetworkFilterConfig for HostnameLookupFilterConfig { + fn new_network_filter(&self, _envoy: &mut ENF) -> Box> { + Box::new(HostnameLookupFilter {}) + } +} + +struct HostnameLookupFilter; + +impl NetworkFilter for HostnameLookupFilter { + fn on_new_connection( + &mut self, + envoy_filter: &mut ENF, + ) -> abi::envoy_dynamic_module_type_on_network_filter_data_status { + let (ip_str, port) = envoy_filter.get_local_address(); + envoy_log_info!( + "hostname_lookup: new connection, local_address={}:{}", + ip_str, + port + ); + + let ip: Ipv4Addr = match ip_str.parse() { + Ok(ip) => ip, + Err(_) => { + envoy_log_warn!( + "hostname_lookup: failed to parse destination IP: {}", + ip_str + ); + return abi::envoy_dynamic_module_type_on_network_filter_data_status::Continue; + } + }; + + let cache = get_cache(); + let policy = match cache.lookup(ip) { + Some(p) => p, + None => { + envoy_log_warn!( + "hostname_lookup: no policy found for virtual IP: {} (cache miss)", + ip + ); + return abi::envoy_dynamic_module_type_on_network_filter_data_status::Continue; + } + }; + + envoy_log_info!( + "hostname_lookup: cache hit for virtual IP {}: domain={}, metadata keys=[{}]", + ip, + policy.domain, + policy + .metadata + .keys() + .cloned() + .collect::>() + .join(", ") + ); + + let hostname_key = "envoy.wildcard.hostname"; + if !envoy_filter.set_filter_state_bytes(hostname_key.as_bytes(), policy.domain.as_bytes()) { + envoy_log_error!("hostname_lookup: failed to set filter state for hostname"); + } else { + envoy_log_info!( + "hostname_lookup: set filter state: {} = {}", + hostname_key, + policy.domain + ); + } + + for (key, value) in &policy.metadata { + let filter_state_key = format!("envoy.wildcard.metadata.{}", key); + + if !envoy_filter.set_filter_state_bytes(filter_state_key.as_bytes(), value.as_bytes()) { + envoy_log_error!( + "hostname_lookup: failed to set filter state for key: {}", + filter_state_key + ); + } else { + envoy_log_info!( + "hostname_lookup: set filter state: {} = {}", + filter_state_key, + value + ); + } + } + + abi::envoy_dynamic_module_type_on_network_filter_data_status::Continue + } + + 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 { + abi::envoy_dynamic_module_type_on_network_filter_data_status::Continue + } + + fn on_write( + &mut self, + _envoy_filter: &mut ENF, + _data_length: usize, + _end_stream: bool, + ) -> abi::envoy_dynamic_module_type_on_network_filter_data_status { + abi::envoy_dynamic_module_type_on_network_filter_data_status::Continue + } + + fn on_event( + &mut self, + _envoy_filter: &mut ENF, + event: abi::envoy_dynamic_module_type_network_connection_event, + ) { + match event { + abi::envoy_dynamic_module_type_network_connection_event::RemoteClose + | abi::envoy_dynamic_module_type_network_connection_event::LocalClose => { + envoy_log_debug!("Connection closed"); + } + _ => {} + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_config_creation() { + let config = HostnameLookupFilterConfig::new(b""); + assert!(config.is_some()); + } + + #[test] + fn test_ipv4_parsing_valid() { + let valid_ips = vec!["10.10.0.1", "192.168.1.1", "0.0.0.0", "255.255.255.255"]; + + for ip_str in valid_ips { + let result: Result = ip_str.parse(); + assert!(result.is_ok(), "Failed to parse valid IP: {}", ip_str); + } + } + + #[test] + fn test_ipv4_parsing_invalid() { + let invalid_ips = vec!["256.1.1.1", "10.10", "not.an.ip", "10.10.10.10.10", ""]; + + for ip_str in invalid_ips { + let result: Result = ip_str.parse(); + assert!(result.is_err(), "Should not parse invalid IP: {}", ip_str); + } + } + + #[test] + fn test_filter_state_key_formatting() { + let hostname_key = "envoy.wildcard.hostname"; + assert_eq!(hostname_key, "envoy.wildcard.hostname"); + + let metadata_keys = vec![ + ("cluster", "envoy.wildcard.metadata.cluster"), + ("region", "envoy.wildcard.metadata.region"), + ("auth_required", "envoy.wildcard.metadata.auth_required"), + ]; + + for (key, expected) in metadata_keys { + let filter_state_key = format!("envoy.wildcard.metadata.{}", key); + assert_eq!(filter_state_key, expected); + } + } + + #[test] + fn test_metadata_key_with_special_chars() { + let key = "my-key_with.special@chars"; + let filter_state_key = format!("envoy.wildcard.metadata.{}", key); + assert_eq!( + filter_state_key, + "envoy.wildcard.metadata.my-key_with.special@chars" + ); + } +} diff --git a/rust/src/egress_policies/mod.rs b/rust/src/egress_policies/mod.rs new file mode 100644 index 0000000..89aeacc --- /dev/null +++ b/rust/src/egress_policies/mod.rs @@ -0,0 +1,101 @@ +//! Egress policies module for wildcard DNS-based routing. +//! +//! This module provides a complete DNS-based routing system with virtual IP allocation: +//! +//! ## Architecture +//! +//! ```text +//! DNS Query (UDP:5353) → dns_gateway → allocate() → Virtual IP +//! ↓ +//! TCP Connection (port 17100) → hostname_lookup → lookup() → Policy metadata +//! ``` +//! +//! ## Components +//! +//! - [`virtual_ip_cache`] - Thread-safe cache for virtual IP allocation and policy lookup +//! - [`dns_gateway`] - UDP listener filter that handles DNS queries and allocates virtual IPs +//! - [`hostname_lookup`] - Network filter that looks up policies from virtual IPs and stores metadata +//! +//! ## Configuration Parameters +//! +//! - `base_ip`: Starting IP address for virtual IP allocation (required) +//! - `prefix_len`: Prefix length (1-32) that determines the allocation range (required) +//! - prefix_len = 24: Allocates 256 IPs (e.g., 10.10.0.0 - 10.10.0.255) +//! - prefix_len = 16: Allocates 65536 IPs (e.g., 10.10.0.0 - 10.10.255.255) +//! - `policies`: Array of domain patterns with associated metadata +//! +//! ## Example Configuration +//! Below is an example configuration that has Envoy receive DNS requests on port 5353, and allocate +//! virtual IPs to any domains falling under *.example.com. +//! A subsequent tcp listener intercepts all other traffic (assume there is an iptables rule forcing all traffic to this listener). +//! The tcp listener recovers the metadata associated with the policy *.example.com, and can then use it arbitrarily via +//! %FILTER_STATE(envoy.wildcard.metadata.:PLAIN)%. +//! This can be used for logging, or with set_filter_state to determine the upstream cluster for example. +//! +//! ```yaml +//! static_resources: +//! listeners: +//! - name: dns_listener +//! address: +//! socket_address: +//! address: 0.0.0.0 +//! port_value: 5353 +//! protocol: UDP +//! listener_filters: +//! - name: envoy.filters.udp_listener.dynamic_modules +//! typed_config: +//! "@type": type.googleapis.com/envoy.extensions.filters.udp.dynamic_modules.v3.DynamicModuleUdpListenerFilter +//! dynamic_module_config: +//! name: rust_module +//! do_not_close: true +//! filter_name: dns_gateway +//! filter_config: +//! "@type": type.googleapis.com/google.protobuf.StringValue +//! value: | +//! { +//! "base_ip": "10.10.0.0", +//! "prefix_len": 24, +//! "policies": [ +//! { +//! "domain": "*.example.com", +//! "metadata": { +//! "upstream_cluster": "cluster_a" +//! "arbitrary_key": "arbitrary_value" +//! } +//! } +//! ] +//! } +//! - name: tcp_listener +//! address: +//! socket_address: +//! address: 0.0.0.0 +//! port_value: 17100 +//! filter_chains: +//! - filters: +//! - name: envoy.filters.network.dynamic_modules +//! typed_config: +//! "@type": type.googleapis.com/envoy.extensions.filters.network.dynamic_modules.v3.DynamicModuleNetworkFilter +//! dynamic_module_config: +//! name: rust_module +//! do_not_close: true +//! filter_name: hostname_lookup +//! - name: envoy.filters.network.tcp_proxy +//! typed_config: +//! "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy +//! stat_prefix: egress_tcp +//! cluster: default_cluster +//! access_log: +//! - name: envoy.access_loggers.stdout +//! typed_config: +//! "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog +//! log_format: +//! text_format: "[LOG] arbitrary_key=%FILTER_STATE(envoy.wildcard.metadata.arbitrary_key:PLAIN)%\n" +//! ``` + +pub mod dns_gateway; +pub mod hostname_lookup; +pub mod virtual_ip_cache; + +pub use dns_gateway::DnsGatewayFilterConfig; +pub use hostname_lookup::HostnameLookupFilterConfig; +pub use virtual_ip_cache::{get_cache, init_cache, EgressPolicy, VirtualIpCache}; diff --git a/rust/src/egress_policies/virtual_ip_cache.rs b/rust/src/egress_policies/virtual_ip_cache.rs new file mode 100644 index 0000000..cd23801 --- /dev/null +++ b/rust/src/egress_policies/virtual_ip_cache.rs @@ -0,0 +1,268 @@ +//! Thread-safe virtual IP cache for wildcard DNS-based routing. + +use dashmap::DashMap; +use envoy_proxy_dynamic_modules_rust_sdk::*; +use once_cell::sync::OnceCell; +use parking_lot::Mutex; +use std::net::Ipv4Addr; +use std::sync::Arc; + +#[derive(Clone, Debug)] +pub struct EgressPolicy { + pub domain: String, // e.g., "*.aws.com" or "s3.aws.com" + pub metadata: std::collections::HashMap, +} + +pub struct VirtualIpCache { + base_ip: u32, + max_ips: u32, + alloc_offset: Mutex, + ip_to_policy: DashMap, + domain_to_ip: DashMap, +} + +impl VirtualIpCache { + /// Creates a new VirtualIpCache. + /// + /// # Arguments + /// * `base_ip` - The base IP address in network byte order + /// * `prefix_len` - The prefix length (1-32) that determines the allocation range + /// + /// # Examples + /// - prefix_len = 24: Allocates 256 IPs (e.g., 10.10.0.0/24) + /// - prefix_len = 16: Allocates 65536 IPs (e.g., 10.10.0.0/16) + pub fn new(base_ip: u32, prefix_len: u8) -> Self { + assert!( + prefix_len >= 1 && prefix_len <= 32, + "prefix_len must be between 1 and 32" + ); + let max_ips = 1u32 << (32 - prefix_len); + envoy_log_info!( + "Creating VirtualIpCache with prefix_len={}, max_ips={}", + prefix_len, + max_ips + ); + Self { + base_ip, + max_ips, + alloc_offset: Mutex::new(0), + ip_to_policy: DashMap::new(), + domain_to_ip: DashMap::new(), + } + } + + pub fn allocate(&self, policy: EgressPolicy) -> Ipv4Addr { + // Check if this policy has already been allocated a virtual IP + if let Some(ip) = self.domain_to_ip.get(&policy.domain) { + return *ip; + } + + // Acquire the offset lock (only 1 thread should give out a virtual IP at a time) + let ip = { + let mut offset = self.alloc_offset.lock(); + + // Double-check after acquiring lock + if let Some(ip) = self.domain_to_ip.get(&policy.domain) { + return *ip; + } + + // Check if we've exceeded the allocation limit + if *offset >= self.max_ips { + panic!( + "Virtual IP allocation exhausted: tried to allocate IP #{} but max is {}", + *offset, self.max_ips + ); + } + + let ip = Ipv4Addr::from(self.base_ip + *offset); + *offset += 1; + + ip + }; + + self.ip_to_policy.insert(ip, policy.clone()); + self.domain_to_ip.insert(policy.domain.clone(), ip); + + envoy_log_info!("Allocated virtual IP {} for domain {}", ip, policy.domain); + + ip + } + + pub fn lookup(&self, ip: Ipv4Addr) -> Option { + self.ip_to_policy.get(&ip).map(|entry| entry.clone()) + } +} + +static VIRTUAL_IP_CACHE: OnceCell> = OnceCell::new(); + +/// Initializes the global cache with the given base IP address and prefix length. +/// +/// This should be called by the DNS gateway filter during configuration. +/// Subsequent calls are silently ignored (first call wins). +/// +/// # Arguments +/// * `base_ip` - The base IP address in network byte order (e.g., 10.10.0.0 = 0x0A0A0000) +/// * `prefix_len` - The prefix length (1-32) that determines the allocation range +pub fn init_cache(base_ip: u32, prefix_len: u8) { + let cache = Arc::new(VirtualIpCache::new(base_ip, prefix_len)); + + if VIRTUAL_IP_CACHE.set(cache).is_err() { + envoy_log_warn!("VirtualIpCache already initialized, ignoring duplicate initialization"); + return; + } + + envoy_log_info!( + "Initialized VirtualIpCache with base IP {}, prefix_len {}", + Ipv4Addr::from(base_ip), + prefix_len + ); +} + +pub fn get_cache() -> Arc { + VIRTUAL_IP_CACHE + .get() + .expect("VirtualIpCache not initialized - DNS gateway filter should call init_cache()") + .clone() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cache_new() { + let cache = VirtualIpCache::new(0x0A0A0000, 24); // 10.10.0.0/24 + assert_eq!(cache.base_ip, 0x0A0A0000); + assert_eq!(cache.max_ips, 256); + } + + #[test] + fn test_prefix_len_calculations() { + let cache_24 = VirtualIpCache::new(0, 24); + assert_eq!(cache_24.max_ips, 256); // 2^(32-24) = 256 + + let cache_16 = VirtualIpCache::new(0, 16); + assert_eq!(cache_16.max_ips, 65536); // 2^(32-16) = 65536 + + let cache_32 = VirtualIpCache::new(0, 32); + assert_eq!(cache_32.max_ips, 1); // 2^(32-32) = 1 + + let cache_8 = VirtualIpCache::new(0, 8); + assert_eq!(cache_8.max_ips, 16777216); // 2^(32-8) = 16777216 + } + + #[test] + #[should_panic(expected = "prefix_len must be between 1 and 32")] + fn test_invalid_prefix_len_zero() { + VirtualIpCache::new(0, 0); + } + + #[test] + #[should_panic(expected = "prefix_len must be between 1 and 32")] + fn test_invalid_prefix_len_too_large() { + VirtualIpCache::new(0, 33); + } + + #[test] + fn test_allocate_sequential_ips() { + let cache = VirtualIpCache::new(0x0A0A0000, 24); // 10.10.0.0/24 + + let policy1 = EgressPolicy { + domain: "api.aws.com".to_string(), + metadata: std::collections::HashMap::new(), + }; + + let policy2 = EgressPolicy { + domain: "s3.aws.com".to_string(), + metadata: std::collections::HashMap::new(), + }; + + let ip1 = cache.allocate(policy1); + let ip2 = cache.allocate(policy2); + + assert_eq!(ip1, Ipv4Addr::new(10, 10, 0, 0)); + assert_eq!(ip2, Ipv4Addr::new(10, 10, 0, 1)); + } + + #[test] + fn test_allocate_same_domain_returns_same_ip() { + let cache = VirtualIpCache::new(0x0A0A0000, 24); + + let policy = EgressPolicy { + domain: "api.aws.com".to_string(), + metadata: std::collections::HashMap::new(), + }; + + let ip1 = cache.allocate(policy.clone()); + let ip2 = cache.allocate(policy.clone()); + + assert_eq!(ip1, ip2); + } + + #[test] + fn test_lookup_allocated_ip() { + let cache = VirtualIpCache::new(0x0A0A0000, 24); + + let mut metadata = std::collections::HashMap::new(); + metadata.insert("cluster".to_string(), "aws_cluster".to_string()); + + let policy = EgressPolicy { + domain: "api.aws.com".to_string(), + metadata: metadata.clone(), + }; + + let ip = cache.allocate(policy); + + let result = cache.lookup(ip).unwrap(); + assert_eq!(result.domain, "api.aws.com"); + assert_eq!(result.metadata.get("cluster").unwrap(), "aws_cluster"); + } + + #[test] + fn test_lookup_unallocated_ip() { + let cache = VirtualIpCache::new(0x0A0A0000, 24); + let unallocated_ip = Ipv4Addr::new(10, 10, 0, 100); + + assert!(cache.lookup(unallocated_ip).is_none()); + } + + #[test] + #[should_panic(expected = "Virtual IP allocation exhausted")] + fn test_allocation_exhaustion() { + let cache = VirtualIpCache::new(0x0A0A0000, 32); // Only 1 IP available + + let policy1 = EgressPolicy { + domain: "domain1.com".to_string(), + metadata: std::collections::HashMap::new(), + }; + + let policy2 = EgressPolicy { + domain: "domain2.com".to_string(), + metadata: std::collections::HashMap::new(), + }; + + cache.allocate(policy1); // Uses the only available IP + cache.allocate(policy2); // Should panic + } + + #[test] + fn test_metadata_preserved() { + let cache = VirtualIpCache::new(0x0A0A0000, 24); + + let mut metadata = std::collections::HashMap::new(); + metadata.insert("key1".to_string(), "value1".to_string()); + metadata.insert("key2".to_string(), "value2".to_string()); + + let policy = EgressPolicy { + domain: "test.com".to_string(), + metadata: metadata.clone(), + }; + + let ip = cache.allocate(policy); + let result = cache.lookup(ip).unwrap(); + + assert_eq!(result.metadata.len(), 2); + assert_eq!(result.metadata.get("key1").unwrap(), "value1"); + assert_eq!(result.metadata.get("key2").unwrap(), "value2"); + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index d87865d..f264260 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -60,7 +60,16 @@ pub mod listener_ip_allowlist; pub mod listener_sni_router; pub mod listener_tls_detector; -declare_init_functions!(init, new_http_filter_config_fn); +// Egress policies module. +// Implements a system for defining egress policies by hostname. +pub mod egress_policies; + +declare_all_init_functions!( + init, + http: new_http_filter_config_fn, + network: new_network_filter_config_fn, + udp_listener: new_udp_listener_filter_config_fn, +); /// This implements the [`envoy_proxy_dynamic_modules_rust_sdk::ProgramInitFunction`]. /// @@ -99,6 +108,51 @@ fn new_http_filter_config_fn( .map(|config| Box::new(config) as Box>), "metrics" => http_metrics::FilterConfig::new(filter_config, envoy_filter_config) .map(|config| Box::new(config) as Box>), - _ => panic!("Unknown filter name: {filter_name}"), + _ => panic!("Unknown HTTP filter name: {filter_name}"), + } +} + + + +/// This implements the [`envoy_proxy_dynamic_modules_rust_sdk::NewNetworkFilterConfigFunction`]. +/// +/// This is the entrypoint every time a new Network filter is created via the DynamicModuleNetworkFilter config. +/// +/// Each argument matches the corresponding argument in the Envoy config here: +/// https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/dynamic_modules/v3/dynamic_modules.proto#envoy-v3-api-msg-extensions-dynamic-modules-v3-dynamicmoduleconfig +/// +/// Returns None if the filter name or config is determined to be invalid by each filter's `new` function. +fn new_network_filter_config_fn( + _envoy_filter_config: &mut EC, + filter_name: &str, + filter_config: &[u8], +) -> Option>> { + match filter_name { + "hostname_lookup" => egress_policies::HostnameLookupFilterConfig::new(filter_config) + .map(|config| Box::new(config) as Box>), + _ => panic!("Unknown network filter name: {filter_name}"), + } +} + +/// This implements the [`envoy_proxy_dynamic_modules_rust_sdk::NewUdpListenerFilterConfigFunction`]. +/// +/// This is the entrypoint every time a new UDP Listener filter is created via the DynamicModuleUdpListenerFilter config. +/// +/// Each argument matches the corresponding argument in the Envoy config here: +/// https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/dynamic_modules/v3/dynamic_modules.proto#envoy-v3-api-msg-extensions-dynamic-modules-v3-dynamicmoduleconfig +/// +/// Returns None if the filter name or config is determined to be invalid by each filter's `new` function. +fn new_udp_listener_filter_config_fn< + EC: EnvoyUdpListenerFilterConfig, + ELF: EnvoyUdpListenerFilter, +>( + _envoy_filter_config: &mut EC, + filter_name: &str, + filter_config: &[u8], +) -> Option>> { + match filter_name { + "dns_gateway" => egress_policies::DnsGatewayFilterConfig::new(filter_config) + .map(|config| Box::new(config) as Box>), + _ => panic!("Unknown UDP listener filter name: {filter_name}"), } }