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/dns_gateway/README.md b/rust/src/dns_gateway/README.md new file mode 100644 index 0000000..8f89a65 --- /dev/null +++ b/rust/src/dns_gateway/README.md @@ -0,0 +1,329 @@ +# DNS Gateway + +Envoy dynamic module filters that intercept DNS queries and route TCP connections to external +domains via virtual IP allocation. + +![DNS Gateway diagram](diagram.png) + +## Prerequisites + +Requires iptables/nftables rules to redirect application traffic to Envoy: + +- **DNS**: UDP port 53 redirected to Envoy's DNS listener (e.g. port 15053) +- **TCP**: Outbound connections redirected to Envoy's TCP listener (e.g. port 15001) + +See [`connectivity-iptables`](../../../../../connectivity-iptables) for setup scripts. + +## How it works + +1. **`dns_gateway`** (UDP listener filter) — Intercepts DNS queries. If the queried domain matches + a configured pattern, allocates a virtual IP from a private subnet and responds with an A record. + Caches the mapping from virtual IP to domain and metadata. Non-matching queries pass through. + +2. **`cache_lookup`** (network filter) — On new TCP connections, looks up the destination virtual IP + in the shared cache and sets the resolved domain and metadata as Envoy + [filter state](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/data_sharing_between_filters#primitives) + for use in routing. + +``` + Application + | DNS query: "bucket-1.aws.com" + v + dns_gateway + | matches "*.aws.com", allocates 10.10.0.1, responds with A record + v + Application + | TCP connect to 10.10.0.1:443 + v + cache_lookup + | resolves 10.10.0.1 -> domain="bucket-1.aws.com", metadata.cluster="aws" + v + tcp_proxy + | routes to upstream cluster using filter state + v + External service (bucket-1.aws.com) +``` + +## Filter state + +`cache_lookup` sets the following keys, accessible via `%FILTER_STATE(...)%`: + +| Key | Example | +| ---------------------------------- | -------------------------------- | +| `envoy.dns_gateway.domain` | `bucket-1.aws.com` | +| `envoy.dns_gateway.metadata.` | value from matched domain config | + +Usage in Envoy config: + +- `%FILTER_STATE(envoy.dns_gateway.domain:PLAIN)%` +- `%FILTER_STATE(envoy.dns_gateway.metadata.cluster:PLAIN)%` +- `%FILTER_STATE(envoy.dns_gateway.metadata.auth_token:PLAIN)%` + +## Domain matching + +- **Exact**: `"example.com"` — matches only `example.com` +- **Wildcard**: `"*.aws.com"` — matches any subdomain (e.g. `bucket-1.aws.com`, + `sub.api.aws.com`) but not `aws.com` itself + +## Configuration reference + +### `dns_gateway` + +| Field | Type | Description | +| -------------------- | ------- | ---------------------------------------------------------------- | +| `base_ip` | string | Base IPv4 address for virtual IP allocation (e.g. `"10.10.0.0"`) | +| `prefix_len` | integer | CIDR prefix length (1-32). A `/24` gives 256 IPs. | +| `domains` | array | Domain matchers | +| `domains[].domain` | string | Exact (`"example.com"`) or wildcard (`"*.example.com"`) pattern | +| `domains[].metadata` | object | String key-value pairs exposed via filter state | + +### `cache_lookup` + +No configuration. Use `filter_config: {}`. + +## Manual testing + +End-to-end test with docker-compose. + +Create the following files: + +**docker-compose.yml**: + +```yaml +services: + envoy: + image: + network_mode: host + volumes: + - ./envoy.yaml:/etc/envoy/envoy.yaml + command: ["envoy", "-c", "/etc/envoy/envoy.yaml", "-l", "debug"] + +upstream-1: + image: python:3.12-slim + network_mode: host + volumes: + - ./upstream_1.py:/app/server.py + command: ["python3", "/app/server.py"] + + upstream-2: + image: python:3.12-slim + network_mode: host + volumes: + - ./upstream_2.py:/app/server.py + command: ["python3", "/app/server.py"] +``` + +**upstream_1.py** (port 18001): + +```python +from http.server import HTTPServer, BaseHTTPRequestHandler + +class Handler(BaseHTTPRequestHandler): + def do_CONNECT(self): + print(f"\nCONNECT {self.path}") + for key, value in self.headers.items(): + print(f" {key}: {value}") + + self.send_response(200) + self.end_headers() + + request = self.connection.recv(4096) + + body = f"cluster_1\nCONNECT: {self.path}\n" + for key, value in self.headers.items(): + body += f"{key}: {value}\n" + + resp = f"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nX-Upstream: cluster_1\r\nContent-Length: {len(body)}\r\n\r\n{body}" + self.connection.sendall(resp.encode()) + +HTTPServer(("0.0.0.0", 18001), Handler).serve_forever() +``` + +**upstream_2.py** (port 18002): + +```python +from http.server import HTTPServer, BaseHTTPRequestHandler + +class Handler(BaseHTTPRequestHandler): + def do_CONNECT(self): + print(f"\nCONNECT {self.path}") + for key, value in self.headers.items(): + print(f" {key}: {value}") + + self.send_response(200) + self.end_headers() + + request = self.connection.recv(4096) + + body = f"cluster_2\nCONNECT: {self.path}\n" + for key, value in self.headers.items(): + body += f"{key}: {value}\n" + + resp = f"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nX-Upstream: cluster_2\r\nContent-Length: {len(body)}\r\n\r\n{body}" + self.connection.sendall(resp.encode()) + +HTTPServer(("0.0.0.0", 18002), Handler).serve_forever() +``` + +**envoy.yaml**: + +```yaml +static_resources: + listeners: + - name: dns_listener + address: + socket_address: + address: 0.0.0.0 + port_value: 15053 + 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: connectivity_envoy_module + do_not_close: true + filter_name: dns_gateway + filter_config: + "@type": type.googleapis.com/google.protobuf.Struct + value: + base_ip: "10.10.0.0" + prefix_len: 24 + domains: + - domain: "*.aws.com" + metadata: + cluster: cluster_1 + auth_token: "abc123" + - domain: "example.com" + metadata: + cluster: cluster_2 + auth_token: "def456" + - name: envoy.filters.udp_listener.dns_filter + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.udp.dns_filter.v3.DnsFilterConfig + stat_prefix: dns_fallback + client_config: + max_pending_lookups: 256 + dns_resolution_config: + resolvers: + - socket_address: + protocol: TCP + address: 172.20.0.10 + port_value: 53 + dns_resolver_options: + no_default_search_domain: true + use_tcp_for_dns_lookups: true + server_config: + inline_dns_table: {} + + - name: tcp_listener + address: + socket_address: + address: 0.0.0.0 + port_value: 15001 + listener_filters: + - name: envoy.filters.listener.original_dst + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.listener.original_dst.v3.OriginalDst + 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: connectivity_envoy_module + do_not_close: true + filter_name: cache_lookup + filter_config: {} + # Setting an upstream cluster directly in the TCP proxy tunneling config with FILTER_STATE(...) + # is not supported. Instead, write the value of FILTER_STATE(...) to 'envoy.tcp_proxy.cluster' + - name: envoy.filters.network.set_filter_state + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.set_filter_state.v3.Config + on_new_connection: + - object_key: envoy.tcp_proxy.cluster + format_string: + text_format_source: + inline_string: "%FILTER_STATE(envoy.dns_gateway.metadata.cluster:PLAIN)%" + - name: envoy.filters.network.tcp_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + stat_prefix: egress + cluster: default + tunneling_config: + hostname: "%FILTER_STATE(envoy.dns_gateway.domain:PLAIN)%" + headers_to_add: + - header: + key: "X-Auth-Token" + value: "%FILTER_STATE(envoy.dns_gateway.metadata.auth_token:PLAIN)%" + + clusters: + - name: cluster_1 + type: STATIC + load_assignment: + cluster_name: cluster_1 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 18001 + + - name: cluster_2 + type: STATIC + load_assignment: + cluster_name: cluster_2 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 18002 +``` + +### 2. Start + +```bash +docker-compose up +``` + +### 3. Set up iptables redirect + +```bash +# Redirect DNS (UDP 53) to Envoy's DNS listener +sudo iptables -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to-destination 127.0.0.1:15053 + +# Redirect TCP to virtual IPs (10.10.0.0/24) to Envoy's TCP listener +sudo iptables -t nat -A OUTPUT -p tcp -d 10.10.0.0/24 -j DNAT --to-destination 127.0.0.1:15001 +``` + +### 4. Test + +```bash +# Will allocate sequentially increasing virtual IPs +dig one.s3.aws.com +dig two.s3.aws.com +dig example.com + +# Unmatched domain, will defer to external DNS +dig github.com + +# Will reach cluster_1 +curl http://s3.aws.com./ + +# Will reach cluster_2 +curl http://example.com./ + +# See logs for upstream-1 and upstream-2 +docker-compose logs upstream-1 +docker-compose logs upstream-2 +``` + +### 5. Clean up iptables + +```bash +sudo iptables -t nat -D OUTPUT -p udp --dport 53 -j DNAT --to-destination 127.0.0.1:15053 +sudo iptables -t nat -D OUTPUT -p tcp -d 10.10.0.0/24 -j DNAT --to-destination 127.0.0.1:15001 +``` diff --git a/rust/src/dns_gateway/cache_lookup.rs b/rust/src/dns_gateway/cache_lookup.rs new file mode 100644 index 0000000..f69bc31 --- /dev/null +++ b/rust/src/dns_gateway/cache_lookup.rs @@ -0,0 +1,81 @@ +//! A cache lookup filter that should be used in conjunction with the DNS gateway filter. +//! +//! The filter looks up the destination virtual IP in the shared cache and sets filter state +//! with the matched domain and metadata for downstream filters. + +use envoy_proxy_dynamic_modules_rust_sdk::*; +use std::net::Ipv4Addr; + +use super::virtual_ip_cache::get_cache; + +/// The filter configuration that implements +/// [`envoy_proxy_dynamic_modules_rust_sdk::NetworkFilterConfig`]. +pub struct CacheLookupFilterConfig; + +impl CacheLookupFilterConfig { + /// Creates a new cache lookup filter configuration. + pub fn new(_config: &[u8]) -> Self { + envoy_log_info!("Filter initialized"); + CacheLookupFilterConfig + } +} + +impl NetworkFilterConfig for CacheLookupFilterConfig { + fn new_network_filter(&self, _envoy: &mut ENF) -> Box> { + Box::new(CacheLookupFilter) + } +} + +/// The cache lookup filter that implements +/// [`envoy_proxy_dynamic_modules_rust_sdk::NetworkFilter`]. +/// +/// Looks up the destination virtual IP in the shared cache and sets filter state +/// with the matched domain and metadata. +struct CacheLookupFilter; + +impl NetworkFilter for CacheLookupFilter { + 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_debug!("New connection, local_address={}:{}", ip_str, port); + + let ip: Ipv4Addr = match ip_str.parse() { + Ok(ip) => ip, + Err(_) => { + envoy_log_warn!("Failed to parse destination IP: {}", ip_str); + return abi::envoy_dynamic_module_type_on_network_filter_data_status::Continue; + } + }; + + let destination = match get_cache().lookup(ip) { + Some(d) => d, + None => { + envoy_log_warn!("No destination found for virtual IP: {} (cache miss)", ip); + return abi::envoy_dynamic_module_type_on_network_filter_data_status::Continue; + } + }; + + envoy_filter + .set_filter_state_bytes(b"envoy.dns_gateway.domain", destination.domain().as_bytes()); + for (key, value) in destination.metadata() { + envoy_filter.set_filter_state_bytes( + format!("envoy.dns_gateway.metadata.{}", key).as_bytes(), + value.as_bytes(), + ); + } + + abi::envoy_dynamic_module_type_on_network_filter_data_status::Continue + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_config_creation() { + let _config = CacheLookupFilterConfig::new(b""); + } +} diff --git a/rust/src/dns_gateway/config.rs b/rust/src/dns_gateway/config.rs new file mode 100644 index 0000000..77f7dd1 --- /dev/null +++ b/rust/src/dns_gateway/config.rs @@ -0,0 +1,24 @@ +use std::collections::HashMap; + +#[derive(Clone, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct DnsGatewayConfig { + /// Base IPv4 address for virtual IP allocation (e.g. "10.10.0.0"). + #[serde(default)] + pub base_ip: String, + /// CIDR prefix length (1-32). A /24 gives 256 IPs. + #[serde(default)] + pub prefix_len: u32, + /// Each entry defines a domain pattern and associated metadata. + #[serde(default)] + pub domains: Vec, +} + +#[derive(Clone, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct DomainMatcher { + /// Exact domain ("example.com") or wildcard pattern ("*.example.com"). + #[serde(default)] + pub domain: String, + /// String key-value pairs exposed in Envoy filter state as `envoy.dns_gateway.metadata..` + #[serde(default)] + pub metadata: HashMap, +} diff --git a/rust/src/dns_gateway/diagram.png b/rust/src/dns_gateway/diagram.png new file mode 100644 index 0000000..b2aa790 Binary files /dev/null and b/rust/src/dns_gateway/diagram.png differ diff --git a/rust/src/dns_gateway/mod.rs b/rust/src/dns_gateway/mod.rs new file mode 100644 index 0000000..85f42ca --- /dev/null +++ b/rust/src/dns_gateway/mod.rs @@ -0,0 +1,503 @@ +//! A DNS gateway filter that intercepts DNS queries and returns virtual IPs for matched domains. +//! +//! This filter demonstrates: +//! 1. UDP listener filter structure with `UdpListenerFilterConfig` and `UdpListenerFilter` traits. +//! 2. DNS query parsing and response. + +pub mod cache_lookup; +mod config; +mod virtual_ip_cache; + +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::collections::HashMap; +use std::net::Ipv4Addr; +use std::sync::Arc; +use virtual_ip_cache::{get_cache, init_cache, Destination}; + +#[derive(Clone)] +struct DomainMatcher { + domain_pattern: String, + metadata: HashMap, +} + +impl DomainMatcher { + /// Matches a domain against this matcher's pattern. + /// Supports exact matches and wildcard patterns like "*.aws.com". + fn matches(&self, domain: &str) -> bool { + let Some(base_domain) = self.domain_pattern.strip_prefix("*.") else { + return domain == self.domain_pattern; + }; + + // For "*.aws.com", base_domain is "aws.com". + // Domain must end with ".aws.com" and have at least one label before it. + let Some(prefix) = domain.strip_suffix(base_domain) else { + return false; + }; + prefix.ends_with('.') && prefix.len() > 1 + } +} + +/// The filter configuration that implements +/// [`envoy_proxy_dynamic_modules_rust_sdk::UdpListenerFilterConfig`]. +/// +/// This configuration is shared across all UDP listener filter instances. +pub struct DnsGatewayFilterConfig { + domains: Arc<[DomainMatcher]>, +} + +impl DnsGatewayFilterConfig { + /// Creates a new DNS gateway filter configuration from the raw config bytes. + /// + /// The config arrives as a JSON-serialized google.protobuf.Struct + /// wrapped in an Any: `{"@type":"...Struct", "value":{"base_ip":"...", ...}}`. + pub fn new(config: &[u8]) -> Option { + let config_str = match std::str::from_utf8(config) { + Ok(s) => s, + Err(err) => { + eprintln!("Error parsing config as UTF-8: {err}"); + return None; + } + }; + + let config_json: serde_json::Value = match serde_json::from_str(config_str) { + Ok(v) => v, + Err(err) => { + eprintln!("Error parsing config JSON: {err}"); + return None; + } + }; + let value = &config_json["value"]; + + let gateway_config: config::DnsGatewayConfig = match serde_json::from_value(value.clone()) { + Ok(cfg) => cfg, + Err(err) => { + eprintln!("Error parsing DnsGatewayConfig: {err}"); + return None; + } + }; + + if gateway_config.base_ip.is_empty() { + eprintln!("base_ip is required for DNS gateway"); + return None; + } + let base_ip: Ipv4Addr = match gateway_config.base_ip.parse() { + Ok(ip) => ip, + Err(err) => { + eprintln!("Invalid base_ip: {err}"); + return None; + } + }; + + let prefix_len = match u8::try_from(gateway_config.prefix_len) { + Ok(v) => v, + Err(err) => { + eprintln!("Invalid prefix_len: {err}"); + return None; + } + }; + if !(1..=32).contains(&prefix_len) { + eprintln!("prefix_len must be between 1 and 32, got {prefix_len}"); + return None; + } + + init_cache(u32::from(base_ip), prefix_len); + + let domains: Arc<[DomainMatcher]> = gateway_config + .domains + .into_iter() + .map(|d| DomainMatcher { + domain_pattern: d.domain, + metadata: d.metadata.into_iter().collect(), + }) + .collect::>() + .into(); + + envoy_log_info!("Initialized with {} domains", domains.len()); + + Some(DnsGatewayFilterConfig { domains }) + } +} + +impl UdpListenerFilterConfig for DnsGatewayFilterConfig { + fn new_udp_listener_filter(&self, _envoy: &mut ELF) -> Box> { + Box::new(DnsGatewayFilter { + domains: Arc::clone(&self.domains), + }) + } +} + +/// The DNS gateway filter that implements +/// [`envoy_proxy_dynamic_modules_rust_sdk::UdpListenerFilter`]. +/// +/// Intercepts DNS queries and returns virtual IPs for domains matching configured matchers. +struct DnsGatewayFilter { + domains: Arc<[DomainMatcher]>, +} + +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_debug!( + "Received UDP datagram, {} bytes, {} chunks", + total_length, + chunks.len() + ); + let data: Vec = chunks.iter().flat_map(|c| c.as_slice()).copied().collect(); + + // From the perspective of DNS gateway, the peer is the client that sent the DNS query. + let peer = envoy_filter.get_peer_address(); + envoy_log_debug!("Peer address: {:?}", peer); + + let mut decoder = BinDecoder::new(&data); + let query_message = match Message::read(&mut decoder) { + Ok(msg) => msg, + Err(e) => { + envoy_log_warn!("Failed to parse DNS query: {}", e); + return abi::envoy_dynamic_module_type_on_udp_listener_filter_status::Continue; + } + }; + + envoy_log_debug!( + "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!("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 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); + + envoy_log_debug!( + "{:?} record query for domain: {} (raw: {})", + question.query_type(), + domain, + domain_raw + ); + + let matcher = match self.domains.iter().find(|m| m.matches(domain)) { + Some(m) => m, + None => { + envoy_log_info!("No matcher for domain: {}", domain); + return abi::envoy_dynamic_module_type_on_udp_listener_filter_status::Continue; + } + }; + + envoy_log_info!( + "Matched pattern '{}' for domain '{}'", + matcher.domain_pattern, + domain + ); + + let response_result = match question.query_type() { + RecordType::A => { + let destination = Destination::new(domain.to_string(), matcher.metadata.clone()); + let virtual_ip = match get_cache().allocate(destination) { + Some(ip) => ip, + None => { + envoy_log_error!("IP exhaustion, cannot allocate for {}", domain); + return abi::envoy_dynamic_module_type_on_udp_listener_filter_status::Continue; + } + }; + envoy_log_info!("Allocated virtual IP {} for domain {}", virtual_ip, domain); + build_dns_response(&query_message, question.name(), virtual_ip) + } + other => { + envoy_log_info!( + "Returning NODATA for {:?} query (only A records supported)", + other + ); + build_nodata_response(&query_message) + } + }; + + let response_bytes = match response_result { + Ok(bytes) => bytes, + Err(e) => { + envoy_log_error!("Failed to craft DNS response: {}", e); + return abi::envoy_dynamic_module_type_on_udp_listener_filter_status::Continue; + } + }; + + let (peer_addr, peer_port) = match peer { + Some(p) => p, + None => { + envoy_log_error!("No peer address available, cannot send response"); + return abi::envoy_dynamic_module_type_on_udp_listener_filter_status::StopIteration; + } + }; + + envoy_log_debug!( + "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!("Failed to send datagram to {}:{}", peer_addr, peer_port); + } + + abi::envoy_dynamic_module_type_on_udp_listener_filter_status::StopIteration + } +} + +fn build_dns_response( + query_message: &Message, + name: &Name, + ip: Ipv4Addr, +) -> Result, Box> { + let mut response = query_message.clone(); + + response.set_message_type(MessageType::Response); + response.set_response_code(ResponseCode::NoError); + response.set_recursion_available(true); + response.set_authoritative(true); + + let record = Record::from_rdata(name.clone(), 600, RData::A(ip.into())); + + response.add_answer(record); + + let bytes = response.to_vec()?; + Ok(bytes) +} + +fn build_nodata_response(query_message: &Message) -> Result, Box> { + let mut response = query_message.clone(); + + response.set_message_type(MessageType::Response); + response.set_response_code(ResponseCode::NoError); + response.set_recursion_available(true); + response.set_authoritative(true); + + let bytes = response.to_vec()?; + Ok(bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_domain_matcher_wildcard() { + let matcher = DomainMatcher { + domain_pattern: "*.aws.com".to_string(), + metadata: HashMap::new(), + }; + + assert!(matcher.matches("api.aws.com")); + assert!(matcher.matches("s3.aws.com")); + assert!(matcher.matches("lambda.aws.com")); + assert!(matcher.matches("sub.api.aws.com")); + + assert!(!matcher.matches("aws.com")); + assert!(!matcher.matches("xaws.com")); + assert!(!matcher.matches("aws.com.evil.com")); + assert!(!matcher.matches("api.aws.org")); + } + + #[test] + fn test_domain_matcher_exact() { + let matcher = DomainMatcher { + domain_pattern: "api.example.com".to_string(), + metadata: HashMap::new(), + }; + + assert!(matcher.matches("api.example.com")); + + assert!(!matcher.matches("www.api.example.com")); + assert!(!matcher.matches("example.com")); + assert!(!matcher.matches("api.example.org")); + } + + #[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, + "domains": [ + { + "domain": "*.aws.com", + "metadata": { + "cluster": "aws_cluster", + "region": "us-east-1" + } + } + ] + } + }"#; + + let config = DnsGatewayFilterConfig::new(config.as_bytes()).unwrap(); + assert_eq!(config.domains.len(), 1); + assert_eq!(config.domains[0].domain_pattern, "*.aws.com"); + assert_eq!( + config.domains[0].metadata.get("cluster").unwrap(), + "aws_cluster" + ); + assert_eq!( + config.domains[0].metadata.get("region").unwrap(), + "us-east-1" + ); + } + + #[test] + fn test_config_parsing_multiple_domains() { + let config = r#"{ + "@type": "type.googleapis.com/google.protobuf.Struct", + "value": { + "base_ip": "10.10.0.0", + "prefix_len": 16, + "domains": [ + {"domain": "*.aws.com", "metadata": {"cluster": "aws"}}, + {"domain": "*.google.com", "metadata": {"cluster": "google"}}, + {"domain": "exact.example.com", "metadata": {"cluster": "exact"}} + ] + } + }"#; + + let config = DnsGatewayFilterConfig::new(config.as_bytes()).unwrap(); + assert_eq!(config.domains.len(), 3); + } + + #[test] + fn test_config_parsing_missing_base_ip() { + let config = r#"{ + "value": { + "prefix_len": 24, + "domains": [] + } + }"#; + + assert!(DnsGatewayFilterConfig::new(config.as_bytes()).is_none()); + } + + #[test] + fn test_config_parsing_missing_prefix_len() { + // serde defaults missing uint32 to 0, which fails the 1..=32 range check. + let config = r#"{ + "value": { + "base_ip": "10.10.0.0", + "domains": [] + } + }"#; + + assert!(DnsGatewayFilterConfig::new(config.as_bytes()).is_none()); + } + + #[test] + fn test_config_parsing_invalid_prefix_len() { + let config = r#"{ + "value": { + "base_ip": "10.10.0.0", + "prefix_len": 33, + "domains": [] + } + }"#; + + assert!(DnsGatewayFilterConfig::new(config.as_bytes()).is_none()); + } + + #[test] + fn test_config_parsing_invalid_json() { + assert!(DnsGatewayFilterConfig::new(b"invalid json {").is_none()); + } + + #[test] + fn test_config_parsing_non_string_metadata_value() { + let config = r#"{ + "value": { + "base_ip": "10.10.0.0", + "prefix_len": 24, + "domains": [ + {"domain": "*.aws.com", "metadata": {"count": 42}} + ] + } + }"#; + + assert!(DnsGatewayFilterConfig::new(config.as_bytes()).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() { + 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); + + let result = build_dns_response(&query, &name, ip); + assert!(result.is_ok()); + + let response_bytes = result.unwrap(); + assert!(!response_bytes.is_empty()); + + 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() { + let mut query = Message::new(); + query.set_id(54321); + query.set_message_type(MessageType::Query); + query.set_recursion_desired(false); + + let result = build_nodata_response(&query); + assert!(result.is_ok()); + + let response_bytes = result.unwrap(); + assert!(!response_bytes.is_empty()); + + 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); + } +} diff --git a/rust/src/dns_gateway/virtual_ip_cache.rs b/rust/src/dns_gateway/virtual_ip_cache.rs new file mode 100644 index 0000000..ebc4cdd --- /dev/null +++ b/rust/src/dns_gateway/virtual_ip_cache.rs @@ -0,0 +1,300 @@ +//! Virtual IP cache for mapping domains to synthetic IPv4 addresses. +//! +//! This module provides a thread-safe cache that allocates sequential virtual IPs from a +//! configured subnet. The DNS gateway filter populates this cache, and the cache lookup +//! network filter reads from it. + +use dashmap::mapref::entry::Entry; +use dashmap::DashMap; +use envoy_proxy_dynamic_modules_rust_sdk::*; +use parking_lot::Mutex; +use std::collections::HashMap; +use std::net::Ipv4Addr; +use std::sync::{Arc, OnceLock}; + +/// A destination entry in the virtual IP cache. +/// +/// Maps a fully qualified domain name (e.g. "bucket-1.aws.com") to its +/// associated metadata (e.g. the upstream cluster to use). +#[derive(Clone, Debug)] +pub struct Destination { + domain: String, + metadata: HashMap, +} + +impl Destination { + pub fn new(domain: String, metadata: HashMap) -> Self { + Self { domain, metadata } + } + + pub fn domain(&self) -> &str { + &self.domain + } + + pub fn metadata(&self) -> &HashMap { + &self.metadata + } +} + +/// Thread-safe cache for virtual IP allocation and lookup. +/// +/// Allocates sequential IPs from a configured base address within a CIDR subnet. +/// Deduplicates allocations by domain name. +pub struct VirtualIpCache { + base_ip: u32, + capacity: u32, + alloc_offset: Mutex, + ip_to_destination: DashMap, + domain_to_ip: DashMap, +} + +impl VirtualIpCache { + pub fn new(base_ip: u32, prefix_len: u8) -> Self { + assert!( + (1..=32).contains(&prefix_len), + "prefix_len must be between 1 and 32" + ); + let subnet_size = 1u32 << (32 - prefix_len); + let host_mask = subnet_size - 1; + let capacity = subnet_size - (base_ip & host_mask); + envoy_log_info!( + "Creating cache with prefix_len={}, capacity={}", + prefix_len, + capacity + ); + Self { + base_ip, + capacity, + alloc_offset: Mutex::new(0), + ip_to_destination: DashMap::new(), + domain_to_ip: DashMap::new(), + } + } + + /// Allocates a virtual IP for the given destination. + /// + /// Returns the same IP if the domain was previously allocated. + /// Returns `None` if the subnet is exhausted. + pub fn allocate(&self, destination: Destination) -> Option { + if let Some(ip) = self.domain_to_ip.get(&destination.domain) { + return Some(*ip); + } + + match self.domain_to_ip.entry(destination.domain.clone()) { + Entry::Occupied(entry) => Some(*entry.get()), + Entry::Vacant(entry) => { + let mut offset = self.alloc_offset.lock(); + + if *offset >= self.capacity { + envoy_log_error!( + "IP allocation exhausted, tried to allocate #{} but max is {}", + *offset, + self.capacity + ); + return None; + } + + let ip = Ipv4Addr::from(self.base_ip + *offset); + *offset += 1; + + envoy_log_info!( + "Allocated virtual IP {} for domain {}", + ip, + destination.domain + ); + + self.ip_to_destination.insert(ip, destination); + entry.insert(ip); + + Some(ip) + } + } + } + + /// Looks up the destination for a given virtual IP. + pub fn lookup(&self, ip: Ipv4Addr) -> Option { + self.ip_to_destination.get(&ip).as_deref().cloned() + } +} + +static VIRTUAL_IP_CACHE: OnceLock> = OnceLock::new(); + +/// Initializes the global virtual IP cache. First call wins; subsequent calls are ignored. +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!("Cache already initialized, ignoring duplicate init"); + return; + } + + envoy_log_info!( + "Initialized with base IP {}, prefix_len {}", + Ipv4Addr::from(base_ip), + prefix_len + ); +} + +pub fn get_cache() -> &'static Arc { + VIRTUAL_IP_CACHE + .get() + .expect("cache not initialized, dns_gateway must be configured first") +} + +#[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.capacity, 256); + } + + #[test] + fn test_prefix_len_calculations() { + let cache_24 = VirtualIpCache::new(0, 24); + assert_eq!(cache_24.capacity, 256); // 2^(32-24) = 256 + + let cache_16 = VirtualIpCache::new(0, 16); + assert_eq!(cache_16.capacity, 65536); // 2^(32-16) = 65536 + + let cache_32 = VirtualIpCache::new(0, 32); + assert_eq!(cache_32.capacity, 1); // 2^(32-32) = 1 + + let cache_8 = VirtualIpCache::new(0, 8); + assert_eq!(cache_8.capacity, 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_unaligned_base_ip_caps_capacity() { + // 10.10.0.200/24 — only 56 IPs remain in the subnet (200..=255) + let cache = VirtualIpCache::new(u32::from(Ipv4Addr::new(10, 10, 0, 200)), 24); + assert_eq!(cache.capacity, 56); + } + + #[test] + fn test_unaligned_base_ip_exhaustion() { + // 10.10.0.252/24 — only 4 IPs remain (252, 253, 254, 255) + let base = u32::from(Ipv4Addr::new(10, 10, 0, 252)); + let cache = VirtualIpCache::new(base, 24); + assert_eq!(cache.capacity, 4); + + for i in 0..4 { + let dest = Destination::new(format!("domain{}.com", i), HashMap::new()); + assert!(cache.allocate(dest).is_some()); + } + + let ip_last = cache.allocate(Destination::new("last-ok.com".to_string(), HashMap::new())); + assert!(ip_last.is_none()); + + let first = cache.lookup(Ipv4Addr::new(10, 10, 0, 252)).unwrap(); + assert_eq!(first.domain(), "domain0.com"); + let last = cache.lookup(Ipv4Addr::new(10, 10, 0, 255)).unwrap(); + assert_eq!(last.domain(), "domain3.com"); + } + + #[test] + fn test_aligned_base_ip_full_subnet() { + let cache = VirtualIpCache::new(u32::from(Ipv4Addr::new(10, 10, 0, 0)), 24); + assert_eq!(cache.capacity, 256); + } + + #[test] + fn test_allocate_sequential_ips() { + let cache = VirtualIpCache::new(0x0A0A0000, 24); // 10.10.0.0/24 + + let dest1 = Destination::new("api.aws.com".to_string(), HashMap::new()); + let dest2 = Destination::new("s3.aws.com".to_string(), HashMap::new()); + + let ip1 = cache.allocate(dest1).unwrap(); + let ip2 = cache.allocate(dest2).unwrap(); + + 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 dest = Destination::new("api.aws.com".to_string(), HashMap::new()); + + let ip1 = cache.allocate(dest.clone()).unwrap(); + let ip2 = cache.allocate(dest.clone()).unwrap(); + + assert_eq!(ip1, ip2); + } + + #[test] + fn test_lookup_allocated_ip() { + let cache = VirtualIpCache::new(0x0A0A0000, 24); + + let mut metadata = HashMap::new(); + metadata.insert("cluster".to_string(), "aws_cluster".to_string()); + + let dest = Destination::new("api.aws.com".to_string(), metadata); + + let ip = cache.allocate(dest).unwrap(); + + 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] + fn test_allocation_exhaustion_returns_none() { + let cache = VirtualIpCache::new(0x0A0A0000, 30); // 4 IPs available (2^(32-30)) + + for i in 0..4 { + let dest = Destination::new(format!("domain{}.com", i), HashMap::new()); + assert!( + cache.allocate(dest).is_some(), + "allocation {} should succeed", + i + ); + } + + let overflow = Destination::new("overflow.com".to_string(), HashMap::new()); + assert!(cache.allocate(overflow).is_none()); + } + + #[test] + fn test_metadata_preserved() { + let cache = VirtualIpCache::new(0x0A0A0000, 24); + + let mut metadata = HashMap::new(); + metadata.insert("key1".to_string(), "value1".to_string()); + metadata.insert("key2".to_string(), "value2".to_string()); + + let dest = Destination::new("test.com".to_string(), metadata); + + let ip = cache.allocate(dest).unwrap(); + 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..679f179 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -60,7 +60,15 @@ pub mod listener_ip_allowlist; pub mod listener_sni_router; pub mod listener_tls_detector; -declare_init_functions!(init, new_http_filter_config_fn); +// DNS gateway module example. +pub mod dns_gateway; + +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 +107,52 @@ 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 { + "cache_lookup" => Some( + Box::new(dns_gateway::cache_lookup::CacheLookupFilterConfig::new( + filter_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" => dns_gateway::DnsGatewayFilterConfig::new(filter_config) + .map(|config| Box::new(config) as Box>), + _ => panic!("Unknown UDP listener filter name: {filter_name}"), } }