commit 0d5fca33805ccddf5cd71a90329ca7866138b74a Author: ParrotXray Date: Sat Mar 28 03:45:46 2026 +0000 feat: using research for academic diff --git a/.github/images/accessControl.png b/.github/images/accessControl.png new file mode 100644 index 0000000..e815dd8 Binary files /dev/null and b/.github/images/accessControl.png differ diff --git a/.github/images/aiDetection.png b/.github/images/aiDetection.png new file mode 100644 index 0000000..81eb510 Binary files /dev/null and b/.github/images/aiDetection.png differ diff --git a/.github/images/dashboard.png b/.github/images/dashboard.png new file mode 100644 index 0000000..30569c7 Binary files /dev/null and b/.github/images/dashboard.png differ diff --git a/.github/images/index.png b/.github/images/index.png new file mode 100644 index 0000000..316c4bd Binary files /dev/null and b/.github/images/index.png differ diff --git a/.github/images/map.png b/.github/images/map.png new file mode 100644 index 0000000..6070b8d Binary files /dev/null and b/.github/images/map.png differ diff --git a/.github/images/statistics.png b/.github/images/statistics.png new file mode 100644 index 0000000..0ac5f6c Binary files /dev/null and b/.github/images/statistics.png differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..104c1fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +### https://raw.github.com/github/gitignore/master/Rust.gitignore + +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +.idea +logs +TODO +.log +.txt +net-guardia/static/web diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4fd0839 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "net-guardia-frontend"] + path = net-guardia-frontend + url = https://github.com/ParrotXray/NetGuardia-frontend-academic-research.git diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b2928a5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3592 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "actix" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" +dependencies = [ + "actix-macros", + "actix-rt", + "actix_derive", + "bitflags", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", + "log", + "once_cell", + "parking_lot", + "pin-project-lite", + "smallvec", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-cors" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d" +dependencies = [ + "actix-utils", + "actix-web", + "derive_more", + "futures-util", + "log", + "once_cell", + "smallvec", +] + +[[package]] +name = "actix-http" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f860ee6746d0c5b682147b2f7f8ef036d4f92fe518251a3a35ffa3650eafdf0e" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "base64", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "foldhash 0.1.4", + "futures-core", + "h2", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.9.2", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.98", +] + +[[package]] +name = "actix-router" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" +dependencies = [ + "bytestring", + "cfg-if", + "http 0.2.12", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2 0.5.8", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff87453bc3b56e9b2b23c1cc0b1be8797184accf51d2abe0f8a33ec275d316bf" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "foldhash 0.1.4", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.6.0", + "time", + "tracing", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "actix-ws" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decf53c3cdd63dd6f289980b430238f9a2f6d19f8bce8e418272e08d3da43f0f" +dependencies = [ + "actix-codec", + "actix-http", + "actix-web", + "bytestring", + "futures-core", + "futures-sink", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix_derive" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy 0.8.33", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "anymap2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" + +[[package]] +name = "anymap3" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170433209e817da6aae2c51aa0dd443009a613425dd041ebfb2492d1c4c11a25" + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "aya" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d18bc4e506fbb85ab7392ed993a7db4d1a452c71b75a246af4a80ab8c9d2dd50" +dependencies = [ + "assert_matches", + "aya-obj", + "bitflags", + "bytes", + "libc", + "log", + "object", + "once_cell", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "aya-ebpf" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8dbaf5409a1a0982e5c9bdc0f499a55fe5ead39fe9c846012053faf0d404f73" +dependencies = [ + "aya-ebpf-bindings", + "aya-ebpf-cty", + "aya-ebpf-macros", + "rustversion", +] + +[[package]] +name = "aya-ebpf-bindings" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "783dc1a82a3d71d83286165381dcc1b1d41643f4b110733d135547527c000a9a" +dependencies = [ + "aya-ebpf-cty", +] + +[[package]] +name = "aya-ebpf-cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cce099aaf3abb89f9a1f8594ffe07fa53738ebc2882fac624d10d9ba31a1b10" + +[[package]] +name = "aya-ebpf-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f47f7b4a75eb5f1d7ba0fb5628d247b1cf20388658899177875dabdda66865" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "aya-log" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b600d806c1d07d3b81ab5f4a2a95fd80f479a0d3f1d68f29064d660865f85f02" +dependencies = [ + "aya", + "aya-log-common", + "bytes", + "log", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "aya-log-common" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "befef9fe882e63164a2ba0161874e954648a72b0e1c4b361f532d590638c4eec" +dependencies = [ + "num_enum", +] + +[[package]] +name = "aya-log-ebpf" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae348f459df78a79e5cd5e164b6562b927033b97ca3b033605b341a474f44510" +dependencies = [ + "aya-ebpf", + "aya-log-common", + "aya-log-ebpf-macros", +] + +[[package]] +name = "aya-log-ebpf-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d8251a75f56077db51892041aa6b77c70ef2723845d7a210979700b2f01bc4" +dependencies = [ + "aya-log-common", + "aya-log-parser", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "aya-log-parser" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b102eb5c88c9aa0b49102d3fbcee08ecb0dfa81014f39b373311de7a7032cb" +dependencies = [ + "aya-log-common", +] + +[[package]] +name = "aya-obj" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51b96c5a8ed8705b40d655273bc4212cbbf38d4e3be2788f36306f154523ec7" +dependencies = [ + "bytes", + "core-error", + "hashbrown 0.15.2", + "log", + "object", + "thiserror 1.0.69", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.98", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" + +[[package]] +name = "bytestring" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" +dependencies = [ + "bytes", +] + +[[package]] +name = "camino" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0b03af37dad7a14518b7691d81acb0f8222604ad3d1b02f6b4bed5188c0cd5" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8abf5d501fd757c2d2ee78d0cc40f606e92e3a63544420316565556ed28485e2" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.16", +] + +[[package]] +name = "cc" +version = "1.2.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "common" +version = "0.1.0" +dependencies = [ + "aya", + "aya-ebpf", + "network-types", + "serde", +] + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-error" +version = "0.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efcdb2972eb64230b4c50646d8498ff73f5128d196a90c7236eec4cbe8619b8f" +dependencies = [ + "version_check", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive-new" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "dyn-hash" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15401da73a9ed8c80e3b2d4dc05fe10e7b72d7243b9f614e516a44fa99986e88" + +[[package]] +name = "egress-ebpf" +version = "0.1.0" +dependencies = [ + "aya-ebpf", + "aya-log-ebpf", + "common", + "which", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[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 = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[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-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[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.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.3+wasi-0.2.4", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy 0.8.33", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.4", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "impl-more" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "ingress-ebpf" +version = "0.1.0" +dependencies = [ + "aya-ebpf", + "aya-log-ebpf", + "common", + "network-types", + "which", +] + +[[package]] +name = "ipnetwork" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "serde", + "static_assertions", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libbpf-sys" +version = "1.5.1+v1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "912fae30b08bcbdb861d4b85bd09c05352c0ac9d7b93765ced5ca23709e7e590" +dependencies = [ + "cc", + "nix", + "pkg-config", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", + "redox_syscall 0.7.0", +] + +[[package]] +name = "libxdp-sys" +version = "0.2.3+1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e5550dcb7e33b49e821ef0a1a274b4094010dae2f1fe7b48fae4c16ef15580" +dependencies = [ + "bindgen", + "cc", + "libbpf-sys", +] + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "liquid" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a494c3f9dad3cb7ed16f1c51812cbe4b29493d6c2e5cd1e2b87477263d9534d" +dependencies = [ + "liquid-core", + "liquid-derive", + "liquid-lib", + "serde", +] + +[[package]] +name = "liquid-core" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc623edee8a618b4543e8e8505584f4847a4e51b805db1af6d9af0a3395d0d57" +dependencies = [ + "anymap2", + "itertools 0.14.0", + "kstring", + "liquid-derive", + "pest", + "pest_derive", + "regex", + "serde", + "time", +] + +[[package]] +name = "liquid-derive" +version = "0.26.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de66c928222984aea59fcaed8ba627f388aaac3c1f57dcb05cc25495ef8faefe" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "liquid-lib" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9befeedd61f5995bc128c571db65300aeb50d62e4f0542c88282dbcb5f72372a" +dependencies = [ + "itertools 0.14.0", + "liquid-core", + "percent-encoding", + "regex", + "time", + "unicode-segmentation", +] + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[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.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "lru" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "maxminddb" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99681a80368084e68fff1a4ec657b09ae6a04f1107762ec6346a82b8cc19d8eb" +dependencies = [ + "ipnetwork", + "log", + "memchr", + "serde", + "thiserror 2.0.16", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "net-guardia" +version = "0.1.0" +dependencies = [ + "actix", + "actix-cors", + "actix-web", + "actix-ws", + "aya", + "aya-log", + "cargo_metadata", + "common", + "crossbeam", + "dotenvy", + "futures", + "futures-util", + "libc", + "lru", + "macros", + "maxminddb", + "mime_guess", + "network-types", + "parking_lot", + "rust-embed", + "serde", + "serde_json", + "sysinfo", + "thiserror 2.0.16", + "tokio", + "tokio-tungstenite", + "toml", + "tracing", + "tracing-appender", + "tracing-subscriber", + "tract-onnx", + "url", + "xsk-rs", +] + +[[package]] +name = "network-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f06f1863cb5565864300c6bfb012312969908878d2ca5881eaf0bbdb8b519c23" +dependencies = [ + "memoffset", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom-language" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2de2bc5b451bfedaef92c90b8939a8fff5770bdcc1fafd6239d086aab8fa6b29" +dependencies = [ + "nom 8.0.0", +] + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "crc32fast", + "hashbrown 0.15.2", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +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 0.5.8", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "pest_meta" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +dependencies = [ + "pest", + "sha2", +] + +[[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 = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy 0.7.35", +] + +[[package]] +name = "prettyplease" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +dependencies = [ + "proc-macro2", + "syn 2.0.98", +] + +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "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]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "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]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rust-embed" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.98", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustfft" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + +[[package]] +name = "safetensors" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172dd94c5a87b5c79f945c863da53b2ebc7ccef4eca24ac63cca66a41aab2178" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scan_fmt" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b53b0a5db882a8e2fdaae0a43f7b39e7e9082389e978398bdf223a55b581248" +dependencies = [ + "regex", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "string-interner" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07f9fdfdd31a0ff38b59deb401be81b73913d76c9cc5b1aed4e1330a223420b9" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "sysinfo" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efc19935b4b66baa6f654ac7924c192f55b175c00a7ab72410fc24284dacda8" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows", +] + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl 2.0.16", +] + +[[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 2.0.98", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +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", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.0", + "tokio-macros", + "windows-sys 0.61.1", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "hashbrown 0.14.5", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "1.0.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tract-core" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d72bdfb1d8809fc16b7e3496c8a3a31e8c55eeec8f648f2715d87bd25e9db1c" +dependencies = [ + "anyhow", + "anymap3", + "bit-set", + "derive-new", + "downcast-rs", + "dyn-clone", + "lazy_static", + "log", + "maplit", + "ndarray", + "num-complex", + "num-integer", + "num-traits", + "pastey", + "rustfft", + "smallvec", + "tract-data", + "tract-linalg", +] + +[[package]] +name = "tract-data" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb9833e90b72a7a8e7abc517e79a90c1463d88550531deaabd4b5dbf706a091b" +dependencies = [ + "anyhow", + "downcast-rs", + "dyn-clone", + "dyn-hash", + "half", + "itertools 0.12.1", + "lazy_static", + "libm", + "maplit", + "ndarray", + "nom 8.0.0", + "nom-language", + "num-integer", + "num-traits", + "parking_lot", + "scan_fmt", + "smallvec", + "string-interner", +] + +[[package]] +name = "tract-hir" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe98f1a0fe9d7bcc39a64258729940da99a26e0ecac8d474d17cfd15f9e4ecf" +dependencies = [ + "derive-new", + "log", + "tract-core", +] + +[[package]] +name = "tract-linalg" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d09562926176740991a4e74ada093ed4edc155a585c45e2dd6fa13994a89f04f" +dependencies = [ + "byteorder", + "cc", + "derive-new", + "downcast-rs", + "dyn-clone", + "dyn-hash", + "half", + "lazy_static", + "liquid", + "liquid-core", + "liquid-derive", + "log", + "num-traits", + "pastey", + "scan_fmt", + "smallvec", + "time", + "tract-data", + "unicode-normalization", + "walkdir", +] + +[[package]] +name = "tract-nnef" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "160625a1b79132698ac292ff555c575a567a2c27cd2371c53c13225cbda8d1de" +dependencies = [ + "byteorder", + "flate2", + "liquid", + "liquid-core", + "log", + "nom 8.0.0", + "nom-language", + "safetensors", + "serde_json", + "tar", + "tract-core", + "walkdir", +] + +[[package]] +name = "tract-onnx" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96a3bf1d24b5ca9e4371dac498c6c3cb47d1e4eb7fede41a5750e611da45274" +dependencies = [ + "bytes", + "derive-new", + "log", + "memmap2", + "num-integer", + "prost", + "smallvec", + "tract-hir", + "tract-nnef", + "tract-onnx-opl", +] + +[[package]] +name = "tract-onnx-opl" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1e8a95ae93fdc53143586ec00d816c2831c6958a8a59cc4122ca5b8dba6070" +dependencies = [ + "getrandom 0.2.17", + "log", + "rand 0.8.5", + "rand_distr", + "rustfft", + "tract-nnef", +] + +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "rand 0.9.2", + "sha1", + "thiserror 2.0.16", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.3+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "which" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" +dependencies = [ + "env_home", + "rustix", + "winsafe", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e6c4a1f363c8210c6f77ba24f645c61c6fb941eccf013da691f7e09515b8ac" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123e712f464a8a60ce1a13f4c446d2d43ab06464cb5842ff68f5c71b6fb7852e" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f3db6b24b120200d649cd4811b4947188ed3a8d2626f7075146c5d178a9a4a" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-numerics" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce3498fe0aba81e62e477408383196b4b0363db5e0c27646f932676283b43d8" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab47f085ad6932defa48855254c758cdd0e2f2d48e62a34118a268d8f345e118" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "wit-bindgen" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "xsk-rs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1fef46e3505c5055082f52ada0a7f8e5dcaebdbb9eccf8e978c32382c159270" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "libxdp-sys", + "log", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive 0.8.33", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d5bed6b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,34 @@ +[workspace] +resolver = "2" +members = ["net-guardia", "common", "macros", "ingress-ebpf", "egress-ebpf"] +default-members = ["net-guardia", "common"] + +[workspace.dependencies] +aya = { version = "0.13.1", default-features = false } +aya-ebpf = { version = "0.1.1", default-features = false } +aya-log = { version = "0.2.1", default-features = false } +aya-log-ebpf = { version = "0.1.0", default-features = false } +cargo_metadata = { version = "0.23.1", default-features = false } +libc = { version = "0.2.159", default-features = false } +network-types = "0.1.0" +serde = { version = "1.0.215", features = ["derive"] } +xsk-rs = { version = "0.8.0", default-features = false } + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" +#opt-level = 3 +#lto = true +#strip = true +#debug = false +#overflow-checks = false + +[profile.release.package.ingress-ebpf] +debug = 2 +codegen-units = 1 + +[profile.release.package.egress-ebpf] +debug = 2 +codegen-units = 1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7b47c7d --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# NetGuardia + +## Project Overview + +**NetGuardia** is a high-performance network security solution that combines eBPF XDP technology with deep learning models to provide advanced network protection. The system operates as a standalone network appliance that can run on any Ubuntu-based system with compatible network hardware. + +## Core Technologies + +- **eBPF XDP Technology** - Provides high-performance packet processing directly at the data link layer +- **Deep Learning Models** - Identifies and predicts potential network attacks with intelligent threat detection +- **Hardware Integration** - Designed to work with Intel i350 T2 and similar enterprise-grade network interface cards + +## Functional Modules + +### Resource Overview +![Home](.github/images/index.png) +- Real-time control system occupancy rate + +### Dashboard Overview +![Dashboard](.github/images/dashboard.png) +- Real-time network traffic monitoring and visualization +- Recent traffic statistics and trend analysis + +### Detailed Traffic Statistics +![Statistics](.github/images/statistics.png) +- Detailed traffic usage information per IP address + +### Network Access Control +![accessControl](.github/images/accessControl.png) +- IPv4/IPv6 whitelist and blacklist management +- Precise port-level access control + +[//]: # (### AI Attack Detection) + +[//]: # (![AI 攻擊偵測介面](.github/images/aiDetection.png)) + +[//]: # (- AI-based attack detection engine) + +## System Features + +- **High Performance** - Low-latency packet processing with minimal network performance impact +- **User-Friendly** - Cross-platform web management interface with intuitive operation +- **Reliability** - Hardware-accelerated processing ensures stable operation +- **Scalability** - Modular design supports functional expansion + +## System Requirements + +- Ubuntu-based operating system (Ubuntu 24.04 LTS or newer recommended) +- Dual-port network interface card (Intel i350 T2 or compatible XDP-capable NIC) +- Root/sudo access for eBPF program loading + +## Hardware Compatibility +NetGuardia is designed to work on any Ubuntu-based system meeting the following requirements: + +- Network Interface: Any dual-port NIC supporting XDP native or offload mode (Intel i350 T2 recommended) +- CPU: Multi-core processor recommended for optimal performance +- Memory: 8GB RAM minimum, 16GB or more for high-traffic environments + +The system is not limited to embedded platforms and can be deployed on standard server hardware, virtual machines, or dedicated appliances running Ubuntu. + diff --git a/common/Cargo.toml b/common/Cargo.toml new file mode 100644 index 0000000..1cfaa89 --- /dev/null +++ b/common/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "common" +version = "0.1.0" +edition = "2024" + +[features] +default = [] +user = ["aya", "serde"] +kernel = ["aya-ebpf"] + +[dependencies] +aya = { workspace = true, optional = true } +aya-ebpf = { workspace = true, optional = true } +serde = { workspace = true, optional = true } +network-types = { workspace = true } + +[lib] +path = "src/lib.rs" diff --git a/common/src/define/mod.rs b/common/src/define/mod.rs new file mode 100644 index 0000000..f2cdfca --- /dev/null +++ b/common/src/define/mod.rs @@ -0,0 +1,4 @@ +pub mod offset; +pub mod other; +pub mod program_array; +pub mod setting; diff --git a/common/src/define/offset.rs b/common/src/define/offset.rs new file mode 100644 index 0000000..70f4702 --- /dev/null +++ b/common/src/define/offset.rs @@ -0,0 +1,25 @@ +use network_types::eth::EthHdr; +use network_types::ip::{Ipv4Hdr, Ipv6Hdr}; +use network_types::tcp::TcpHdr; +use network_types::udp::UdpHdr; + +pub const ETHER_HEADER_START: usize = 0; +pub const ETHER_HEADER_END: usize = ETHER_HEADER_START + size_of::(); + +pub const IPV4_HEADER_START: usize = ETHER_HEADER_END; +pub const IPV4_HEADER_END: usize = IPV4_HEADER_START + size_of::(); + +pub const IPV6_HEADER_START: usize = ETHER_HEADER_END; +pub const IPV6_HEADER_END: usize = IPV6_HEADER_START + size_of::(); + +pub const IPV4_TCP_HEADER_START: usize = IPV4_HEADER_END; +pub const IPV4_TCP_HEADER_END: usize = IPV4_TCP_HEADER_START + size_of::(); + +pub const IPV6_TCP_HEADER_START: usize = IPV6_HEADER_END; +pub const IPV6_TCP_HEADER_END: usize = IPV6_TCP_HEADER_START + size_of::(); + +pub const IPV4_UDP_HEADER_START: usize = IPV4_HEADER_END; +pub const IPV4_UDP_HEADER_END: usize = IPV4_UDP_HEADER_START + size_of::(); + +pub const IPV6_UDP_HEADER_START: usize = IPV6_HEADER_END; +pub const IPV6_UDP_HEADER_END: usize = IPV6_UDP_HEADER_START + size_of::(); diff --git a/common/src/define/other.rs b/common/src/define/other.rs new file mode 100644 index 0000000..135990e --- /dev/null +++ b/common/src/define/other.rs @@ -0,0 +1 @@ +pub const STANDARD_MTU: usize = 1500; diff --git a/common/src/define/program_array.rs b/common/src/define/program_array.rs new file mode 100644 index 0000000..f2b3bb1 --- /dev/null +++ b/common/src/define/program_array.rs @@ -0,0 +1,11 @@ +pub mod ingress { + pub const ACCESS_CONTROL: u32 = 0; + pub const SERVICE: u32 = 1; + pub const STATISTICS: u32 = 2; + pub const TRANSMISSION: u32 = 3; +} + +pub mod egress { + pub const STATISTICS: u32 = 0; + pub const TRANSMISSION: u32 = 1; +} diff --git a/common/src/define/setting.rs b/common/src/define/setting.rs new file mode 100644 index 0000000..fda8448 --- /dev/null +++ b/common/src/define/setting.rs @@ -0,0 +1,4 @@ +pub const MAX_STATS: usize = 131072; +pub const MAX_RULES: usize = 128; +pub const MAX_RULES_PORT: usize = 32; +pub const MAX_BUFFERED_PACKETS: usize = 1024; diff --git a/common/src/ebpf/mod.rs b/common/src/ebpf/mod.rs new file mode 100644 index 0000000..29ec0ba --- /dev/null +++ b/common/src/ebpf/mod.rs @@ -0,0 +1 @@ +pub mod parsing; diff --git a/common/src/ebpf/parsing.rs b/common/src/ebpf/parsing.rs new file mode 100644 index 0000000..f746cac --- /dev/null +++ b/common/src/ebpf/parsing.rs @@ -0,0 +1,147 @@ +use aya_ebpf::helpers::bpf_ktime_get_ns; +use network_types::eth::{EthHdr, EtherType}; +use network_types::ip::{IpProto, Ipv4Hdr, Ipv6Hdr}; +use network_types::tcp::TcpHdr; +use network_types::udp::UdpHdr; + +use crate::define::offset::*; +use crate::model::event::{Event, IPv4Event, IPv6Event}; + +pub fn parse_packet(start: usize, end: usize, target: *mut Event) -> Result<(), ()> { + unsafe { + if start + ETHER_HEADER_END > end { + return Err(()); + } + let eth = &*((start + ETHER_HEADER_START) as *const EthHdr); + let ether_type = eth.ether_type().map_err(|_| ())?; + match ether_type { + EtherType::Ipv4 => parse_ipv4_packet(start, end, target), + EtherType::Ipv6 => parse_ipv6_packet(start, end, target), + _ => Err(()), + } + } +} + +#[inline(always)] +unsafe fn parse_ipv4_packet(start: usize, end: usize, target: *mut Event) -> Result<(), ()> { + unsafe { + if start + IPV4_HEADER_END > end { + return Err(()); + } + + let ipv4 = &*((start + IPV4_HEADER_START) as *const Ipv4Hdr); + + let (source_port, destination_port) = match ipv4.proto { + IpProto::Tcp => parse_tcp_port(start, end, IPV4_TCP_HEADER_START, IPV4_TCP_HEADER_END)?, + IpProto::Udp => parse_udp_port(start, end, IPV4_UDP_HEADER_START, IPV4_UDP_HEADER_END)?, + _ => return Err(()), + }; + + *(target as *mut u32) = 0; + + let ipv4_data_ptr = (target as *mut u8).add(16); + + core::ptr::write(ipv4_data_ptr as *mut IpProto, ipv4.proto); + core::ptr::copy_nonoverlapping( + ipv4.src_addr.as_ptr(), + ipv4_data_ptr.add(core::mem::offset_of!(IPv4Event, src_ip)), + 4, + ); + core::ptr::copy_nonoverlapping( + ipv4.dst_addr.as_ptr(), + ipv4_data_ptr.add(core::mem::offset_of!(IPv4Event, dst_ip)), + 4, + ); + core::ptr::write( + ipv4_data_ptr.add(core::mem::offset_of!(IPv4Event, src_port)) as *mut u16, + source_port, + ); + core::ptr::write( + ipv4_data_ptr.add(core::mem::offset_of!(IPv4Event, dst_port)) as *mut u16, + destination_port, + ); + core::ptr::write( + ipv4_data_ptr.add(core::mem::offset_of!(IPv4Event, packet_length)) as *mut u32, + (end - start) as u32, + ); + core::ptr::write( + ipv4_data_ptr.add(core::mem::offset_of!(IPv4Event, timestamp_us)) as *mut u64, + bpf_ktime_get_ns(), + ); + + Ok(()) + } +} + +#[inline(always)] +unsafe fn parse_ipv6_packet(start: usize, end: usize, target: *mut Event) -> Result<(), ()> { + unsafe { + if start + IPV6_HEADER_END > end { + return Err(()); + } + + let ipv6 = &*((start + IPV6_HEADER_START) as *const Ipv6Hdr); + + let (source_port, destination_port) = match ipv6.next_hdr { + IpProto::Tcp => parse_tcp_port(start, end, IPV6_TCP_HEADER_START, IPV6_TCP_HEADER_END)?, + IpProto::Udp => parse_udp_port(start, end, IPV6_UDP_HEADER_START, IPV6_UDP_HEADER_END)?, + _ => return Err(()), + }; + + *(target as *mut u32) = 1; + + let ipv6_data_ptr = (target as *mut u8).add(16); + + core::ptr::write(ipv6_data_ptr as *mut IpProto, ipv6.next_hdr); + core::ptr::copy_nonoverlapping( + ipv6.src_addr.as_ptr(), + ipv6_data_ptr.add(core::mem::offset_of!(IPv6Event, src_ip)), + 16, + ); + core::ptr::copy_nonoverlapping( + ipv6.dst_addr.as_ptr(), + ipv6_data_ptr.add(core::mem::offset_of!(IPv6Event, dst_ip)), + 16, + ); + core::ptr::write( + ipv6_data_ptr.add(core::mem::offset_of!(IPv6Event, src_port)) as *mut u16, + source_port, + ); + core::ptr::write( + ipv6_data_ptr.add(core::mem::offset_of!(IPv6Event, dst_port)) as *mut u16, + destination_port, + ); + core::ptr::write( + ipv6_data_ptr.add(core::mem::offset_of!(IPv6Event, packet_length)) as *mut u32, + (end - start) as u32, + ); + core::ptr::write( + ipv6_data_ptr.add(core::mem::offset_of!(IPv6Event, timestamp_us)) as *mut u64, + bpf_ktime_get_ns(), + ); + + Ok(()) + } +} + +#[inline(always)] +unsafe fn parse_tcp_port(start: usize, end: usize, tcp_start: usize, tcp_end: usize) -> Result<(u16, u16), ()> { + unsafe { + if start + tcp_end > end { + return Err(()); + } + let tcp = &*((start + tcp_start) as *const TcpHdr); + Ok((u16::from_be_bytes(tcp.source), u16::from_be_bytes(tcp.dest))) + } +} + +#[inline(always)] +unsafe fn parse_udp_port(start: usize, end: usize, udp_start: usize, udp_end: usize) -> Result<(u16, u16), ()> { + unsafe { + if start + udp_end > end { + return Err(()); + } + let udp = &*((start + udp_start) as *const UdpHdr); + Ok((udp.src_port(), udp.dst_port())) + } +} diff --git a/common/src/lib.rs b/common/src/lib.rs new file mode 100644 index 0000000..1ecf6c6 --- /dev/null +++ b/common/src/lib.rs @@ -0,0 +1,9 @@ +#![no_std] + +#[cfg(feature = "user")] +extern crate std; + +pub mod define; +#[cfg(feature = "kernel")] +pub mod ebpf; +pub mod model; diff --git a/common/src/model/event.rs b/common/src/model/event.rs new file mode 100644 index 0000000..e862aa4 --- /dev/null +++ b/common/src/model/event.rs @@ -0,0 +1,197 @@ +use network_types::ip::IpProto; + +use crate::model::ip_address::{AddrPortV4, AddrPortV6}; + +#[repr(C, align(8))] +#[derive(Clone)] +pub enum Event { + IPv4(IPv4Event), + IPv6(IPv6Event), +} + +#[repr(C, align(8))] +#[derive(Clone)] +pub enum RawIp { + V4(u32), + V6(u128), +} + +impl Event { + pub fn timestamp_us(&self) -> u64 { + match self { + Event::IPv4(e) => e.timestamp_us, + Event::IPv6(e) => e.timestamp_us, + } + } + + pub fn packet_length(&self) -> u32 { + match self { + Event::IPv4(e) => e.packet_length, + Event::IPv6(e) => e.packet_length, + } + } + + pub fn header_length(&self) -> u16 { + match self { + Event::IPv4(e) => e.header_length, + Event::IPv6(e) => e.header_length, + } + } + + pub fn payload_length(&self) -> u32 { + match self { + Event::IPv4(e) => e.payload_length, + Event::IPv6(e) => e.payload_length, + } + } + + pub fn tcp_flags(&self) -> &TcpFlags { + match self { + Event::IPv4(e) => &e.tcp_flags, + Event::IPv6(e) => &e.tcp_flags, + } + } + + pub fn tcp_window_size(&self) -> u16 { + match self { + Event::IPv4(e) => e.tcp_window_size, + Event::IPv6(e) => e.tcp_window_size, + } + } + + pub fn is_forward(&self) -> bool { + match self { + Event::IPv4(e) => e.is_forward, + Event::IPv6(e) => e.is_forward, + } + } + + pub fn protocol(&self) -> &IpProto { + match self { + Event::IPv4(e) => &e.protocol, + Event::IPv6(e) => &e.protocol, + } + } + + pub fn src_ip(&self) -> RawIp { + match self { + Event::IPv4(e) => RawIp::V4(e.src_ip), + Event::IPv6(e) => RawIp::V6(e.src_ip), + } + } + + pub fn dst_ip(&self) -> RawIp { + match self { + Event::IPv4(e) => RawIp::V4(e.dst_ip), + Event::IPv6(e) => RawIp::V6(e.dst_ip), + } + } + + pub fn src_port(&self) -> u16 { + match self { + Event::IPv4(e) => e.src_port, + Event::IPv6(e) => e.src_port, + } + } + + pub fn dst_port(&self) -> u16 { + match self { + Event::IPv4(e) => e.dst_port, + Event::IPv6(e) => e.dst_port, + } + } + + pub fn set_is_forward(&mut self, value: bool) { + match self { + Event::IPv4(e) => e.is_forward = value, + Event::IPv6(e) => e.is_forward = value, + } + } +} + + +#[repr(C, align(8))] +#[derive(Debug, Clone)] +pub struct IPv4Event { + pub protocol: IpProto, + pub src_ip: u32, + pub dst_ip: u32, + pub src_port: u16, + pub dst_port: u16, + pub packet_length: u32, + pub payload_length: u32, + pub header_length: u16, + pub timestamp_us: u64, + pub tcp_flags: TcpFlags, + pub tcp_window_size: u16, + pub is_forward: bool, +} + +impl IPv4Event { + #[inline(always)] + pub fn source_addr(&self) -> AddrPortV4 { + AddrPortV4::new(self.src_ip, self.src_port) + } + + #[inline(always)] + pub fn destination_addr(&self) -> AddrPortV4 { + AddrPortV4::new(self.dst_ip, self.dst_port) + } +} + +#[repr(C, align(8))] +#[derive(Clone)] +pub struct IPv6Event { + pub protocol: IpProto, + pub src_ip: u128, + pub dst_ip: u128, + pub src_port: u16, + pub dst_port: u16, + pub packet_length: u32, + pub payload_length: u32, + pub header_length: u16, + pub timestamp_us: u64, + pub tcp_flags: TcpFlags, + pub tcp_window_size: u16, + pub is_forward: bool, +} + +impl IPv6Event { + #[inline(always)] + pub fn source_addr(&self) -> AddrPortV6 { + AddrPortV6::new(self.src_ip, self.src_port) + } + + #[inline(always)] + pub fn destination_addr(&self) -> AddrPortV6 { + AddrPortV6::new(self.dst_ip, self.dst_port) + } +} + +#[repr(C, align(8))] +#[derive(Debug, Clone, Default)] +pub struct TcpFlags { + pub fin: bool, + pub syn: bool, + pub rst: bool, + pub psh: bool, + pub ack: bool, + pub urg: bool, + pub ece: bool, + pub cwr: bool, +} + +impl TcpFlags { + pub fn from_byte(flags: u8) -> Self { + Self { + fin: (flags & 0x01) != 0, + syn: (flags & 0x02) != 0, + rst: (flags & 0x04) != 0, + psh: (flags & 0x08) != 0, + ack: (flags & 0x10) != 0, + urg: (flags & 0x20) != 0, + ece: (flags & 0x40) != 0, + cwr: (flags & 0x80) != 0, + } + } +} diff --git a/common/src/model/flow_stats.rs b/common/src/model/flow_stats.rs new file mode 100644 index 0000000..d9ccb5c --- /dev/null +++ b/common/src/model/flow_stats.rs @@ -0,0 +1,26 @@ +#[cfg(feature = "user")] +use aya::Pod; +#[cfg(feature = "user")] +use serde::Serialize; + +#[repr(C, align(8))] +#[derive(Clone, Copy)] +#[cfg_attr(feature = "user", derive(Serialize, Debug))] +pub struct FlowStats { + pub bytes: u64, + pub packets: u64, + pub last_seen: u64, +} + +impl FlowStats { + pub fn new(bytes: u64, packets: u64, last_seen: u64) -> Self { + Self { + bytes, + packets, + last_seen, + } + } +} + +#[cfg(feature = "user")] +unsafe impl Pod for FlowStats {} diff --git a/common/src/model/http_method.rs b/common/src/model/http_method.rs new file mode 100644 index 0000000..6f91e71 --- /dev/null +++ b/common/src/model/http_method.rs @@ -0,0 +1,56 @@ +#[cfg(all(feature = "user"))] +use std::vec::Vec; + +#[cfg(feature = "user")] +use serde::{Deserialize, Serialize}; + +pub type HttpMethodBitmap = u16; + +#[derive(Copy, Clone)] +#[cfg_attr(feature = "user", derive(Serialize, Deserialize, Debug, Eq, PartialEq))] +pub enum HttpMethod { + GET = 0b0000_0000_0000_0001, + POST = 0b0000_0000_0000_0010, + PUT = 0b0000_0000_0000_0100, + DELETE = 0b0000_0000_0000_1000, + HEAD = 0b0000_0000_0001_0000, + OPTIONS = 0b0000_0000_0010_0000, + PATCH = 0b0000_0000_0100_0000, + TRACE = 0b0000_0000_1000_0000, + CONNECT = 0b0000_0001_0000_0000, +} + +#[cfg(feature = "user")] +impl HttpMethod { + pub fn convert_from_bitmap(http_method_bitmap: HttpMethodBitmap) -> Vec { + let value = http_method_bitmap as u16; + let mut http_methods = Vec::new(); + + let all_methods = [ + HttpMethod::GET, + HttpMethod::POST, + HttpMethod::PUT, + HttpMethod::DELETE, + HttpMethod::HEAD, + HttpMethod::OPTIONS, + HttpMethod::PATCH, + HttpMethod::TRACE, + HttpMethod::CONNECT, + ]; + + for method in all_methods { + if value & (method as u16) != 0 { + http_methods.push(method); + } + } + http_methods + } + + pub fn convert_to_bitmap(http_methods: Vec) -> HttpMethodBitmap { + let mut ebpf_http_method = 0_u16; + for http_method in http_methods { + ebpf_http_method |= http_method as u16; + } + ebpf_http_method + } +} diff --git a/common/src/model/ip_address.rs b/common/src/model/ip_address.rs new file mode 100644 index 0000000..84a0e99 --- /dev/null +++ b/common/src/model/ip_address.rs @@ -0,0 +1,78 @@ +#[cfg(feature = "user")] +use aya::Pod; + +pub type IPv4 = u32; +pub type IPv6 = u128; +pub type Port = u16; + +#[repr(transparent)] +#[derive(Debug, Copy, Clone)] +pub struct AddrPortV4([u8; 8]); + +impl AddrPortV4 { + #[inline(always)] + pub fn new(ip: u32, port: u16) -> Self { + let mut key = [0u8; 8]; + key[0..4].copy_from_slice(&ip.to_ne_bytes()); + key[4..6].copy_from_slice(&port.to_ne_bytes()); + Self(key) + } + + #[inline(always)] + pub fn as_bytes(&self) -> &[u8; 8] { + &self.0 + } + + #[inline(always)] + pub fn ip(&self) -> IPv4 { + let mut ip_bytes = [0u8; 4]; + ip_bytes.copy_from_slice(&self.0[0..4]); + u32::from_ne_bytes(ip_bytes) + } + + #[inline(always)] + pub fn port(&self) -> Port { + let mut port_bytes = [0u8; 2]; + port_bytes.copy_from_slice(&self.0[4..6]); + u16::from_ne_bytes(port_bytes) + } +} + +#[cfg(feature = "user")] +unsafe impl Pod for AddrPortV4 {} + +#[repr(transparent)] +#[derive(Debug, Copy, Clone)] +pub struct AddrPortV6([u8; 32]); + +impl AddrPortV6 { + #[inline(always)] + pub fn new(ip: u128, port: u16) -> Self { + let mut key = [0u8; 32]; + key[0..16].copy_from_slice(&ip.to_ne_bytes()); + key[16..18].copy_from_slice(&port.to_ne_bytes()); + Self(key) + } + + #[inline(always)] + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + #[inline(always)] + pub fn ip(&self) -> IPv6 { + let mut ip_bytes = [0u8; 16]; + ip_bytes.copy_from_slice(&self.0[0..16]); + u128::from_ne_bytes(ip_bytes) + } + + #[inline(always)] + pub fn port(&self) -> Port { + let mut port_bytes = [0u8; 2]; + port_bytes.copy_from_slice(&self.0[16..18]); + u16::from_ne_bytes(port_bytes) + } +} + +#[cfg(feature = "user")] +unsafe impl Pod for AddrPortV6 {} diff --git a/common/src/model/mod.rs b/common/src/model/mod.rs new file mode 100644 index 0000000..a008c77 --- /dev/null +++ b/common/src/model/mod.rs @@ -0,0 +1,7 @@ +pub mod event; +pub mod flow_stats; +pub mod http_method; +pub mod ip_address; +pub mod packet; +pub mod placeholder; +pub mod pseudo_header; diff --git a/common/src/model/packet.rs b/common/src/model/packet.rs new file mode 100644 index 0000000..dd7f95f --- /dev/null +++ b/common/src/model/packet.rs @@ -0,0 +1,5 @@ +use crate::define::other::STANDARD_MTU; +use crate::model::event::Event; + +#[repr(transparent)] +pub struct Packet(pub [u8; size_of::() + STANDARD_MTU]); diff --git a/common/src/model/placeholder.rs b/common/src/model/placeholder.rs new file mode 100644 index 0000000..b88edcb --- /dev/null +++ b/common/src/model/placeholder.rs @@ -0,0 +1 @@ +pub type PlaceHolder = u8; diff --git a/common/src/model/pseudo_header.rs b/common/src/model/pseudo_header.rs new file mode 100644 index 0000000..7e91211 --- /dev/null +++ b/common/src/model/pseudo_header.rs @@ -0,0 +1,19 @@ +#[repr(C)] +#[derive(Clone, Copy)] +pub struct IPv4PseudoHeader { + pub source_ip: u32, + pub destination_ip: u32, + pub zeros: u8, + pub protocol: u8, + pub length: u16, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct IPv6PseudoHeader { + pub source_ip: u128, + pub destination_ip: u128, + pub length: u16, + pub zeros: u8, + pub next_header: u8, +} diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..7961902 --- /dev/null +++ b/config.toml @@ -0,0 +1,26 @@ +[Config] +ingress_ifname = "enp4s0f1" # Ingress NIC Name +egress_ifname = "enp4s0f0" # Egress NIC Name +geoip_db_name = "GeoLite2-City.mmdb" +deep_autoencoder_name = "deep_autoencoder.onnx" +classifier_name = "classifier.onnx" +models_config_name = "inference_config.json" +combined_queue_count = 8 # NIC Combined Queue Count (ethtool -l ) +channel_size = 4096 +fill_queue_size = 4096 # Umem Used (Should not modify) +comp_queue_size = 4096 # Umem Used (Should not modify) +tx_queue_size = 4096 # Umem Used (Should not modify) +rx_queue_size = 4096 # Umem Used (Should not modify) +frame_size = 4096 # Umem Used (Should not modify) +frame_count = 4096 # Umem Used (Should not modify) +http_server_bind_port = 8080 # Http Server Listen Port +refresh_interval = 5 # Statistics Refresh Time + +max_concurrent_flows = 10000 # max_flows: track up to 10000 concurrent flows +min_packets_for_inference = 5 # min_packets: minimum 10 packets per flow for inference +inference_interval_secs = 5 # interval_secs: run inference every 5 seconds +aggregator_window_secs = 30 +inference_batch_size = 200 + +traffic_logging_mode = true # When true, disables ML inference and records all ingress/egress packets to CSV +traffic_log_csv_path = "traffic_log.csv" # Output CSV file path for traffic logging mode \ No newline at end of file diff --git a/egress-ebpf/.cargo/config.toml b/egress-ebpf/.cargo/config.toml new file mode 100644 index 0000000..1678510 --- /dev/null +++ b/egress-ebpf/.cargo/config.toml @@ -0,0 +1,5 @@ +[build] +target = ["bpfeb-unknown-none", "bpfel-unknown-none"] + +[unstable] +build-std = ["core"] diff --git a/egress-ebpf/Cargo.toml b/egress-ebpf/Cargo.toml new file mode 100644 index 0000000..5bb79f9 --- /dev/null +++ b/egress-ebpf/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "egress-ebpf" +version = "0.1.0" +edition = "2024" + +[dependencies] +common = { path = "../common", features = ["kernel"] } + +aya-ebpf = { workspace = true } +aya-log-ebpf = { workspace = true } + +[build-dependencies] +which = "8.0.0" + +[[bin]] +name = "net-guardia-egress" +path = "src/main.rs" +test = false +doctest = false +bench = false diff --git a/egress-ebpf/build.rs b/egress-ebpf/build.rs new file mode 100644 index 0000000..0bdd7b5 --- /dev/null +++ b/egress-ebpf/build.rs @@ -0,0 +1,6 @@ +use which::which; + +fn main() { + let bpf_linker = which("bpf-linker").unwrap(); + println!("cargo:rerun-if-changed={}", bpf_linker.to_str().unwrap()); +} diff --git a/egress-ebpf/rust-toolchain.toml b/egress-ebpf/rust-toolchain.toml new file mode 100644 index 0000000..f70d225 --- /dev/null +++ b/egress-ebpf/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly" +components = ["rust-src"] diff --git a/egress-ebpf/src/action/mod.rs b/egress-ebpf/src/action/mod.rs new file mode 100644 index 0000000..3449ec7 --- /dev/null +++ b/egress-ebpf/src/action/mod.rs @@ -0,0 +1 @@ +pub mod statistics; diff --git a/egress-ebpf/src/action/statistics.rs b/egress-ebpf/src/action/statistics.rs new file mode 100644 index 0000000..f0a4d41 --- /dev/null +++ b/egress-ebpf/src/action/statistics.rs @@ -0,0 +1,93 @@ +use aya_ebpf::macros::map; +use aya_ebpf::maps::LruHashMap; +use common::define::setting::MAX_STATS; +use common::model::event::{IPv4Event, IPv6Event}; +use common::model::flow_stats::FlowStats; +use common::model::ip_address::{AddrPortV4, AddrPortV6}; + +#[map] +static IPV4_EGRESS_SRC_1MIN: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV4_EGRESS_SRC_10MIN: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV4_EGRESS_SRC_1HOUR: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV6_EGRESS_SRC_1MIN: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV6_EGRESS_SRC_10MIN: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV6_EGRESS_SRC_1HOUR: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV4_EGRESS_DST_1MIN: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV4_EGRESS_DST_10MIN: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV4_EGRESS_DST_1HOUR: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV6_EGRESS_DST_1MIN: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV6_EGRESS_DST_10MIN: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV6_EGRESS_DST_1HOUR: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); + +pub fn ipv4_update_stats(event: &IPv4Event) { + unsafe { + let source = event.source_addr(); + let destination = event.destination_addr(); + ipv4_update_flow_stats(&IPV4_EGRESS_SRC_1MIN, &source, event); + ipv4_update_flow_stats(&IPV4_EGRESS_SRC_10MIN, &source, event); + ipv4_update_flow_stats(&IPV4_EGRESS_SRC_1HOUR, &source, event); + ipv4_update_flow_stats(&IPV4_EGRESS_DST_1MIN, &destination, event); + ipv4_update_flow_stats(&IPV4_EGRESS_DST_10MIN, &destination, event); + ipv4_update_flow_stats(&IPV4_EGRESS_DST_1HOUR, &destination, event); + } +} + +pub fn ipv6_update_stats(event: &IPv6Event) { + unsafe { + let source = event.source_addr(); + let destination = event.destination_addr(); + ipv6_update_flow_status(&IPV6_EGRESS_SRC_1MIN, &source, event); + ipv6_update_flow_status(&IPV6_EGRESS_SRC_10MIN, &source, event); + ipv6_update_flow_status(&IPV6_EGRESS_SRC_1HOUR, &source, event); + ipv6_update_flow_status(&IPV6_EGRESS_DST_1MIN, &destination, event); + ipv6_update_flow_status(&IPV6_EGRESS_DST_10MIN, &destination, event); + ipv6_update_flow_status(&IPV6_EGRESS_DST_1HOUR, &destination, event); + } +} + +#[inline(always)] +unsafe fn ipv4_update_flow_stats(map: &LruHashMap, key: &AddrPortV4, event: &IPv4Event) { + unsafe { + if let Some(status) = map.get_ptr_mut(key) { + (*status).bytes += event.packet_length as u64; + (*status).packets += 1; + (*status).last_seen = event.timestamp_us; + } else { + let new_stats = FlowStats { + bytes: event.packet_length as u64, + packets: 1, + last_seen: event.timestamp_us, + }; + let _ = map.insert(key, &new_stats, 0); + } + } +} + +#[inline(always)] +unsafe fn ipv6_update_flow_status(map: &LruHashMap, key: &AddrPortV6, event: &IPv6Event) { + unsafe { + if let Some(status) = map.get_ptr_mut(key) { + (*status).bytes += event.packet_length as u64; + (*status).packets += 1; + (*status).last_seen = event.timestamp_us; + } else { + let new_stats = FlowStats { + bytes: event.packet_length as u64, + packets: 1, + last_seen: event.timestamp_us, + }; + let _ = map.insert(key, &new_stats, 0); + } + } +} diff --git a/egress-ebpf/src/lib.rs b/egress-ebpf/src/lib.rs new file mode 100644 index 0000000..0c9ac1a --- /dev/null +++ b/egress-ebpf/src/lib.rs @@ -0,0 +1 @@ +#![no_std] diff --git a/egress-ebpf/src/main.rs b/egress-ebpf/src/main.rs new file mode 100644 index 0000000..b9796ff --- /dev/null +++ b/egress-ebpf/src/main.rs @@ -0,0 +1,79 @@ +#![no_std] +#![no_main] +mod action; + +use action::statistics; +use aya_ebpf::bindings::xdp_action; +use aya_ebpf::macros::{map, xdp}; +use aya_ebpf::maps::{PerCpuArray, ProgramArray, XskMap}; +use aya_ebpf::programs::XdpContext; +#[allow(unused_imports)] +use aya_log_ebpf::info; +use common::define::program_array::egress::*; +use common::{ebpf::parsing, model::event::Event}; + +#[map] +static PROGRAM_ARRAY: ProgramArray = ProgramArray::with_max_entries(8, 0); +#[map] +static PARSED_PACKET: PerCpuArray = PerCpuArray::with_max_entries(1, 0); +#[map] +static EGRESS_XSKS_MAP: XskMap = XskMap::pinned(64, 0); + +#[xdp] +pub fn net_guardia(ctx: XdpContext) -> u32 { + unsafe { + let _ = packet_intake(ctx); + xdp_action::XDP_PASS + } +} + +unsafe fn packet_intake(ctx: XdpContext) -> Result { + unsafe { + let start = ctx.data(); + let end = ctx.data_end(); + let ptr = PARSED_PACKET.get_ptr_mut(0).ok_or(())?; + parsing::parse_packet(start, end, ptr)?; + let _ = PROGRAM_ARRAY.tail_call(&ctx, STATISTICS); + Err(()) + } +} + +#[xdp] +pub fn statistics(ctx: XdpContext) -> u32 { + unsafe { + let _ = try_statistics(&ctx); + xdp_action::XDP_PASS + } +} + +unsafe fn try_statistics(ctx: &XdpContext) -> Result { + unsafe { + let ptr = PARSED_PACKET.get_ptr(0).ok_or(())?; + let parsed_packet = &*ptr; + match parsed_packet { + Event::IPv4(event) => { + statistics::ipv4_update_stats(event); + } + Event::IPv6(event) => { + statistics::ipv6_update_stats(event); + } + } + let _ = PROGRAM_ARRAY.tail_call(ctx, TRANSMISSION); + Ok(xdp_action::XDP_PASS) + } +} + +#[xdp] +pub fn transmission(ctx: XdpContext) -> u32 { + let queue_id = unsafe { (*ctx.ctx).rx_queue_index }; + match EGRESS_XSKS_MAP.redirect(queue_id, 0) { + Ok(action) => action, + Err(_) => xdp_action::XDP_PASS, + } +} + +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { core::hint::unreachable_unchecked() } +} diff --git a/ingress-ebpf/.cargo/config.toml b/ingress-ebpf/.cargo/config.toml new file mode 100644 index 0000000..1678510 --- /dev/null +++ b/ingress-ebpf/.cargo/config.toml @@ -0,0 +1,5 @@ +[build] +target = ["bpfeb-unknown-none", "bpfel-unknown-none"] + +[unstable] +build-std = ["core"] diff --git a/ingress-ebpf/Cargo.toml b/ingress-ebpf/Cargo.toml new file mode 100644 index 0000000..aaff5dc --- /dev/null +++ b/ingress-ebpf/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "ingress-ebpf" +version = "0.1.0" +edition = "2024" + +[dependencies] +common = { path = "../common", features = ["kernel"] } + +aya-ebpf = { workspace = true } +aya-log-ebpf = { workspace = true } +network-types = { workspace = true } + +[build-dependencies] +which = "8.0.0" + +[[bin]] +name = "net-guardia-ingress" +path = "src/main.rs" +test = false +doctest = false +bench = false \ No newline at end of file diff --git a/ingress-ebpf/build.rs b/ingress-ebpf/build.rs new file mode 100644 index 0000000..0bdd7b5 --- /dev/null +++ b/ingress-ebpf/build.rs @@ -0,0 +1,6 @@ +use which::which; + +fn main() { + let bpf_linker = which("bpf-linker").unwrap(); + println!("cargo:rerun-if-changed={}", bpf_linker.to_str().unwrap()); +} diff --git a/ingress-ebpf/rust-toolchain.toml b/ingress-ebpf/rust-toolchain.toml new file mode 100644 index 0000000..f70d225 --- /dev/null +++ b/ingress-ebpf/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly" +components = ["rust-src"] diff --git a/ingress-ebpf/src/action/access_control.rs b/ingress-ebpf/src/action/access_control.rs new file mode 100644 index 0000000..c447edc --- /dev/null +++ b/ingress-ebpf/src/action/access_control.rs @@ -0,0 +1,102 @@ +use aya_ebpf::macros::map; +use aya_ebpf::maps::HashMap; +use common::define::setting::{MAX_RULES, MAX_RULES_PORT}; +use common::model::event::{IPv4Event, IPv6Event}; +use common::model::ip_address::{IPv4, IPv6, Port}; + +#[map] +static IPV4_SRC_WHITELIST: HashMap = HashMap::with_max_entries(MAX_RULES as u32, 0); +#[map] +static IPV6_SRC_WHITELIST: HashMap = HashMap::with_max_entries(MAX_RULES as u32, 0); +#[map] +static IPV4_DST_WHITELIST: HashMap = HashMap::with_max_entries(MAX_RULES as u32, 0); +#[map] +static IPV6_DST_WHITELIST: HashMap = HashMap::with_max_entries(MAX_RULES as u32, 0); +#[map] +static IPV4_SRC_BLACKLIST: HashMap = HashMap::with_max_entries(MAX_RULES as u32, 0); +#[map] +static IPV6_SRC_BLACKLIST: HashMap = HashMap::with_max_entries(MAX_RULES as u32, 0); +#[map] +static IPV4_DST_BLACKLIST: HashMap = HashMap::with_max_entries(MAX_RULES as u32, 0); +#[map] +static IPV6_DST_BLACKLIST: HashMap = HashMap::with_max_entries(MAX_RULES as u32, 0); + +pub fn ipv4_is_whitelisted(event: &IPv4Event) -> bool { + unsafe { + if let Some(ports) = IPV4_SRC_WHITELIST.get(&event.src_ip) { + if is_port_exist(ports, event.src_port) { + return true; + } + } + if let Some(ports) = IPV4_DST_WHITELIST.get(&event.dst_ip) { + if is_port_exist(ports, event.dst_port) { + return true; + } + } + } + false +} + +pub fn ipv6_is_whitelisted(event: &IPv6Event) -> bool { + unsafe { + if let Some(ports) = IPV6_SRC_WHITELIST.get(&event.src_ip) { + if is_port_exist(ports, event.src_port) { + return true; + } + } + if let Some(ports) = IPV6_DST_WHITELIST.get(&event.dst_ip) { + if is_port_exist(ports, event.dst_port) { + return true; + } + } + } + false +} + +pub fn ipv4_is_blacklisted(event: &IPv4Event) -> bool { + unsafe { + if let Some(ports) = IPV4_SRC_BLACKLIST.get(&event.src_ip) { + if is_port_exist(ports, event.src_port) { + return true; + } + } + if let Some(ports) = IPV4_DST_BLACKLIST.get(&event.dst_ip) { + if is_port_exist(ports, event.dst_port) { + return true; + } + } + } + false +} + +pub fn ipv6_is_blacklisted(event: &IPv6Event) -> bool { + unsafe { + if let Some(ports) = IPV6_SRC_BLACKLIST.get(&event.src_ip) { + if is_port_exist(ports, event.src_port) { + return true; + } + } + if let Some(ports) = IPV6_DST_BLACKLIST.get(&event.dst_ip) { + if is_port_exist(ports, event.dst_port) { + return true; + } + } + } + false +} + +#[inline(always)] +fn is_port_exist(ports: &[Port; MAX_RULES_PORT], target_port: Port) -> bool { + if ports.get(0) == Some(&0) { + return true; + } + for &port in ports.iter() { + if port == 0 { + break; + } + if port == target_port { + return true; + } + } + false +} diff --git a/ingress-ebpf/src/action/mod.rs b/ingress-ebpf/src/action/mod.rs new file mode 100644 index 0000000..501fc26 --- /dev/null +++ b/ingress-ebpf/src/action/mod.rs @@ -0,0 +1,3 @@ +pub mod access_control; +pub mod service; +pub mod statistics; diff --git a/ingress-ebpf/src/action/service.rs b/ingress-ebpf/src/action/service.rs new file mode 100644 index 0000000..28ae4fc --- /dev/null +++ b/ingress-ebpf/src/action/service.rs @@ -0,0 +1,163 @@ +use aya_ebpf::macros::map; +use aya_ebpf::maps::{Array, HashMap}; +use common::define::offset::*; +use common::define::setting::MAX_RULES; +use common::model::event::{IPv4Event, IPv6Event}; +use common::model::http_method::HttpMethodBitmap; +use common::model::ip_address::*; +use common::model::placeholder::PlaceHolder; +use network_types::ip::IpProto; +use network_types::tcp::TcpHdr; + +#[map] +static IPV4_HTTP_SERVICE: HashMap = HashMap::with_max_entries(MAX_RULES as u32, 0); +#[map] +static IPV6_HTTP_SERVICE: HashMap = HashMap::with_max_entries(MAX_RULES as u32, 0); +#[map] +static SSH_WHITE_LIST_ENABLE: Array = Array::with_max_entries(1, 0); +#[map] +static IPV4_SSH_SERVICE: HashMap = HashMap::with_max_entries(MAX_RULES as u32, 0); +#[map] +static IPV6_SSH_SERVICE: HashMap = HashMap::with_max_entries(MAX_RULES as u32, 0); +#[map] +static IPV4_SSH_WHITE_LIST: HashMap = HashMap::with_max_entries(MAX_RULES as u32, 0); +#[map] +static IPV6_SSH_WHITE_LIST: HashMap = HashMap::with_max_entries(MAX_RULES as u32, 0); +#[map] +static IPV4_SSH_BLACK_LIST: HashMap = HashMap::with_max_entries(MAX_RULES as u32, 0); +#[map] +static IPV6_SSH_BLACK_LIST: HashMap = HashMap::with_max_entries(MAX_RULES as u32, 0); + +pub fn ipv4_service_rule_violation(start: usize, end: usize, event: &IPv4Event) -> bool { + let protocol = event.protocol; + let source = event.source_addr(); + let destination = event.destination_addr(); + ipv4_http_service_violation(start, end, &protocol, &destination) + || ipv4_ssh_service_violation(&source, &destination) +} + +pub fn ipv6_service_rule_violation(start: usize, end: usize, event: &IPv6Event) -> bool { + let protocol = event.protocol; + let source = event.source_addr(); + let destination = event.destination_addr(); + ipv6_http_service_violation(start, end, &protocol, &destination) + || ipv6_ssh_service_violation(&source, &destination) +} + +#[inline(always)] +fn ipv4_http_service_violation(start: usize, end: usize, protocol: &IpProto, destination: &AddrPortV4) -> bool { + match IPV4_HTTP_SERVICE.get_ptr_mut(destination) { + Some(allow_method) => { + if !matches!(protocol, IpProto::Tcp) { + return false; + } + unsafe { + if start + IPV4_TCP_HEADER_END > end { + return false; + } + let tcp_header = &*((start + IPV4_TCP_HEADER_START) as *const TcpHdr); + if tcp_header.syn() != 0 || tcp_header.rst() != 0 || tcp_header.fin() != 0 { + return false; + } + if tcp_header.psh() == 0 || tcp_header.ack() == 0 { + return false; + } + let doff = tcp_header.doff(); + if doff < 5 || doff > 15 { + return false; + } + let tcp_header_len = (doff * 4) as usize; + let tcp_payload_start = IPV4_TCP_HEADER_END + tcp_header_len; + match get_http_request_method(start, end, tcp_payload_start) { + Some(http_method) => *allow_method & http_method == 0, + None => true, + } + } + } + None => false, + } +} + +#[inline(always)] +fn ipv6_http_service_violation(start: usize, end: usize, protocol: &IpProto, destination: &AddrPortV6) -> bool { + match IPV6_HTTP_SERVICE.get_ptr_mut(destination) { + Some(allow_method) => { + if !matches!(protocol, IpProto::Tcp) { + return false; + } + unsafe { + if start + IPV6_TCP_HEADER_END > end { + return false; + } + let tcp_header = &*((start + IPV6_TCP_HEADER_START) as *const TcpHdr); + if tcp_header.syn() != 0 || tcp_header.rst() != 0 || tcp_header.fin() != 0 { + return false; + } + if tcp_header.psh() == 0 || tcp_header.ack() == 0 { + return false; + } + let doff = tcp_header.doff(); + if doff < 5 || doff > 15 { + return false; + } + let tcp_header_len = (doff * 4) as usize; + let tcp_payload_start = IPV6_TCP_HEADER_END + tcp_header_len; + match get_http_request_method(start, end, tcp_payload_start) { + Some(http_method) => *allow_method & http_method == 0, + None => true, + } + } + } + None => false, + } +} + +#[inline(always)] +fn get_http_request_method(start: usize, end: usize, offset: usize) -> Option { + if start + offset + 8 > end { + return None; + } + let data = unsafe { core::slice::from_raw_parts((start + offset) as *const u8, 8) }; + match &data[..4] { + b"GET " => Some(1 << 0), + b"POST" if &data[4..5] == b" " => Some(1 << 1), + b"PUT " => Some(1 << 2), + b"DELE" if &data[4..7] == b"TE " => Some(1 << 3), + b"HEAD" if &data[4..5] == b" " => Some(1 << 4), + b"OPTI" if &data[4..8] == b"ONS " => Some(1 << 5), + b"PATC" if &data[4..6] == b"H " => Some(1 << 6), + b"TRAC" if &data[4..6] == b"E " => Some(1 << 7), + b"CONN" if &data[4..8] == b"ECT " => Some(1 << 8), + _ => None, + } +} + +#[inline(always)] +fn ipv4_ssh_service_violation(source: &AddrPortV4, destination: &AddrPortV4) -> bool { + unsafe { + if IPV4_SSH_SERVICE.get(destination).is_some() { + if SSH_WHITE_LIST_ENABLE.get(0).is_some() { + IPV4_SSH_WHITE_LIST.get(&source.ip()).is_none() + } else { + IPV4_SSH_BLACK_LIST.get(&source.ip()).is_some() + } + } else { + false + } + } +} + +#[inline(always)] +fn ipv6_ssh_service_violation(source_ip: &AddrPortV6, destination: &AddrPortV6) -> bool { + unsafe { + if IPV6_SSH_SERVICE.get(destination).is_some() { + if SSH_WHITE_LIST_ENABLE.get(0).is_some() { + IPV6_SSH_WHITE_LIST.get(&source_ip.ip()).is_none() + } else { + IPV6_SSH_BLACK_LIST.get(&source_ip.ip()).is_some() + } + } else { + false + } + } +} diff --git a/ingress-ebpf/src/action/statistics.rs b/ingress-ebpf/src/action/statistics.rs new file mode 100644 index 0000000..9ac6f77 --- /dev/null +++ b/ingress-ebpf/src/action/statistics.rs @@ -0,0 +1,93 @@ +use aya_ebpf::macros::map; +use aya_ebpf::maps::LruHashMap; +use common::define::setting::MAX_STATS; +use common::model::event::{IPv4Event, IPv6Event}; +use common::model::flow_stats::FlowStats; +use common::model::ip_address::{AddrPortV4, AddrPortV6}; + +#[map] +static IPV4_INGRESS_SRC_1MIN: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV4_INGRESS_SRC_10MIN: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV4_INGRESS_SRC_1HOUR: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV6_INGRESS_SRC_1MIN: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV6_INGRESS_SRC_10MIN: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV6_INGRESS_SRC_1HOUR: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV4_INGRESS_DST_1MIN: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV4_INGRESS_DST_10MIN: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV4_INGRESS_DST_1HOUR: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV6_INGRESS_DST_1MIN: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV6_INGRESS_DST_10MIN: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); +#[map] +static IPV6_INGRESS_DST_1HOUR: LruHashMap = LruHashMap::with_max_entries(MAX_STATS as u32, 0); + +pub fn ipv4_update_stats(event: &IPv4Event) { + unsafe { + let source = event.source_addr(); + let destination = event.destination_addr(); + ipv4_update_flow_stats(&IPV4_INGRESS_SRC_1MIN, &source, event); + ipv4_update_flow_stats(&IPV4_INGRESS_SRC_10MIN, &source, event); + ipv4_update_flow_stats(&IPV4_INGRESS_SRC_1HOUR, &source, event); + ipv4_update_flow_stats(&IPV4_INGRESS_DST_1MIN, &destination, event); + ipv4_update_flow_stats(&IPV4_INGRESS_DST_10MIN, &destination, event); + ipv4_update_flow_stats(&IPV4_INGRESS_DST_1HOUR, &destination, event); + } +} + +pub fn ipv6_update_stats(event: &IPv6Event) { + unsafe { + let source = event.source_addr(); + let destination = event.destination_addr(); + ipv6_update_flow_status(&IPV6_INGRESS_SRC_1MIN, &source, event); + ipv6_update_flow_status(&IPV6_INGRESS_SRC_10MIN, &source, event); + ipv6_update_flow_status(&IPV6_INGRESS_SRC_1HOUR, &source, event); + ipv6_update_flow_status(&IPV6_INGRESS_DST_1MIN, &destination, event); + ipv6_update_flow_status(&IPV6_INGRESS_DST_10MIN, &destination, event); + ipv6_update_flow_status(&IPV6_INGRESS_DST_1HOUR, &destination, event); + } +} + +#[inline(always)] +unsafe fn ipv4_update_flow_stats(map: &LruHashMap, key: &AddrPortV4, event: &IPv4Event) { + unsafe { + if let Some(status) = map.get_ptr_mut(key) { + (*status).bytes += event.packet_length as u64; + (*status).packets += 1; + (*status).last_seen = event.timestamp_us; + } else { + let new_stats = FlowStats { + bytes: event.packet_length as u64, + packets: 1, + last_seen: event.timestamp_us, + }; + let _ = map.insert(key, &new_stats, 0); + } + } +} + +#[inline(always)] +unsafe fn ipv6_update_flow_status(map: &LruHashMap, key: &AddrPortV6, event: &IPv6Event) { + unsafe { + if let Some(status) = map.get_ptr_mut(key) { + (*status).bytes += event.packet_length as u64; + (*status).packets += 1; + (*status).last_seen = event.timestamp_us; + } else { + let new_stats = FlowStats { + bytes: event.packet_length as u64, + packets: 1, + last_seen: event.timestamp_us, + }; + let _ = map.insert(key, &new_stats, 0); + } + } +} diff --git a/ingress-ebpf/src/lib.rs b/ingress-ebpf/src/lib.rs new file mode 100644 index 0000000..0c9ac1a --- /dev/null +++ b/ingress-ebpf/src/lib.rs @@ -0,0 +1 @@ +#![no_std] diff --git a/ingress-ebpf/src/main.rs b/ingress-ebpf/src/main.rs new file mode 100644 index 0000000..15675e3 --- /dev/null +++ b/ingress-ebpf/src/main.rs @@ -0,0 +1,164 @@ +#![no_std] +#![no_main] +mod action; + +use aya_ebpf::bindings::xdp_action; +use aya_ebpf::macros::{map, xdp}; +use aya_ebpf::maps::{PerCpuArray, ProgramArray, XskMap}; +use aya_ebpf::programs::XdpContext; +#[allow(unused_imports)] +use aya_log_ebpf::info; +use common::define::program_array::ingress::*; +use common::ebpf::parsing; +use common::model::event::Event; + +use crate::action::{access_control, service, statistics}; + +#[map] +static PROGRAM_ARRAY: ProgramArray = ProgramArray::with_max_entries(8, 0); +#[map] +static PARSED_PACKET: PerCpuArray = PerCpuArray::with_max_entries(1, 0); +#[map] +static INGRESS_XSKS_MAP: XskMap = XskMap::pinned(64, 0); + +#[xdp] +pub fn net_guardia(ctx: XdpContext) -> u32 { + unsafe { + let _ = packet_intake(&ctx); + let _ = PROGRAM_ARRAY.tail_call(&ctx, TRANSMISSION); + xdp_action::XDP_PASS + } +} + +#[inline(always)] +unsafe fn packet_intake(ctx: &XdpContext) -> Result { + unsafe { + let start = ctx.data(); + let end = ctx.data_end(); + let ptr = PARSED_PACKET.get_ptr_mut(0).ok_or(())?; + parsing::parse_packet(start, end, ptr)?; + let _ = PROGRAM_ARRAY.tail_call(ctx, ACCESS_CONTROL); + Err(()) + } +} + +#[xdp] +pub fn access_control(ctx: XdpContext) -> u32 { + unsafe { + match try_access_control(&ctx) { + Ok(action) => action, + Err(_) => { + let _ = PROGRAM_ARRAY.tail_call(&ctx, TRANSMISSION); + xdp_action::XDP_PASS + } + } + } +} + +#[inline(always)] +unsafe fn try_access_control(ctx: &XdpContext) -> Result { + unsafe { + let ptr = PARSED_PACKET.get_ptr(0).ok_or(())?; + let parsed_packet = &*ptr; + match parsed_packet { + Event::IPv4(event) => { + if access_control::ipv4_is_whitelisted(event) { + let _ = PROGRAM_ARRAY.tail_call(ctx, STATISTICS); + return Err(()); + } + if access_control::ipv4_is_blacklisted(event) { + return Ok(xdp_action::XDP_DROP); + } + } + Event::IPv6(event) => { + if access_control::ipv6_is_whitelisted(event) { + let _ = PROGRAM_ARRAY.tail_call(ctx, STATISTICS); + return Err(()); + } + if access_control::ipv6_is_blacklisted(event) { + return Ok(xdp_action::XDP_DROP); + } + } + } + let _ = PROGRAM_ARRAY.tail_call(ctx, SERVICE); + Err(()) + } +} + +#[xdp] +pub fn service(ctx: XdpContext) -> u32 { + unsafe { + match try_service(&ctx) { + Ok(action) => action, + Err(_) => { + let _ = PROGRAM_ARRAY.tail_call(&ctx, TRANSMISSION); + xdp_action::XDP_PASS + } + } + } +} + +#[inline(always)] +unsafe fn try_service(ctx: &XdpContext) -> Result { + unsafe { + let start = ctx.data(); + let end = ctx.data_end(); + let ptr = PARSED_PACKET.get_ptr(0).ok_or(())?; + let parsed_packet = &*ptr; + match parsed_packet { + Event::IPv4(event) => { + if service::ipv4_service_rule_violation(start, end, event) { + return Ok(xdp_action::XDP_DROP); + } + } + Event::IPv6(event) => { + if service::ipv6_service_rule_violation(start, end, event) { + return Ok(xdp_action::XDP_DROP); + } + } + } + let _ = PROGRAM_ARRAY.tail_call(ctx, STATISTICS); + Err(()) + } +} + +#[xdp] +pub fn statistics(ctx: XdpContext) -> u32 { + unsafe { + let _ = try_statistics(&ctx); + xdp_action::XDP_PASS + } +} + +#[inline(always)] +unsafe fn try_statistics(ctx: &XdpContext) -> Result { + unsafe { + let ptr = PARSED_PACKET.get_ptr(0).ok_or(())?; + let parsed_packet = &*ptr; + match parsed_packet { + Event::IPv4(event) => { + statistics::ipv4_update_stats(&event); + } + Event::IPv6(event) => { + statistics::ipv6_update_stats(&event); + } + } + let _ = PROGRAM_ARRAY.tail_call(ctx, TRANSMISSION); + Err(()) + } +} + +#[xdp] +pub fn transmission(ctx: XdpContext) -> u32 { + let queue_id = unsafe { (*ctx.ctx).rx_queue_index }; + match INGRESS_XSKS_MAP.redirect(queue_id, 0) { + Ok(action) => action, + Err(_) => xdp_action::XDP_PASS, + } +} + +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { core::hint::unreachable_unchecked() } +} diff --git a/macros/Cargo.toml b/macros/Cargo.toml new file mode 100644 index 0000000..5a60ab1 --- /dev/null +++ b/macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "macros" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", features = ["full"] } diff --git a/macros/src/error_enum.rs b/macros/src/error_enum.rs new file mode 100644 index 0000000..a491ec6 --- /dev/null +++ b/macros/src/error_enum.rs @@ -0,0 +1,220 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::spanned::Spanned; +use syn::{Attribute, Error, Expr, Ident, LitStr, Result, Token, Type}; + +pub struct ErrorVariant { + pub attributes: Vec, + pub error_msg: LitStr, + pub name: Ident, + pub fields: Vec<(Ident, Type)>, + pub level: Expr, +} + +impl ErrorVariant { + pub fn has_no_source(&self) -> bool { + self.attributes.iter().any(|attr| attr.path().is_ident("no_source")) + } + + pub fn should_generate_constructor(&self, force_no_source: bool) -> bool { + if force_no_source || self.has_no_source() { + !self.fields.is_empty() + } else { + true + } + } +} + +pub struct ErrorEnumInput { + pub enum_name: Ident, + pub variants: Vec, +} + +impl Parse for ErrorEnumInput { + fn parse(input: ParseStream) -> Result { + let enum_name = input.parse::()?; + + let content; + syn::braced!(content in input); + + let mut variants = Vec::new(); + + while !content.is_empty() { + let mut attributes = Vec::new(); + + while content.peek(Token![#]) { + attributes.push(content.call(Attribute::parse_outer)?); + } + + let attributes: Vec<_> = attributes.into_iter().flatten().collect(); + + let error_attr = attributes + .iter() + .find(|attr| attr.path().is_ident("error")) + .ok_or_else(|| Error::new(content.span(), "Missing #[error] attribute"))?; + + let error_msg = match &error_attr.meta { + syn::Meta::List(list) => syn::parse2::(list.tokens.clone())?, + _ => { + return Err(Error::new(error_attr.span(), "Invalid error attribute format")); + } + }; + + let name = content.parse::()?; + + let mut fields = Vec::new(); + if content.peek(syn::token::Brace) { + let fields_content; + syn::braced!(fields_content in content); + + while !fields_content.is_empty() { + let field_name = fields_content.parse::()?; + fields_content.parse::()?; + let field_type = fields_content.parse::()?; + fields.push((field_name, field_type)); + + if !fields_content.is_empty() { + fields_content.parse::()?; + } + } + } + + content.parse::]>()?; + let level = content.parse::()?; + + if !content.is_empty() { + content.parse::()?; + } + + variants.push(ErrorVariant { + attributes, + error_msg, + name, + fields, + level, + }); + } + + Ok(ErrorEnumInput { enum_name, variants }) + } +} + +pub fn generate_error_enum(input: TokenStream, force_no_source: bool) -> TokenStream { + let input = syn::parse_macro_input!(input as ErrorEnumInput); + + let enum_name = &input.enum_name; + let variants = &input.variants; + + let enum_variants = variants.iter().map(|variant| { + let name = &variant.name; + let error_msg = &variant.error_msg; + let fields = &variant.fields; + + let field_definitions = fields.iter().map(|(name, ty)| { + quote! { #name: #ty } + }); + + if force_no_source || variant.has_no_source() { + if variant.fields.is_empty() { + quote! { + #[error(#error_msg)] + #name + } + } else { + quote! { + #[error(#error_msg)] + #name { #(#field_definitions,)* } + } + } + } else { + quote! { + #[error(#error_msg)] + #name { + #(#field_definitions,)* + err: String + } + } + } + }); + + let level_match_arms = variants.iter().map(|variant| { + let name = &variant.name; + let level = &variant.level; + + if force_no_source || variant.has_no_source() { + if variant.fields.is_empty() { + quote! { + Self::#name => #level + } + } else { + quote! { + Self::#name { .. } => #level + } + } + } else { + quote! { + Self::#name { err: _, .. } => #level + } + } + }); + + let constructors = variants.iter().filter_map(|variant| { + if !variant.should_generate_constructor(force_no_source) { + return None; + } + + let name = &variant.name; + let fields = &variant.fields; + + let params = fields.iter().map(|(field_name, field_type)| { + quote! { #field_name: impl Into<#field_type> } + }); + + let field_assignments = fields.iter().map(|(field_name, _)| { + quote! { #field_name: #field_name.into() } + }); + + if force_no_source || variant.has_no_source() { + Some(quote! { + #[allow(non_snake_case)] + pub fn #name(#(#params),*) -> Self { + Self::#name { + #(#field_assignments,)* + } + } + }) + } else { + Some(quote! { + #[allow(non_snake_case)] + pub fn #name(#(#params,)* source: impl std::fmt::Display) -> Self { + Self::#name { + #(#field_assignments,)* + err: source.to_string() + } + } + }) + } + }); + + let expanded = quote! { + #[allow(dead_code)] + #[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize)] + pub enum #enum_name { + #(#enum_variants,)* + } + + impl #enum_name { + #[allow(dead_code)] + pub fn level(&self) -> tracing::Level { + match self { + #(#level_match_arms,)* + } + } + + #(#constructors)* + } + }; + + TokenStream::from(expanded) +} diff --git a/macros/src/lib.rs b/macros/src/lib.rs new file mode 100644 index 0000000..6dfd832 --- /dev/null +++ b/macros/src/lib.rs @@ -0,0 +1,21 @@ +mod error_enum; +mod log; +mod loggable; +mod traceable; + +use proc_macro::TokenStream; + +#[proc_macro] +pub fn log(input: TokenStream) -> TokenStream { + log::log_impl(input) +} + +#[proc_macro] +pub fn loggable(input: TokenStream) -> TokenStream { + loggable::loggable_impl(input) +} + +#[proc_macro] +pub fn traceable(input: TokenStream) -> TokenStream { + traceable::traceable_impl(input) +} diff --git a/macros/src/log.rs b/macros/src/log.rs new file mode 100644 index 0000000..ff56b3f --- /dev/null +++ b/macros/src/log.rs @@ -0,0 +1,66 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{parse_macro_input, Expr, Token}; + +struct LogInput { + error: Expr, + debug_info: Option, +} + +impl Parse for LogInput { + fn parse(input: ParseStream) -> syn::Result { + let error = input.parse::()?; + + let debug_info = if input.peek(Token![,]) { + input.parse::()?; + Some(input.parse::()?) + } else { + None + }; + + Ok(LogInput { error, debug_info }) + } +} + +pub fn log_impl(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as LogInput); + + let error_expr = &input.error; + + if let Some(debug_info) = &input.debug_info { + quote! { + { + let error = #error_expr; + let level = error.level(); + let message = error.to_string(); + let debug_info = #debug_info; + + match level { + tracing::Level::ERROR => tracing::error!(message = %message, debug = ?debug_info), + tracing::Level::WARN => tracing::warn!(message = %message, debug = ?debug_info), + tracing::Level::INFO => tracing::info!(message = %message, debug = ?debug_info), + tracing::Level::DEBUG => tracing::debug!(message = %message, debug = ?debug_info), + tracing::Level::TRACE => tracing::trace!(message = %message, debug = ?debug_info), + } + } + } + } else { + quote! { + { + let error = #error_expr; + let level = error.level(); + let message = error.to_string(); + + match level { + tracing::Level::ERROR => tracing::error!("{}", message), + tracing::Level::WARN => tracing::warn!("{}", message), + tracing::Level::INFO => tracing::info!("{}", message), + tracing::Level::DEBUG => tracing::debug!("{}", message), + tracing::Level::TRACE => tracing::trace!("{}", message), + } + } + } + } + .into() +} diff --git a/macros/src/loggable.rs b/macros/src/loggable.rs new file mode 100644 index 0000000..13c2eaa --- /dev/null +++ b/macros/src/loggable.rs @@ -0,0 +1,7 @@ +use proc_macro::TokenStream; + +use crate::error_enum; + +pub fn loggable_impl(input: TokenStream) -> TokenStream { + error_enum::generate_error_enum(input, true) +} diff --git a/macros/src/traceable.rs b/macros/src/traceable.rs new file mode 100644 index 0000000..5421384 --- /dev/null +++ b/macros/src/traceable.rs @@ -0,0 +1,7 @@ +use proc_macro::TokenStream; + +use crate::error_enum; + +pub fn traceable_impl(input: TokenStream) -> TokenStream { + error_enum::generate_error_enum(input, false) +} diff --git a/models/classifier.onnx b/models/classifier.onnx new file mode 100644 index 0000000..43b003c Binary files /dev/null and b/models/classifier.onnx differ diff --git a/models/deep_autoencoder.onnx b/models/deep_autoencoder.onnx new file mode 100644 index 0000000..b5ffc06 Binary files /dev/null and b/models/deep_autoencoder.onnx differ diff --git a/models/full_config.json b/models/full_config.json new file mode 100644 index 0000000..b590d5d --- /dev/null +++ b/models/full_config.json @@ -0,0 +1,289 @@ +{ + "created_at": "2026-03-05T14:04:08.736531", + "framework": "PyTorch", + "model": { + "deep_autoencoder": { + "file": "deep_autoencoder.onnx", + "input_dim": 27, + "encoding_dim": 16, + "ae_feature_names": [ + "flow_duration", + "fwd_packets", + "bwd_packets", + "fwd_bytes", + "bwd_bytes", + "flow_bytes_per_sec", + "flow_pkts_per_sec", + "fwd_win_bytes", + "bwd_win_bytes", + "fwd_pkt_len_mean", + "bwd_pkt_len_mean", + "fwd_iat_mean", + "bwd_iat_mean", + "flow_iat_mean", + "pkt_len_mean", + "dst_port", + "protocol", + "psh_flag_cnt", + "ack_flag_cnt", + "syn_flag_cnt", + "fin_flag_cnt", + "rst_flag_cnt", + "pkt_len_std", + "fwd_pkt_len_std", + "bwd_pkt_len_std", + "fwd_seg_size_min", + "fwd_act_data_pkts" + ], + "ae_threshold": 0.17166012525558472 + }, + "classifier": { + "file": "classifier.onnx", + "type": "ResNet MLP", + "n_features": 28, + "n_classes": 5, + "classifier_feature_names": [ + "flow_duration", + "fwd_packets", + "bwd_packets", + "fwd_bytes", + "bwd_bytes", + "flow_bytes_per_sec", + "flow_pkts_per_sec", + "fwd_win_bytes", + "bwd_win_bytes", + "fwd_pkt_len_mean", + "bwd_pkt_len_mean", + "fwd_iat_mean", + "bwd_iat_mean", + "flow_iat_mean", + "pkt_len_mean", + "dst_port", + "protocol", + "psh_flag_cnt", + "ack_flag_cnt", + "syn_flag_cnt", + "fin_flag_cnt", + "rst_flag_cnt", + "pkt_len_std", + "fwd_pkt_len_std", + "bwd_pkt_len_std", + "fwd_seg_size_min", + "fwd_act_data_pkts", + "ae_anomaly_score" + ] + } + }, + "preprocessing": { + "ae_clip_params": { + "flow_duration": { + "lower": 1.0, + "upper": 118485085.19000001 + }, + "fwd_packets": { + "lower": 0.0, + "upper": 54.0 + }, + "bwd_packets": { + "lower": 0.0, + "upper": 82.0 + }, + "fwd_bytes": { + "lower": 0.0, + "upper": 6960.0 + }, + "bwd_bytes": { + "lower": 0.0, + "upper": 89558.6950000003 + }, + "flow_bytes_per_sec": { + "lower": 0.0, + "upper": 12500000.0 + }, + "flow_pkts_per_sec": { + "lower": 0.0226829350855, + "upper": 2000000.0 + }, + "fwd_win_bytes": { + "lower": -1.0, + "upper": 65535.0 + }, + "bwd_win_bytes": { + "lower": -1.0, + "upper": 64000.0 + }, + "fwd_pkt_len_mean": { + "lower": 0.0, + "upper": 744.7272727272729 + }, + "bwd_pkt_len_mean": { + "lower": 0.0, + "upper": 1393.9601153594992 + }, + "fwd_iat_mean": { + "lower": 0.0, + "upper": 86366722.035 + }, + "bwd_iat_mean": { + "lower": 0.0, + "upper": 21700000.0 + }, + "flow_iat_mean": { + "lower": 1.0, + "upper": 86353400.64500001 + }, + "pkt_len_mean": { + "lower": 0.0, + "upper": 945.0294113974604 + }, + "dst_port": { + "lower": 0.0, + "upper": 64499.0 + }, + "protocol": { + "lower": 0.0, + "upper": 17.0 + }, + "psh_flag_cnt": { + "lower": 0.0, + "upper": 240.0 + }, + "ack_flag_cnt": { + "lower": 0.0, + "upper": 655.0 + }, + "syn_flag_cnt": { + "lower": 0.0, + "upper": 4.0 + }, + "fin_flag_cnt": { + "lower": 0.0, + "upper": 2.0 + }, + "rst_flag_cnt": { + "lower": 0.0, + "upper": 1.0 + }, + "pkt_len_std": { + "lower": 0.0, + "upper": 943.9221842600019 + }, + "fwd_pkt_len_std": { + "lower": 0.0, + "upper": 721.4199648708689 + }, + "bwd_pkt_len_std": { + "lower": 0.0, + "upper": 911.0272448420104 + }, + "fwd_seg_size_min": { + "lower": 0.0, + "upper": 40.0 + }, + "fwd_act_data_pkts": { + "lower": 0.0, + "upper": 58.0 + } + }, + "ae_scaler": { + "mean": [ + 9783470.902880548, + 3.6532605393470496, + 3.2219949045990472, + 311.35281195367173, + 1407.508354811673, + 298997.28705051675, + 32378.36735569276, + 6158.3256759444475, + 6125.023325143747, + 58.1683929498305, + 149.72168204849987, + 2991407.86700246, + 543337.1970583026, + 2727511.340181723, + 105.03829359866097, + 13463.473246959382, + 8.920585011866685, + 5.457814320208867, + 12.214631683524651, + 0.4885976379312313, + 0.246598391240437, + 0.08478201266637703, + 151.71809799252043, + 76.89649795414182, + 143.21014925828936, + 21.447338538177014, + 3.2607463735155715 + ], + "std": [ + 27895851.239715174, + 6.4263677035863696, + 8.326275195918974, + 739.2548064174216, + 7898.711708881367, + 1187804.3239195002, + 190280.1548511611, + 15556.664474188405, + 17012.235322987635, + 98.89458775902833, + 242.79522892109904, + 13146157.964503227, + 2242915.166631539, + 12878527.145353919, + 151.95316919115754, + 21264.915159327567, + 4.943209145195101, + 23.02750953458606, + 57.0633472601567, + 1.2662749497077417, + 0.4553262784517569, + 0.278557037237646, + 211.1373807979287, + 143.47790203327, + 228.24531573376805, + 9.323458649060111, + 7.228913366949205 + ], + "feature_names": [ + "flow_duration", + "fwd_packets", + "bwd_packets", + "fwd_bytes", + "bwd_bytes", + "flow_bytes_per_sec", + "flow_pkts_per_sec", + "fwd_win_bytes", + "bwd_win_bytes", + "fwd_pkt_len_mean", + "bwd_pkt_len_mean", + "fwd_iat_mean", + "bwd_iat_mean", + "flow_iat_mean", + "pkt_len_mean", + "dst_port", + "protocol", + "psh_flag_cnt", + "ack_flag_cnt", + "syn_flag_cnt", + "fin_flag_cnt", + "rst_flag_cnt", + "pkt_len_std", + "fwd_pkt_len_std", + "bwd_pkt_len_std", + "fwd_seg_size_min", + "fwd_act_data_pkts" + ] + }, + "post_scaling_clip": { + "min": -5.0, + "max": 5.0 + } + }, + "attack_labels": { + "0": "Brute Force", + "1": "DDoS", + "2": "DoS", + "3": "Exploitation", + "4": "Reconnaissance" + } +} \ No newline at end of file diff --git a/models/inference_config.json b/models/inference_config.json new file mode 100644 index 0000000..1d42207 --- /dev/null +++ b/models/inference_config.json @@ -0,0 +1,239 @@ +{ + "ae_feature_names": [ + "flow_duration", + "fwd_packets", + "bwd_packets", + "fwd_bytes", + "bwd_bytes", + "flow_bytes_per_sec", + "flow_pkts_per_sec", + "fwd_win_bytes", + "bwd_win_bytes", + "fwd_pkt_len_mean", + "bwd_pkt_len_mean", + "fwd_iat_mean", + "bwd_iat_mean", + "flow_iat_mean", + "pkt_len_mean", + "dst_port", + "protocol", + "psh_flag_cnt", + "ack_flag_cnt", + "syn_flag_cnt", + "fin_flag_cnt", + "rst_flag_cnt", + "pkt_len_std", + "fwd_pkt_len_std", + "bwd_pkt_len_std", + "fwd_seg_size_min", + "fwd_act_data_pkts" + ], + "ae_clip_params": { + "flow_duration": { + "lower": 1.0, + "upper": 118485085.19000001 + }, + "fwd_packets": { + "lower": 0.0, + "upper": 54.0 + }, + "bwd_packets": { + "lower": 0.0, + "upper": 82.0 + }, + "fwd_bytes": { + "lower": 0.0, + "upper": 6960.0 + }, + "bwd_bytes": { + "lower": 0.0, + "upper": 89558.6950000003 + }, + "flow_bytes_per_sec": { + "lower": 0.0, + "upper": 12500000.0 + }, + "flow_pkts_per_sec": { + "lower": 0.0226829350855, + "upper": 2000000.0 + }, + "fwd_win_bytes": { + "lower": -1.0, + "upper": 65535.0 + }, + "bwd_win_bytes": { + "lower": -1.0, + "upper": 64000.0 + }, + "fwd_pkt_len_mean": { + "lower": 0.0, + "upper": 744.7272727272729 + }, + "bwd_pkt_len_mean": { + "lower": 0.0, + "upper": 1393.9601153594992 + }, + "fwd_iat_mean": { + "lower": 0.0, + "upper": 86366722.035 + }, + "bwd_iat_mean": { + "lower": 0.0, + "upper": 21700000.0 + }, + "flow_iat_mean": { + "lower": 1.0, + "upper": 86353400.64500001 + }, + "pkt_len_mean": { + "lower": 0.0, + "upper": 945.0294113974604 + }, + "dst_port": { + "lower": 0.0, + "upper": 64499.0 + }, + "protocol": { + "lower": 0.0, + "upper": 17.0 + }, + "psh_flag_cnt": { + "lower": 0.0, + "upper": 240.0 + }, + "ack_flag_cnt": { + "lower": 0.0, + "upper": 655.0 + }, + "syn_flag_cnt": { + "lower": 0.0, + "upper": 4.0 + }, + "fin_flag_cnt": { + "lower": 0.0, + "upper": 2.0 + }, + "rst_flag_cnt": { + "lower": 0.0, + "upper": 1.0 + }, + "pkt_len_std": { + "lower": 0.0, + "upper": 943.9221842600019 + }, + "fwd_pkt_len_std": { + "lower": 0.0, + "upper": 721.4199648708689 + }, + "bwd_pkt_len_std": { + "lower": 0.0, + "upper": 911.0272448420104 + }, + "fwd_seg_size_min": { + "lower": 0.0, + "upper": 40.0 + }, + "fwd_act_data_pkts": { + "lower": 0.0, + "upper": 58.0 + } + }, + "ae_scaler_mean": [ + 9783470.902880548, + 3.6532605393470496, + 3.2219949045990472, + 311.35281195367173, + 1407.508354811673, + 298997.28705051675, + 32378.36735569276, + 6158.3256759444475, + 6125.023325143747, + 58.1683929498305, + 149.72168204849987, + 2991407.86700246, + 543337.1970583026, + 2727511.340181723, + 105.03829359866097, + 13463.473246959382, + 8.920585011866685, + 5.457814320208867, + 12.214631683524651, + 0.4885976379312313, + 0.246598391240437, + 0.08478201266637703, + 151.71809799252043, + 76.89649795414182, + 143.21014925828936, + 21.447338538177014, + 3.2607463735155715 + ], + "ae_scaler_std": [ + 27895851.239715174, + 6.4263677035863696, + 8.326275195918974, + 739.2548064174216, + 7898.711708881367, + 1187804.3239195002, + 190280.1548511611, + 15556.664474188405, + 17012.235322987635, + 98.89458775902833, + 242.79522892109904, + 13146157.964503227, + 2242915.166631539, + 12878527.145353919, + 151.95316919115754, + 21264.915159327567, + 4.943209145195101, + 23.02750953458606, + 57.0633472601567, + 1.2662749497077417, + 0.4553262784517569, + 0.278557037237646, + 211.1373807979287, + 143.47790203327, + 228.24531573376805, + 9.323458649060111, + 7.228913366949205 + ], + "ae_post_clip_min": -5.0, + "ae_post_clip_max": 5.0, + "ae_threshold": 0.17166012525558472, + "classifier_feature_names": [ + "flow_duration", + "fwd_packets", + "bwd_packets", + "fwd_bytes", + "bwd_bytes", + "flow_bytes_per_sec", + "flow_pkts_per_sec", + "fwd_win_bytes", + "bwd_win_bytes", + "fwd_pkt_len_mean", + "bwd_pkt_len_mean", + "fwd_iat_mean", + "bwd_iat_mean", + "flow_iat_mean", + "pkt_len_mean", + "dst_port", + "protocol", + "psh_flag_cnt", + "ack_flag_cnt", + "syn_flag_cnt", + "fin_flag_cnt", + "rst_flag_cnt", + "pkt_len_std", + "fwd_pkt_len_std", + "bwd_pkt_len_std", + "fwd_seg_size_min", + "fwd_act_data_pkts", + "ae_anomaly_score" + ], + "attack_labels": { + "0": "Brute Force", + "1": "DDoS", + "2": "DoS", + "3": "Exploitation", + "4": "Reconnaissance" + } +} \ No newline at end of file diff --git a/net-guardia-frontend/package-lock.json b/net-guardia-frontend/package-lock.json new file mode 100644 index 0000000..c93e81a --- /dev/null +++ b/net-guardia-frontend/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "net-guardia-frontend", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/net-guardia/Cargo.toml b/net-guardia/Cargo.toml new file mode 100644 index 0000000..1863c54 --- /dev/null +++ b/net-guardia/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "net-guardia" +version = "0.1.0" +edition = "2024" + +[dependencies] +common = { path = "../common", features = ["user"] } +macros = { path = "../macros" } + +actix = "0.13.5" +actix-cors = "0.7.1" +actix-web = "4.11.0" +actix-ws = "0.4.0" +aya = { workspace = true } +aya-log = { workspace = true } +network-types = { workspace = true } +crossbeam = "0.8.4" +futures-util = "0.3.30" +libc = { workspace = true } +mime_guess = "2.0.5" +parking_lot = "0.12.5" +rust-embed = "8.7.2" +serde = { workspace = true } +serde_json = "1.0.143" +sysinfo = "0.38.2" +thiserror = "2.0.3" +tokio = { version = "1.40.0", features = ["full", "macros"] } +tokio-tungstenite = "0.28.0" +toml = "1.0.3" +tracing = "0.1.41" +tracing-appender = "0.2.3" +tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } +url = "2.5.7" +xsk-rs = { workspace = true } +maxminddb = "0.27.1" +lru = "0.16.2" +futures = "0.3.31" +tract-onnx = "0.22.0" +#csv = "1.4.0" +#anyhow = "1.0.100" + +[build-dependencies] +cargo_metadata = { workspace = true } +dotenvy = "0.15.7" + +[[bin]] +name = "net-guardia" +path = "src/main.rs" diff --git a/net-guardia/build.rs b/net-guardia/build.rs new file mode 100644 index 0000000..57b14a8 --- /dev/null +++ b/net-guardia/build.rs @@ -0,0 +1,431 @@ +use std::env; +use std::fs; +use std::io::{BufRead as _, BufReader}; +use std::path::PathBuf; +use std::process::{Child, Command, Stdio}; +use std::time::SystemTime; + +use cargo_metadata::{Artifact, CompilerMessage, Message, Metadata, MetadataCommand, Package, Target, TargetKind}; + +fn main() { + build_ingress_ebpf(); + build_egress_ebpf(); + build_frontend(); +} + +fn build_ingress_ebpf() { + let Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap(); + let ebpf_package = packages + .into_iter() + .find(|Package { name, .. }| **name == "ingress-ebpf") + .unwrap(); + + let out_dir = env::var_os("OUT_DIR").unwrap(); + let out_dir = PathBuf::from(out_dir); + + let endian = env::var_os("CARGO_CFG_TARGET_ENDIAN").unwrap(); + let target = if endian == "big" { + "bpfeb" + } else if endian == "little" { + "bpfel" + } else { + panic!("unsupported endian={:?}", endian) + }; + + let build_ebpf = true; + if build_ebpf { + let arch = env::var_os("CARGO_CFG_TARGET_ARCH").unwrap(); + + let target = format!("{target}-unknown-none"); + + let Package { manifest_path, .. } = ebpf_package; + let ebpf_dir = manifest_path.parent().unwrap(); + + println!("cargo:rerun-if-changed={}", ebpf_dir.as_str()); + + let mut cmd = Command::new("cargo"); + cmd.args([ + "build", + "-Z", + "build-std=core", + "--bins", + "--message-format=json", + "--release", + "--target", + &target, + ]); + + cmd.env("CARGO_CFG_BPF_TARGET_ARCH", arch); + + for key in ["RUSTUP_TOOLCHAIN", "RUSTC", "RUSTC_WORKSPACE_WRAPPER"] { + cmd.env_remove(key); + } + cmd.current_dir(ebpf_dir); + + let ebpf_target_dir = out_dir.join("../ingress-ebpf"); + cmd.arg("--target-dir").arg(&ebpf_target_dir); + + let mut child = cmd + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap_or_else(|err| panic!("failed to spawn {cmd:?}: {err}")); + let Child { stdout, stderr, .. } = &mut child; + + let stderr = stderr.take().unwrap(); + let stderr = BufReader::new(stderr); + let stderr = std::thread::spawn(move || { + for line in stderr.lines() { + let line = line.unwrap(); + println!("{line}"); + } + }); + + let stdout = stdout.take().unwrap(); + let stdout = BufReader::new(stdout); + let mut executables = Vec::new(); + for message in Message::parse_stream(stdout) { + #[allow(clippy::collapsible_match)] + match message.expect("valid JSON") { + Message::CompilerArtifact(Artifact { + executable, + target: Target { name, .. }, + .. + }) => { + if let Some(executable) = executable { + executables.push((name, executable.into_std_path_buf())); + } + } + Message::CompilerMessage(CompilerMessage { message, .. }) => { + for line in message.rendered.unwrap_or_default().split('\n') { + println!("{line}"); + } + } + Message::TextLine(line) => { + println!("{line}"); + } + _ => {} + } + } + + let status = child + .wait() + .unwrap_or_else(|err| panic!("failed to wait for {cmd:?}: {err}")); + assert_eq!(status.code(), Some(0), "{cmd:?} failed: {status:?}"); + + stderr.join().map_err(std::panic::resume_unwind).unwrap(); + + for (name, binary) in executables { + let dst = out_dir.join(name); + let _: u64 = + fs::copy(&binary, &dst).unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}")); + } + } else { + let Package { targets, .. } = ebpf_package; + for Target { name, kind, .. } in targets { + if *kind != [TargetKind::Bin] { + continue; + } + let dst = out_dir.join(name); + fs::write(&dst, []).unwrap_or_else(|err| panic!("failed to create {dst:?}: {err}")); + } + } +} + +fn build_egress_ebpf() { + let Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap(); + let ebpf_package = packages + .into_iter() + .find(|Package { name, .. }| **name == "egress-ebpf") + .unwrap(); + + let out_dir = env::var_os("OUT_DIR").unwrap(); + let out_dir = PathBuf::from(out_dir); + + let endian = env::var_os("CARGO_CFG_TARGET_ENDIAN").unwrap(); + let target = if endian == "big" { + "bpfeb" + } else if endian == "little" { + "bpfel" + } else { + panic!("unsupported endian={:?}", endian) + }; + + let build_ebpf = true; + if build_ebpf { + let arch = env::var_os("CARGO_CFG_TARGET_ARCH").unwrap(); + + let target = format!("{target}-unknown-none"); + + let Package { manifest_path, .. } = ebpf_package; + let ebpf_dir = manifest_path.parent().unwrap(); + + println!("cargo:rerun-if-changed={}", ebpf_dir.as_str()); + + let mut cmd = Command::new("cargo"); + cmd.args([ + "build", + "-Z", + "build-std=core", + "--bins", + "--message-format=json", + "--release", + "--target", + &target, + ]); + + cmd.env("CARGO_CFG_BPF_TARGET_ARCH", arch); + cmd.env("CARGO_TERM_COLOR", "always"); + + for key in ["RUSTUP_TOOLCHAIN", "RUSTC", "RUSTC_WORKSPACE_WRAPPER"] { + cmd.env_remove(key); + } + cmd.current_dir(ebpf_dir); + + let ebpf_target_dir = out_dir.join("../egress-ebpf"); + cmd.arg("--target-dir").arg(&ebpf_target_dir); + + let mut child = cmd + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap_or_else(|err| panic!("failed to spawn {cmd:?}: {err}")); + let Child { stdout, stderr, .. } = &mut child; + + let stderr = stderr.take().unwrap(); + let stderr = BufReader::new(stderr); + let stderr = std::thread::spawn(move || { + for line in stderr.lines() { + let line = line.unwrap(); + println!("{line}"); + } + }); + + let stdout = stdout.take().unwrap(); + let stdout = BufReader::new(stdout); + let mut executables = Vec::new(); + for message in Message::parse_stream(stdout) { + #[allow(clippy::collapsible_match)] + match message.expect("valid JSON") { + Message::CompilerArtifact(Artifact { + executable, + target: Target { name, .. }, + .. + }) => { + if let Some(executable) = executable { + executables.push((name, executable.into_std_path_buf())); + } + } + Message::CompilerMessage(CompilerMessage { message, .. }) => { + for line in message.rendered.unwrap_or_default().split('\n') { + println!("{line}"); + } + } + Message::TextLine(line) => { + println!("{line}"); + } + _ => {} + } + } + + let status = child + .wait() + .unwrap_or_else(|err| panic!("failed to wait for {cmd:?}: {err}")); + assert_eq!(status.code(), Some(0), "{cmd:?} failed: {status:?}"); + + stderr.join().map_err(std::panic::resume_unwind).unwrap(); + + for (name, binary) in executables { + let dst = out_dir.join(name); + let _: u64 = + fs::copy(&binary, &dst).unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}")); + } + } else { + let Package { targets, .. } = ebpf_package; + for Target { name, kind, .. } in targets { + if *kind != [TargetKind::Bin] { + continue; + } + let dst = out_dir.join(name); + fs::write(&dst, []).unwrap_or_else(|err| panic!("failed to create {dst:?}: {err}")); + } + } +} + +fn build_frontend() { + let _ = dotenvy::dotenv(); + + // let Some(frontend_dir) = env::var_os("FRONTEND_DIR") else { + // panic!("FRONTEND_DIR environment variable is required but not set"); + // }; + + let project_root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let static_dir = project_root.join("static").join("web"); + + let project_name = project_root.file_name().unwrap().to_string_lossy(); + let frontend_dir = project_root + .parent() + .unwrap() + .join(format!("{}-frontend", project_name)); + + if !frontend_dir.exists() { + panic!("Frontend directory {:?} does not exist", frontend_dir); + } + + println!("cargo:rerun-if-changed={}", frontend_dir.join("src").display()); + println!("cargo:rerun-if-changed={}", frontend_dir.join("public").display()); + println!("cargo:rerun-if-changed={}", frontend_dir.join("package.json").display()); + println!( + "cargo:rerun-if-changed={}", + frontend_dir.join("package-lock.json").display() + ); + println!( + "cargo:rerun-if-changed={}", + frontend_dir.join("next.config.js").display() + ); + println!( + "cargo:rerun-if-changed={}", + frontend_dir.join("tailwind.config.js").display() + ); + println!( + "cargo:rerun-if-changed={}", + frontend_dir.join("postcss.config.js").display() + ); + println!( + "cargo:rerun-if-changed={}", + frontend_dir.join("tsconfig.json").display() + ); + + let out_dir = frontend_dir.join("out"); + let need_build = needs_frontend_rebuild(&frontend_dir, &out_dir, &static_dir); + if !need_build { + return; + } + + let mut cmd = Command::new("npm"); + cmd.arg("install") + .current_dir(&frontend_dir); + + let status = cmd + .status() + .unwrap_or_else(|err| panic!("failed to run npm install: {err}")); + if !status.success() { + panic!("npm install failed with exit code: {:?}", status.code()); + } + + let mut cmd = Command::new("npx"); + cmd.args(["next", "build"]) + .current_dir(&frontend_dir); + + let status = cmd + .status() + .unwrap_or_else(|err| panic!("failed to run next build: {err}")); + if !status.success() { + panic!("next build failed with exit code: {:?}", status.code()); + } + + if static_dir.exists() { + fs::remove_dir_all(&static_dir).unwrap_or_else(|err| panic!("failed to remove {:?}: {err}", static_dir)); + } + fs::create_dir_all(&static_dir).unwrap_or_else(|err| panic!("failed to create {:?}: {err}", static_dir)); + + copy_dir_all(&out_dir, &static_dir).unwrap_or_else(|err| panic!("failed to copy frontend build: {err}")); +} + +fn needs_frontend_rebuild(frontend_dir: &PathBuf, out_dir: &PathBuf, static_dir: &PathBuf) -> bool { + if !out_dir.exists() { + return true; + } + + if !static_dir.exists() { + return true; + } + + let out_modified = match fs::metadata(out_dir).and_then(|m| m.modified()) { + Ok(time) => time, + Err(_) => { + return true; + } + }; + + let static_modified = match fs::metadata(static_dir).and_then(|m| m.modified()) { + Ok(time) => time, + Err(_) => { + return true; + } + }; + + let essential_items = [ + "src", + "public", + "package.json", + "next.config.js", + "tailwind.config.js", + "postcss.config.js", + "tsconfig.json", + "package-lock.json", + ]; + + for item_name in essential_items { + let item_path = frontend_dir.join(item_name); + if !item_path.exists() { + continue; + } + + let item_modified = match get_dir_last_modified(&item_path) { + Some(time) => time, + None => continue, + }; + + if item_modified > out_modified { + return true; + } + } + + if out_modified > static_modified { + return true; + } + + false +} + +fn get_dir_last_modified(path: &PathBuf) -> Option { + if path.is_file() { + return fs::metadata(path).and_then(|m| m.modified()).ok(); + } + + if path.is_dir() { + let mut latest = fs::metadata(path).and_then(|m| m.modified()).ok()?; + + if let Ok(entries) = fs::read_dir(path) { + for entry in entries.flatten() { + if let Some(modified) = get_dir_last_modified(&entry.path()) { + if modified > latest { + latest = modified; + } + } + } + } + + return Some(latest); + } + + None +} + +fn copy_dir_all(src: &PathBuf, dst: &PathBuf) -> std::io::Result<()> { + for entry in fs::read_dir(src)? { + let entry = entry?; + let file_type = entry.file_type()?; + let src_path = entry.path(); + let dst_path = dst.join(entry.file_name()); + + if file_type.is_dir() { + fs::create_dir_all(&dst_path)?; + copy_dir_all(&src_path, &dst_path)?; + } else { + fs::copy(&src_path, &dst_path)?; + } + } + Ok(()) +} diff --git a/net-guardia/src/core/ebpf/access_control.rs b/net-guardia/src/core/ebpf/access_control.rs new file mode 100644 index 0000000..c485c0a --- /dev/null +++ b/net-guardia/src/core/ebpf/access_control.rs @@ -0,0 +1,206 @@ +use std::collections::HashMap; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; + +use aya::maps::{HashMap as AyaHashMap, MapData}; +use aya::{Ebpf, Pod}; +use common::define::setting::MAX_RULES_PORT; +use common::model::ip_address::{IPv4, IPv6, Port}; +use tokio::sync::RwLock; + +use crate::model::direction::FlowDirection; +use crate::model::error::ebpf::EbpfError; +use crate::model::error::Error; +use crate::model::ip_address::NativeConvert; +use crate::model::list_type::ListType; +use crate::utils::ip_address::convert_ports_to_vec; + +pub struct AccessControl { + ipv4_src_whitelist: RwLock>, + ipv4_src_blacklist: RwLock>, + ipv4_dst_whitelist: RwLock>, + ipv4_dst_blacklist: RwLock>, + ipv6_src_whitelist: RwLock>, + ipv6_src_blacklist: RwLock>, + ipv6_dst_whitelist: RwLock>, + ipv6_dst_blacklist: RwLock>, +} + +impl AccessControl { + pub fn new(ebpf: &mut Ebpf) -> Result { + let access_control = Self { + ipv4_src_whitelist: RwLock::new(MapWrapper::new(ebpf, "IPV4_SRC_WHITELIST")?), + ipv4_src_blacklist: RwLock::new(MapWrapper::new(ebpf, "IPV4_SRC_BLACKLIST")?), + ipv4_dst_whitelist: RwLock::new(MapWrapper::new(ebpf, "IPV4_DST_WHITELIST")?), + ipv4_dst_blacklist: RwLock::new(MapWrapper::new(ebpf, "IPV4_DST_BLACKLIST")?), + ipv6_src_whitelist: RwLock::new(MapWrapper::new(ebpf, "IPV6_SRC_WHITELIST")?), + ipv6_src_blacklist: RwLock::new(MapWrapper::new(ebpf, "IPV6_SRC_BLACKLIST")?), + ipv6_dst_whitelist: RwLock::new(MapWrapper::new(ebpf, "IPV6_DST_WHITELIST")?), + ipv6_dst_blacklist: RwLock::new(MapWrapper::new(ebpf, "IPV6_DST_BLACKLIST")?), + }; + Ok(access_control) + } + + pub async fn get_ipv4_list(&self, direction: FlowDirection, list_type: ListType) -> HashMap> { + let map_wrapper = match (direction, list_type) { + (FlowDirection::Source, ListType::White) => self.ipv4_src_whitelist.read().await, + (FlowDirection::Source, ListType::Black) => self.ipv4_src_blacklist.read().await, + (FlowDirection::Destination, ListType::White) => self.ipv4_dst_whitelist.read().await, + (FlowDirection::Destination, ListType::Black) => self.ipv4_dst_blacklist.read().await, + }; + map_wrapper.get_list() + } + + pub async fn get_ipv6_list(&self, direction: FlowDirection, list_type: ListType) -> HashMap> { + let map_wrapper = match (direction, list_type) { + (FlowDirection::Source, ListType::White) => self.ipv6_src_whitelist.read().await, + (FlowDirection::Source, ListType::Black) => self.ipv6_src_blacklist.read().await, + (FlowDirection::Destination, ListType::White) => self.ipv6_dst_whitelist.read().await, + (FlowDirection::Destination, ListType::Black) => self.ipv6_dst_blacklist.read().await, + }; + map_wrapper.get_list() + } + + pub async fn add_ipv4_list( + &self, + direction: FlowDirection, + list_type: ListType, + address: SocketAddrV4, + ) -> Result<(), Error> { + let ip: u32 = (*address.ip()).to_bits().to_be(); + let port = address.port(); + let mut map_wrapper = match (direction, list_type) { + (FlowDirection::Source, ListType::White) => self.ipv4_src_whitelist.write().await, + (FlowDirection::Source, ListType::Black) => self.ipv4_src_blacklist.write().await, + (FlowDirection::Destination, ListType::White) => self.ipv4_dst_whitelist.write().await, + (FlowDirection::Destination, ListType::Black) => self.ipv4_dst_blacklist.write().await, + }; + map_wrapper.add(ip, port) + } + + pub async fn add_ipv6_list( + &self, + direction: FlowDirection, + list_type: ListType, + address: SocketAddrV6, + ) -> Result<(), Error> { + let ip: u128 = (*address.ip()).to_bits().to_be(); + let port = address.port(); + let mut map_wrapper = match (direction, list_type) { + (FlowDirection::Source, ListType::White) => self.ipv6_src_whitelist.write().await, + (FlowDirection::Source, ListType::Black) => self.ipv6_src_blacklist.write().await, + (FlowDirection::Destination, ListType::White) => self.ipv6_dst_whitelist.write().await, + (FlowDirection::Destination, ListType::Black) => self.ipv6_dst_blacklist.write().await, + }; + map_wrapper.add(ip, port) + } + + pub async fn remove_ipv4_list( + &self, + direction: FlowDirection, + list_type: ListType, + address: SocketAddrV4, + ) -> Result<(), Error> { + let ip: u32 = (*address.ip()).to_bits().to_be(); + let port = address.port(); + let mut map_wrapper = match (direction, list_type) { + (FlowDirection::Source, ListType::White) => self.ipv4_src_whitelist.write().await, + (FlowDirection::Source, ListType::Black) => self.ipv4_src_blacklist.write().await, + (FlowDirection::Destination, ListType::White) => self.ipv4_dst_whitelist.write().await, + (FlowDirection::Destination, ListType::Black) => self.ipv4_dst_blacklist.write().await, + }; + map_wrapper.remove(ip, port) + } + + pub async fn remove_ipv6_list( + &self, + direction: FlowDirection, + list_type: ListType, + address: SocketAddrV6, + ) -> Result<(), Error> { + let ip: u128 = (*address.ip()).to_bits().to_be(); + let port = address.port(); + let mut map_wrapper = match (direction, list_type) { + (FlowDirection::Source, ListType::White) => self.ipv6_src_whitelist.write().await, + (FlowDirection::Source, ListType::Black) => self.ipv6_src_blacklist.write().await, + (FlowDirection::Destination, ListType::White) => self.ipv6_dst_whitelist.write().await, + (FlowDirection::Destination, ListType::Black) => self.ipv6_dst_blacklist.write().await, + }; + map_wrapper.remove(ip, port) + } +} + +struct MapWrapper { + map: AyaHashMap, +} + +impl MapWrapper { + fn new(ebpf: &mut Ebpf, map_name: &str) -> Result { + let map = ebpf.take_map(map_name).ok_or(EbpfError::MapNotFound)?; + let map = AyaHashMap::try_from(map).map_err(EbpfError::MapOperationError)?; + Ok(Self { map }) + } + + fn get_list(&self) -> HashMap> { + self.map + .iter() + .filter_map(Result::ok) + .map(|(key, value)| (key.into_native(), convert_ports_to_vec(value))) + .collect() + } + + fn add(&mut self, ip: T, port: Port) -> Result<(), Error> { + let mut new_ports = [0_u16; MAX_RULES_PORT]; + if port == 0 { + new_ports[0] = 0; + } else if let Ok(ports) = self.map.get(&ip, 0) { + if ports[0] == 0 { + return Ok(()); + } + let mut index = None; + for (i, &value) in ports.iter().enumerate() { + if value == port { + return Ok(()); + } + if index.is_none() && value == 0 { + index = Some(i); + } + } + if index.is_none() { + Err(EbpfError::RuleReachLimit)?; + } + new_ports.copy_from_slice(&ports); + new_ports[index.unwrap()] = port; + } else { + new_ports[0] = port; + } + self.map + .insert(ip, new_ports, 0) + .map_err(EbpfError::MapOperationError)?; + Ok(()) + } + + fn remove(&mut self, ip: T, port: Port) -> Result<(), Error> { + if let Ok(mut ports) = self.map.get(&ip, 0) { + if port == 0 { + self.map.remove(&ip).map_err(EbpfError::MapOperationError)?; + return Ok(()); + } + + if let Some(index) = ports.iter().position(|&x| x == port) { + for i in index..(MAX_RULES_PORT - 1) { + ports[i] = ports[i + 1]; + } + ports[MAX_RULES_PORT - 1] = 0; + + if ports[0] == 0 { + self.map.remove(&ip).map_err(EbpfError::MapOperationError)?; + } else { + self.map.insert(ip, ports, 0).map_err(EbpfError::MapOperationError)?; + } + } + Ok(()) + } else { + Err(EbpfError::IpDoesNotExist)? + } + } +} diff --git a/net-guardia/src/core/ebpf/mod.rs b/net-guardia/src/core/ebpf/mod.rs new file mode 100644 index 0000000..6f3066a --- /dev/null +++ b/net-guardia/src/core/ebpf/mod.rs @@ -0,0 +1,69 @@ +pub mod access_control; +pub mod service; +pub mod statistics; +pub mod xsk_manager; + +use std::sync::Arc; + +use aya::Ebpf; +use crossbeam::queue::SegQueue; +use macros::log; +use tokio::sync::oneshot; + +use crate::core::ebpf::access_control::AccessControl; +use crate::core::ebpf::service::Service; +use crate::core::ebpf::statistics::Statistics; +use crate::core::ebpf::xsk_manager::XskManager; +use crate::core::infrastructure::app_config::AppConfig; +use crate::model::error::system::SystemError; +use crate::model::error::Error; +use crate::ml::engine::Engine; + +pub struct EbpfServices { + pub xsk_manager: Arc, + pub access_control: Arc, + pub service: Arc, + pub statistics: Arc, + pub shutdowns: SegQueue>, +} + +impl EbpfServices { + pub fn new( + app_config: Arc, + ingress_ebpf: &mut Ebpf, + egress_ebpf: &mut Ebpf, + ) -> Result { + let xsk_manager = XskManager::new(app_config.clone(), ingress_ebpf, egress_ebpf)?; + let access_control = AccessControl::new(ingress_ebpf)?; + let service = Service::new(ingress_ebpf)?; + let statistics = Statistics::new(app_config.clone(), ingress_ebpf, egress_ebpf)?; + let ebpf_services = Self { + xsk_manager: Arc::new(xsk_manager), + access_control: Arc::new(access_control), + service: Arc::new(service), + statistics: Arc::new(statistics), + shutdowns: SegQueue::new(), + }; + Ok(ebpf_services) + } + + pub async fn run(self: Arc, ml_engine: Arc) -> Result<(), Error> { + let xsk_manager = self.xsk_manager.clone(); + let statistics = self.statistics.clone(); + + xsk_manager.run(Some(ml_engine), &self.shutdowns)?; + + let statistics_shutdown = statistics.run().await; + + self.shutdowns.push(statistics_shutdown); + Ok(()) + } + + pub fn terminate(self: Arc) { + while let Some(shutdown) = self.shutdowns.pop() { + if shutdown.send(()).is_err() { + log!(SystemError::ShutdownSignalFailed); + } + } + } +} \ No newline at end of file diff --git a/net-guardia/src/core/ebpf/service.rs b/net-guardia/src/core/ebpf/service.rs new file mode 100644 index 0000000..7afe782 --- /dev/null +++ b/net-guardia/src/core/ebpf/service.rs @@ -0,0 +1,332 @@ +use std::collections::HashMap; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; + +use aya::maps::{Array as AyaArray, HashMap as AyaHashMap, MapData}; +use aya::{Ebpf, Pod}; +use common::model::http_method::{HttpMethod, HttpMethodBitmap}; +use common::model::ip_address::{AddrPortV4, AddrPortV6, IPv4, IPv6}; +use common::model::placeholder::PlaceHolder; +use tokio::sync::RwLock; + +use crate::model::error::ebpf::EbpfError; +use crate::model::error::Error; +use crate::model::ip_address::NativeConvert; + +pub struct Service { + ipv4_http_service: RwLock>, + ipv6_http_service: RwLock>, + ssh_white_list_enable: RwLock, + ipv4_ssh_service: RwLock>, + ipv6_ssh_service: RwLock>, + ipv4_ssh_white_list: RwLock>, + ipv6_ssh_white_list: RwLock>, + ipv4_ssh_black_list: RwLock>, + ipv6_ssh_black_list: RwLock>, +} + +impl Service { + pub fn new(ebpf: &mut Ebpf) -> Result { + let service = Self { + ipv4_http_service: RwLock::new(HttpServiceWrapper::new(ebpf, "IPV4_HTTP_SERVICE")?), + ipv6_http_service: RwLock::new(HttpServiceWrapper::new(ebpf, "IPV6_HTTP_SERVICE")?), + ssh_white_list_enable: RwLock::new(WhiteListControl::new(ebpf, "SSH_WHITE_LIST_ENABLE")?), + ipv4_ssh_service: RwLock::new(SshServiceWrapper::new(ebpf, "IPV4_SSH_SERVICE")?), + ipv6_ssh_service: RwLock::new(SshServiceWrapper::new(ebpf, "IPV6_SSH_SERVICE")?), + ipv4_ssh_white_list: RwLock::new(SshListWrapper::new(ebpf, "IPV4_SSH_WHITE_LIST")?), + ipv6_ssh_white_list: RwLock::new(SshListWrapper::new(ebpf, "IPV6_SSH_WHITE_LIST")?), + ipv4_ssh_black_list: RwLock::new(SshListWrapper::new(ebpf, "IPV4_SSH_BLACK_LIST")?), + ipv6_ssh_black_list: RwLock::new(SshListWrapper::new(ebpf, "IPV6_SSH_BLACK_LIST")?), + }; + Ok(service) + } + + pub async fn get_ipv4_http_service(&self) -> HashMap> { + self.ipv4_http_service.read().await.get_http_method() + } + + pub async fn get_ipv6_http_service(&self) -> HashMap> { + self.ipv6_http_service.read().await.get_http_method() + } + + pub async fn add_ipv4_http_service( + &self, + address: SocketAddrV4, + http_method: Vec, + ) -> Result<(), Error> { + self.ipv4_http_service + .write() + .await + .add_http_service(address, http_method) + } + + pub async fn add_ipv6_http_service( + &self, + address: SocketAddrV6, + http_method: Vec, + ) -> Result<(), Error> { + self.ipv6_http_service + .write() + .await + .add_http_service(address, http_method) + } + + pub async fn remove_ipv4_http_service( + &self, + address: SocketAddrV4, + removed_http_method: Vec, + ) -> Result<(), Error> { + self.ipv4_http_service + .write() + .await + .remove_http_service(address, removed_http_method) + } + + pub async fn remove_ipv6_http_service( + &self, + address: SocketAddrV6, + removed_http_method: Vec, + ) -> Result<(), Error> { + self.ipv6_http_service + .write() + .await + .remove_http_service(address, removed_http_method) + } + + pub async fn is_ssh_white_list_enable(&self) -> bool { + self.ssh_white_list_enable.read().await.is_white_list_enable() + } + + pub async fn enable_ssh_white_list(&self) -> Result<(), Error> { + self.ssh_white_list_enable.write().await.enable_white_list() + } + + pub async fn disable_ssh_white_list(&self) -> Result<(), Error> { + self.ssh_white_list_enable.write().await.disable_white_list() + } + + pub async fn get_ipv4_ssh_service(&self) -> Vec { + self.ipv4_ssh_service.read().await.get_ssh_service() + } + + pub async fn get_ipv6_ssh_service(&self) -> Vec { + self.ipv6_ssh_service.read().await.get_ssh_service() + } + + pub async fn add_ipv4_ssh_service(&self, address: SocketAddrV4) -> Result<(), Error> { + self.ipv4_ssh_service.write().await.add_ssh_service(address) + } + + pub async fn add_ipv6_ssh_service(&self, address: SocketAddrV6) -> Result<(), Error> { + self.ipv6_ssh_service.write().await.add_ssh_service(address) + } + + pub async fn remove_ipv4_ssh_service(&self, address: SocketAddrV4) -> Result<(), Error> { + self.ipv4_ssh_service.write().await.remove_ssh_service(address) + } + + pub async fn remove_ipv6_ssh_service(&self, address: SocketAddrV6) -> Result<(), Error> { + self.ipv6_ssh_service.write().await.remove_ssh_service(address) + } + + pub async fn get_ipv4_ssh_white_list(&self) -> Vec { + self.ipv4_ssh_white_list.read().await.get_list() + } + + pub async fn get_ipv6_ssh_white_list(&self) -> Vec { + self.ipv6_ssh_white_list.read().await.get_list() + } + + pub async fn add_ipv4_ssh_white_list(&self, ip: Ipv4Addr) -> Result<(), Error> { + self.ipv4_ssh_white_list.write().await.add_list(ip) + } + + pub async fn add_ipv6_ssh_white_list(&self, ip: Ipv6Addr) -> Result<(), Error> { + self.ipv6_ssh_white_list.write().await.add_list(ip) + } + + pub async fn remove_ipv4_ssh_white_list(&self, ip: Ipv4Addr) -> Result<(), Error> { + self.ipv4_ssh_white_list.write().await.remove_list(ip) + } + + pub async fn remove_ipv6_ssh_white_list(&self, ip: Ipv6Addr) -> Result<(), Error> { + self.ipv6_ssh_white_list.write().await.remove_list(ip) + } + + pub async fn get_ipv4_ssh_black_list(&self) -> Vec { + self.ipv4_ssh_black_list.read().await.get_list() + } + + pub async fn get_ipv6_ssh_black_list(&self) -> Vec { + self.ipv6_ssh_black_list.read().await.get_list() + } + + pub async fn add_ipv4_ssh_black_list(&self, ip: Ipv4Addr) -> Result<(), Error> { + self.ipv4_ssh_black_list.write().await.add_list(ip) + } + + pub async fn add_ipv6_ssh_black_list(&self, ip: Ipv6Addr) -> Result<(), Error> { + self.ipv6_ssh_black_list.write().await.add_list(ip) + } + + pub async fn remove_ipv4_ssh_black_list(&self, ip: Ipv4Addr) -> Result<(), Error> { + self.ipv4_ssh_black_list.write().await.remove_list(ip) + } + + pub async fn remove_ipv6_ssh_black_list(&self, ip: Ipv6Addr) -> Result<(), Error> { + self.ipv6_ssh_black_list.write().await.remove_list(ip) + } +} + +struct WhiteListControl { + map: AyaArray, +} + +impl WhiteListControl { + fn new(ebpf: &mut Ebpf, map_name: &str) -> Result { + let map = ebpf.take_map(map_name).ok_or(EbpfError::MapNotFound)?; + let map = AyaArray::try_from(map).map_err(EbpfError::MapOperationError)?; + Ok(Self { map }) + } + + fn is_white_list_enable(&self) -> bool { + match self.map.get(&0, 0) { + Ok(status) => { + if status == 0 { + false + } else { + true + } + } + Err(_) => false, + } + } + + fn enable_white_list(&mut self) -> Result<(), Error> { + self.map.set(0, 1_u8, 0).map_err(EbpfError::MapOperationError)?; + Ok(()) + } + + fn disable_white_list(&mut self) -> Result<(), Error> { + self.map.set(0, 0_u8, 0).map_err(EbpfError::MapOperationError)?; + Ok(()) + } +} + +struct HttpServiceWrapper { + map: AyaHashMap, +} + +impl HttpServiceWrapper { + fn new(ebpf: &mut Ebpf, map_name: &str) -> Result { + let map = ebpf.take_map(map_name).ok_or(EbpfError::MapNotFound)?; + let map = AyaHashMap::try_from(map).map_err(EbpfError::MapOperationError)?; + Ok(Self { map }) + } + + fn get_http_method(&self) -> HashMap> { + self.map + .iter() + .filter_map(Result::ok) + .map(|(key, value)| { + let address = key.into_native(); + (address, HttpMethod::convert_from_bitmap(value)) + }) + .collect() + } + + fn add_http_service(&mut self, address: T::Native, http_method: Vec) -> Result<(), Error> { + let address = T::from_native(address); + let ebpf_method = HttpMethod::convert_to_bitmap(http_method); + self.map + .insert(address, ebpf_method, 0) + .map_err(|_| EbpfError::RuleReachLimit)?; + Ok(()) + } + + fn remove_http_service(&mut self, address: T::Native, removed_http_method: Vec) -> Result<(), Error> { + let address = T::from_native(address); + if let Ok(current_http_method) = self.map.get(&address, 0) { + let mut http_method = HttpMethod::convert_from_bitmap(current_http_method); + http_method.retain(|method| !removed_http_method.contains(method)); + if http_method.is_empty() { + self.map.remove(&address).map_err(EbpfError::MapOperationError)?; + } else { + let new_http_method = HttpMethod::convert_to_bitmap(http_method); + self.map + .insert(&address, new_http_method, 0) + .map_err(EbpfError::MapOperationError)?; + } + Ok(()) + } else { + Err(EbpfError::IpDoesNotExist)? + } + } +} + +struct SshServiceWrapper { + map: AyaHashMap, +} + +impl SshServiceWrapper { + fn new(ebpf: &mut Ebpf, map_name: &str) -> Result { + let map = ebpf.take_map(map_name).ok_or(EbpfError::MapNotFound)?; + let map = AyaHashMap::try_from(map).map_err(EbpfError::MapOperationError)?; + Ok(Self { map }) + } + + fn get_ssh_service(&self) -> Vec { + self.map + .keys() + .filter_map(Result::ok) + .map(|key| key.into_native()) + .collect() + } + + fn add_ssh_service(&mut self, address: T::Native) -> Result<(), Error> { + let address = T::from_native(address); + self.map + .insert(address, 0_u8, 0) + .map_err(|_| EbpfError::RuleReachLimit)?; + Ok(()) + } + + fn remove_ssh_service(&mut self, address: T::Native) -> Result<(), Error> { + let address = T::from_native(address); + self.map.remove(&address).map_err(|_| EbpfError::IpDoesNotExist)?; + Ok(()) + } +} + +struct SshListWrapper { + map: AyaHashMap, +} + +impl SshListWrapper { + fn new(ebpf: &mut Ebpf, map_name: &str) -> Result { + let map = ebpf.take_map(map_name).ok_or(EbpfError::MapNotFound)?; + let map = AyaHashMap::try_from(map).map_err(EbpfError::MapOperationError)?; + Ok(Self { map }) + } + + fn get_list(&self) -> Vec { + self.map + .keys() + .filter_map(Result::ok) + .map(|key| key.into_native()) + .collect() + } + + fn add_list(&mut self, address: T::Native) -> Result<(), Error> { + let address = T::from_native(address); + self.map + .insert(address, 0_u8, 0) + .map_err(|_| EbpfError::RuleReachLimit)?; + Ok(()) + } + + fn remove_list(&mut self, address: T::Native) -> Result<(), Error> { + let address = T::from_native(address); + self.map.remove(&address).map_err(|_| EbpfError::IpDoesNotExist)?; + Ok(()) + } +} diff --git a/net-guardia/src/core/ebpf/statistics.rs b/net-guardia/src/core/ebpf/statistics.rs new file mode 100644 index 0000000..f760df3 --- /dev/null +++ b/net-guardia/src/core/ebpf/statistics.rs @@ -0,0 +1,258 @@ +use std::collections::HashMap; +use std::net::{IpAddr, SocketAddrV4, SocketAddrV6}; +use std::sync::Arc; + +use aya::maps::{HashMap as AyaHashMap, MapData}; +use aya::{Ebpf, Pod}; +use common::model::flow_stats::FlowStats; +use common::model::ip_address::{AddrPortV4, AddrPortV6}; +use futures::future::join_all; +use macros::log; +use tokio::select; +use tokio::sync::{RwLock, oneshot}; +use tokio::time::{Duration, sleep}; + +use crate::core::infrastructure::app_config::AppConfig; +use crate::core::infrastructure::geoip::GeoIpService; +use crate::model::direction::{Direction, FlowDirection}; +use crate::model::error::Error; +use crate::model::error::ebpf::EbpfError; +use crate::model::error::misc::MiscError; +use crate::model::geo_stats::FlowStatsWithGeo; +use crate::model::ip_address::NativeConvert; +use crate::model::time_type::TimeType; +use crate::utils::boot_time::boot_time; + +pub struct Statistics { + app_config: Arc, + boot_time: u64, + geo_ip: Option>, + ipv4_maps: HashMap<(Direction, FlowDirection, TimeType), RwLock>>, + ipv6_maps: HashMap<(Direction, FlowDirection, TimeType), RwLock>>, +} + +impl Statistics { + const INGRESS_MAPS: [((Direction, FlowDirection, TimeType), (&'static str, &'static str)); 6] = [ + ( + (Direction::Ingress, FlowDirection::Source, TimeType::_1Min), + ("IPV4_INGRESS_SRC_1MIN", "IPV6_INGRESS_SRC_1MIN"), + ), + ( + (Direction::Ingress, FlowDirection::Source, TimeType::_10Min), + ("IPV4_INGRESS_SRC_10MIN", "IPV6_INGRESS_SRC_10MIN"), + ), + ( + (Direction::Ingress, FlowDirection::Source, TimeType::_1Hour), + ("IPV4_INGRESS_SRC_1HOUR", "IPV6_INGRESS_SRC_1HOUR"), + ), + ( + (Direction::Ingress, FlowDirection::Destination, TimeType::_1Min), + ("IPV4_INGRESS_DST_1MIN", "IPV6_INGRESS_DST_1MIN"), + ), + ( + (Direction::Ingress, FlowDirection::Destination, TimeType::_10Min), + ("IPV4_INGRESS_DST_10MIN", "IPV6_INGRESS_DST_10MIN"), + ), + ( + (Direction::Ingress, FlowDirection::Destination, TimeType::_1Hour), + ("IPV4_INGRESS_DST_1HOUR", "IPV6_INGRESS_DST_1HOUR"), + ), + ]; + + const EGRESS_MAPS: [((Direction, FlowDirection, TimeType), (&'static str, &'static str)); 6] = [ + ( + (Direction::Egress, FlowDirection::Source, TimeType::_1Min), + ("IPV4_EGRESS_SRC_1MIN", "IPV6_EGRESS_SRC_1MIN"), + ), + ( + (Direction::Egress, FlowDirection::Source, TimeType::_10Min), + ("IPV4_EGRESS_SRC_10MIN", "IPV6_EGRESS_SRC_10MIN"), + ), + ( + (Direction::Egress, FlowDirection::Source, TimeType::_1Hour), + ("IPV4_EGRESS_SRC_1HOUR", "IPV6_EGRESS_SRC_1HOUR"), + ), + ( + (Direction::Egress, FlowDirection::Destination, TimeType::_1Min), + ("IPV4_EGRESS_DST_1MIN", "IPV6_EGRESS_DST_1MIN"), + ), + ( + (Direction::Egress, FlowDirection::Destination, TimeType::_10Min), + ("IPV4_EGRESS_DST_10MIN", "IPV6_EGRESS_DST_10MIN"), + ), + ( + (Direction::Egress, FlowDirection::Destination, TimeType::_1Hour), + ("IPV4_EGRESS_DST_1HOUR", "IPV6_EGRESS_DST_1HOUR"), + ), + ]; + + pub fn new( + app_config: Arc, + ingress_ebpf: &mut Ebpf, + egress_ebpf: &mut Ebpf, + ) -> Result { + let boot_time = boot_time(); + let mut ipv4_maps = HashMap::new(); + let mut ipv6_maps = HashMap::new(); + let geo_ip = match GeoIpService::new(&app_config.geoip_db_name) { + Ok(service) => Some(Arc::new(service)), + Err(err) => { + log!(MiscError::InvalidGeoIPConfiguration(err)); + None + } + }; + for (key, (ipv4_name, ipv6_name)) in Self::INGRESS_MAPS { + ipv4_maps.insert(key, RwLock::new(FlowMap::new(ingress_ebpf, ipv4_name)?)); + ipv6_maps.insert(key, RwLock::new(FlowMap::new(ingress_ebpf, ipv6_name)?)); + } + for (key, (ipv4_name, ipv6_name)) in Self::EGRESS_MAPS { + ipv4_maps.insert(key, RwLock::new(FlowMap::new(egress_ebpf, ipv4_name)?)); + ipv6_maps.insert(key, RwLock::new(FlowMap::new(egress_ebpf, ipv6_name)?)); + } + let statistics = Statistics { + app_config, + boot_time, + geo_ip, + ipv4_maps, + ipv6_maps, + }; + Ok(statistics) + } + + pub async fn run(self: Arc) -> oneshot::Sender<()> { + let refresh_interval = self.app_config.refresh_interval; + let (sender, receiver) = oneshot::channel(); + tokio::spawn(async move { + let mut receiver = receiver; + loop { + select! { + biased; + _ = &mut receiver => break, + _ = sleep(Duration::from_secs(refresh_interval)) => { + self.cleanup_expired_flows().await; + }, + } + } + }); + sender + } + + pub async fn cleanup_expired_flows(self: &Arc) { + let boot_time = self.boot_time; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; + for ((_, _, time_type), map) in self.ipv4_maps.iter() { + map.write().await.cleanup(boot_time, now, time_type.duration()) + } + for ((_, _, time_type), map) in self.ipv6_maps.iter() { + map.write().await.cleanup(boot_time, now, time_type.duration()) + } + } + + pub async fn get_ipv4_flow_data( + &self, + direction: Direction, + flow_direction: FlowDirection, + time_type: TimeType, + ) -> HashMap { + let flow_data = self + .ipv4_maps + .get(&(direction, flow_direction, time_type)) + .unwrap() + .write() + .await + .get_map(); + + if let Some(ref geo_ip) = self.geo_ip { + let futures: Vec<_> = flow_data + .into_iter() + .map(|(addr, stats)| { + let geo_ip = geo_ip.clone(); + async move { + let ip = IpAddr::V4(*addr.ip()); + let geo = geo_ip.lookup(ip).await.ok().flatten(); + (addr, FlowStatsWithGeo { stats, geo }) + } + }) + .collect(); + join_all(futures).await.into_iter().collect() + } else { + flow_data + .into_iter() + .map(|(addr, stats)| (addr, FlowStatsWithGeo { stats, geo: None })) + .collect() + } + } + + pub async fn get_ipv6_flow_data( + &self, + direction: Direction, + flow_direction: FlowDirection, + time_type: TimeType, + ) -> HashMap { + let flow_data = self + .ipv6_maps + .get(&(direction, flow_direction, time_type)) + .unwrap() + .write() + .await + .get_map(); + + if let Some(ref geo_ip) = self.geo_ip { + let futures: Vec<_> = flow_data + .into_iter() + .map(|(addr, stats)| { + let geo_ip = geo_ip.clone(); + async move { + let ip = IpAddr::V6(*addr.ip()); + let geo = geo_ip.lookup(ip).await.ok().flatten(); + (addr, FlowStatsWithGeo { stats, geo }) + } + }) + .collect(); + join_all(futures).await.into_iter().collect() + } else { + flow_data + .into_iter() + .map(|(addr, stats)| (addr, FlowStatsWithGeo { stats, geo: None })) + .collect() + } + } +} + +struct FlowMap { + map: AyaHashMap, +} + +impl FlowMap { + fn new(ebpf: &mut Ebpf, map_name: &str) -> Result { + let map = ebpf.take_map(map_name).ok_or(EbpfError::MapNotFound)?; + let map = AyaHashMap::try_from(map).map_err(EbpfError::MapOperationError)?; + Ok(Self { map }) + } + + fn get_map(&self) -> HashMap { + self.map + .iter() + .filter_map(Result::ok) + .map(|(key, value)| (key.into_native(), FlowStats::from(value))) + .collect() + } + + fn cleanup(&mut self, boot_time: u64, now: u64, window: u64) { + let expired_keys: Vec = self + .map + .iter() + .filter_map(|result| { + result + .ok() + .and_then(|(key, stats)| (now - stats.last_seen - boot_time > window).then_some(key)) + }) + .collect(); + expired_keys.iter().for_each(|key| { + let _ = self.map.remove(key); + }); + } +} diff --git a/net-guardia/src/core/ebpf/xsk_manager.rs b/net-guardia/src/core/ebpf/xsk_manager.rs new file mode 100644 index 0000000..1b629b6 --- /dev/null +++ b/net-guardia/src/core/ebpf/xsk_manager.rs @@ -0,0 +1,368 @@ +use std::ffi::CString; +use std::io::Write; +use std::num::NonZero; +use std::os::fd::AsRawFd; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +use aya::maps::{MapData, XskMap}; +use aya::Ebpf; +use crossbeam::channel::{bounded, Receiver, Sender}; +use crossbeam::queue::SegQueue; +use macros::log; +use parking_lot::Mutex; +use tokio::sync::oneshot; +use xsk_rs::config::{BindFlags, FrameSize, Interface, LibxdpFlags, QueueSize, SocketConfig, UmemConfig}; +use xsk_rs::{CompQueue, FillQueue, FrameDesc, RxQueue, Socket, TxQueue, Umem}; + +use crate::core::infrastructure::app_config::AppConfig; +use crate::ml::engine::{Engine, PacketProcessor}; +use crate::model::config::Config; +use crate::model::direction::Direction; +use crate::model::error::ebpf::EbpfError; +use crate::model::error::system::SystemError; +use crate::model::error::Error; +use crate::model::log::ebpf::EbpfLog; + +pub struct XskManager { + app_config: Arc, + xsk_map: Mutex>, + egress_xsk_map: Mutex>, +} + +impl XskManager { + pub fn new(app_config: Arc, ingress_ebpf: &mut Ebpf, egress_ebpf: &mut Ebpf) -> Result { + let map = ingress_ebpf + .take_map("INGRESS_XSKS_MAP") + .ok_or(EbpfError::MapNotFound)?; + let xsk_map = XskMap::try_from(map).map_err(EbpfError::MapOperationError)?; + + let egress_map = egress_ebpf.take_map("EGRESS_XSKS_MAP").ok_or(EbpfError::MapNotFound)?; + let egress_xsk_map = XskMap::try_from(egress_map).map_err(EbpfError::MapOperationError)?; + + Ok(Self { + app_config, + xsk_map: Mutex::new(xsk_map), + egress_xsk_map: Mutex::new(egress_xsk_map), + }) + } + + pub fn run(&self, ml_engine: Option>, shutdowns: &SegQueue>) -> Result<(), Error> { + let config = self.app_config.config.clone(); + let combined_queue_count = config.combined_queue_count; + + let packet_processor = ml_engine.map(|engine| Arc::new(PacketProcessor::new(engine))); + + for queue_id in 0..combined_queue_count { + let (ingress_to_egress_tx, ingress_to_egress_rx) = bounded(config.channel_size); + let (egress_to_ingress_tx, egress_to_ingress_rx) = bounded(config.channel_size); + + let ingress_xsk = XskPair::new( + config.clone(), + queue_id, + &config.ingress_ifname, + &config.egress_ifname, + Direction::Ingress, + packet_processor.clone(), + )?; + + let egress_xsk = XskPair::new( + config.clone(), + queue_id, + &config.egress_ifname, + &config.ingress_ifname, + Direction::Egress, + packet_processor.clone(), + )?; + + let mut xsk_map = self.xsk_map.lock(); + let ingress_fd = ingress_xsk.rx.fd().as_raw_fd(); + xsk_map + .set(queue_id, ingress_fd, 0) + .map_err(EbpfError::AfXdpSetFailed)?; + drop(xsk_map); + + let mut egress_xsk_map = self.egress_xsk_map.lock(); + let egress_fd = egress_xsk.rx.fd().as_raw_fd(); + egress_xsk_map + .set(queue_id, egress_fd, 0) + .map_err(EbpfError::AfXdpSetFailed)?; + drop(egress_xsk_map); + + let ingress_shutdown = ingress_xsk.run(ingress_to_egress_tx, egress_to_ingress_rx)?; + shutdowns.push(ingress_shutdown); + + let egress_shutdown = egress_xsk.run(egress_to_ingress_tx, ingress_to_egress_rx)?; + shutdowns.push(egress_shutdown); + + log!(EbpfLog::QueuePairStarted(queue_id)); + } + + Ok(()) + } +} + +pub struct XskPair { + direction: Direction, + umem: Arc, + fill_queue: FillQueue, + comp_queue: CompQueue, + tx: TxQueue, + rx: RxQueue, + frame_pool: Arc>>, // SegQueue + packet_processor: Option>, +} + +impl XskPair { + pub fn new( + config: Config, + queue_id: u32, + rx_ifname: &str, + tx_ifname: &str, + direction: Direction, + packet_processor: Option>, + ) -> Result { + let rx_ifname_c = CString::new(rx_ifname).map_err(|_| SystemError::UnknownError)?; + + let fill_queue_size = QueueSize::new(config.fill_queue_size).map_err(|_| SystemError::InvalidConfig)?; + let comp_queue_size = QueueSize::new(config.comp_queue_size).map_err(|_| SystemError::InvalidConfig)?; + let tx_queue_size = QueueSize::new(config.tx_queue_size).map_err(|_| SystemError::InvalidConfig)?; + let rx_queue_size = QueueSize::new(config.rx_queue_size).map_err(|_| SystemError::InvalidConfig)?; + let frame_size = FrameSize::new(config.frame_size).map_err(|_| SystemError::InvalidConfig)?; + let frame_count = NonZero::new(config.frame_count).ok_or(SystemError::InvalidConfig)?; + + let umem_config = UmemConfig::builder() + .fill_queue_size(fill_queue_size) + .comp_queue_size(comp_queue_size) + .frame_size(frame_size) + .frame_headroom(0) + .build() + .map_err(EbpfError::UmemSetFailed)?; + + let (umem, frame_descs) = Umem::new(umem_config, frame_count, false).map_err(EbpfError::UmemSetFailed)?; + + let socket_config = SocketConfig::builder() + .tx_queue_size(tx_queue_size) + .rx_queue_size(rx_queue_size) + .bind_flags(BindFlags::XDP_ZEROCOPY) + .libxdp_flags(LibxdpFlags::XSK_LIBXDP_FLAGS_INHIBIT_PROG_LOAD) + .build(); + + let interface = Interface::new(rx_ifname_c); + + let (tx, rx, queue) = + unsafe { Socket::new(socket_config, &umem, &interface, queue_id).map_err(EbpfError::SocketSetFailed)? }; + + let (mut fill_queue, comp_queue) = queue.ok_or(EbpfError::UnknownError)?; + + let total_frames = frame_descs.len(); + let fill_frames_count = (total_frames / 2).min(config.fill_queue_size as usize); + + let fill_frames: Vec = frame_descs.iter().take(fill_frames_count).copied().collect(); + + let submitted = unsafe { fill_queue.produce(&fill_frames) }; + if submitted != fill_frames.len() { + log!(EbpfLog::QueueInitIncomplete); + } + + let pool_frames: Vec = frame_descs.iter().skip(fill_frames_count).copied().collect(); + + let xsk_pair = Self { + direction, + umem: Arc::new(umem), + fill_queue, + comp_queue, + tx, + rx, + frame_pool: Arc::new(Mutex::new(pool_frames)), + packet_processor, + }; + + Ok(xsk_pair) + } + + pub fn run( + mut self, + forward_tx: Sender>, + forward_rx: Receiver>, + ) -> Result, EbpfError> { + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + + let thread_name = format!("xsk-{:?}", self.direction); + + thread::Builder::new() + .name(thread_name.clone()) + .spawn(move || { + let mut shutdown_rx = Some(shutdown_rx); + let mut idle_count: u32 = 0; + + loop { + if let Some(ref mut rx) = shutdown_rx { + match rx.try_recv() { + Ok(_) | Err(oneshot::error::TryRecvError::Closed) => { + break; + } + Err(oneshot::error::TryRecvError::Empty) => {} + } + } + + let mut total_activity = 0; + + match self.process_comp_queue() { + Ok(count) => total_activity += count, + Err(e) => log!(EbpfLog::CompQueueError(format!("{:?}", e))), + } + + match self.process_rx_queue(&forward_tx) { + Ok(count) => total_activity += count, + Err(e) => log!(EbpfLog::RXQueueError(format!("{:?}", e))), + } + + match self.process_tx_queue(&forward_rx) { + Ok(count) => total_activity += count, + Err(e) => log!(EbpfLog::TXQueueError(format!("{:?}", e))), + } + + if total_activity == 0 { + idle_count = idle_count.saturating_add(1); + } else { + idle_count = 0; + } + + let sleep_us = match idle_count { + 0..=10 => 1, + 11..=100 => 10, + _ => 100, + }; + + thread::sleep(Duration::from_micros(sleep_us)); + } + + log!(EbpfLog::XSKShutdown); + }) + .map(|_| shutdown_tx) + .map_err(|e| { + log!(EbpfLog::ThreadSpawnFailed(thread_name.clone(), e.to_string())); + EbpfError::ThreadSpawnFailed(e) + }) + } + + fn process_comp_queue(&mut self) -> Result { + let mut comp_descs = vec![FrameDesc::default(); 256]; + + let nb_completed = unsafe { self.comp_queue.consume(&mut comp_descs) }; + + if nb_completed > 0 { + let mut pool = self.frame_pool.lock(); + + for desc in comp_descs.iter().take(nb_completed) { + pool.push(*desc); + } + } + + Ok(nb_completed) + } + + fn process_rx_queue(&mut self, forward_tx: &Sender>) -> Result { + let mut rx_descs = vec![FrameDesc::default(); 64]; + let rx_count = unsafe { self.rx.consume(&mut rx_descs) }; + + if rx_count > 0 { + for rx_desc in rx_descs.iter().take(rx_count) { + let lengths = rx_desc.lengths(); + let packet_len = lengths.data() as usize; + + let data = unsafe { self.umem.data(rx_desc) }; + let packet_data = data.contents()[..packet_len].to_vec(); + + if let Some(ref processor) = self.packet_processor { + processor.process(&packet_data, self.direction == Direction::Ingress); + } + + if let Err(e) = forward_tx.try_send(packet_data) { + match e { + crossbeam::channel::TrySendError::Full(_) => { + log!(EbpfLog::ForwardChannelFull); + } + crossbeam::channel::TrySendError::Disconnected(_) => { + log!(EbpfLog::ForwardChannelDisconnected); + } + } + } + } + + unsafe { + let produced = self.fill_queue.produce(&rx_descs[..rx_count]); + if produced != rx_count { + log!(EbpfLog::FillQueueIncomplete(produced, rx_count)); + } + } + } + + Ok(rx_count) + } + + fn process_tx_queue(&mut self, forward_rx: &Receiver>) -> Result { + let mut packets_to_send = Vec::with_capacity(64); + while let Ok(packet) = forward_rx.try_recv() { + packets_to_send.push(packet); + if packets_to_send.len() >= 64 { + break; + } + } + + if packets_to_send.is_empty() { + return Ok(0); + } + + let _ = self.process_comp_queue(); + + let pool_size = { + let pool = self.frame_pool.lock(); + pool.len() + }; + + if pool_size == 0 { + log!(EbpfLog::FramePoolExhausted(packets_to_send.len())); + return Ok(0); + } + + let mut frames = Vec::with_capacity(packets_to_send.len()); + { + let mut pool = self.frame_pool.lock(); + let available = pool.len().min(packets_to_send.len()); + + for _ in 0..available { + if let Some(frame) = pool.pop() { + frames.push(frame); + } + } + } + + if frames.is_empty() { + log!(EbpfLog::NoFramesAvailable); + return Ok(0); + } + + for (frame, packet) in frames.iter_mut().zip(packets_to_send.iter()) { + unsafe { + self.umem + .data_mut(frame) + .cursor() + .write_all(packet) + .map_err(EbpfError::AfXdpSetFailed)?; + } + } + + let nb_submitted = unsafe { self.tx.produce(&frames) }; + + if let Err(e) = self.tx.wakeup() { + if e.kind() != std::io::ErrorKind::WouldBlock { + log!(EbpfLog::TXWakeupFailed(e.to_string())); + } + } + + Ok(nb_submitted) + } +} diff --git a/net-guardia/src/core/infrastructure/app_config.rs b/net-guardia/src/core/infrastructure/app_config.rs new file mode 100644 index 0000000..7f67ee0 --- /dev/null +++ b/net-guardia/src/core/infrastructure/app_config.rs @@ -0,0 +1,39 @@ +use std::fs; +use std::ops::Deref; + +use crate::model::config::{Config, ConfigTable}; +use crate::model::error::system::SystemError; +use crate::model::error::Error; + +pub struct AppConfig { + pub config: Config, +} + +impl AppConfig { + pub fn new() -> Result { + let toml_string = fs::read_to_string("./config.toml").map_err(SystemError::ConfigNotFound)?; + let config_table = toml::from_str::(&toml_string).map_err(|_| SystemError::InvalidConfig)?; + let config = config_table.config; + if !Self::validate(&config) { + Err(SystemError::InvalidConfig)? + } else { + Ok(Self { config }) + } + } + + fn validate(config: &Config) -> bool { + Self::validate_second(config.refresh_interval) + } + + fn validate_second(second: u64) -> bool { + second <= 3600 + } +} + +impl Deref for AppConfig { + type Target = Config; + + fn deref(&self) -> &Self::Target { + &self.config + } +} diff --git a/net-guardia/src/core/infrastructure/geoip.rs b/net-guardia/src/core/infrastructure/geoip.rs new file mode 100644 index 0000000..1810392 --- /dev/null +++ b/net-guardia/src/core/infrastructure/geoip.rs @@ -0,0 +1,112 @@ +use std::net::IpAddr; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use maxminddb::{geoip2, MaxMindDbError, Reader}; +use tokio::sync::RwLock; +use lru::LruCache; +use std::num::NonZeroUsize; +use tokio::task; + +use crate::model::geo_stats::GeoLocation; +use crate::utils::ip_address; + +pub struct GeoIpService { + reader: Arc>>, + cache: Arc>>>, +} + +impl GeoIpService { + pub fn new(db_name: &str) -> Result { + let db_path = PathBuf::from("net-guardia/static/geo").join(db_name); + Self::with_cache_size(db_path, 10000) + } + + pub fn with_cache_size>( + db_path: P, + cache_size: usize, + ) -> Result { + let reader = Reader::open_readfile(db_path)?; + let cache_capacity = NonZeroUsize::new(cache_size) + .unwrap_or_else(|| NonZeroUsize::new(10000).unwrap()); + + Ok(Self { + reader: Arc::new(reader), + cache: Arc::new(RwLock::new(LruCache::new(cache_capacity))), + }) + } + + pub async fn lookup(&self, ip: IpAddr) -> Result, MaxMindDbError> { + if ip_address::is_private_ip(&ip) { + return Ok(Some(GeoLocation { + country: Some("Local IP".into()), + country_code: Some("Local".into()), + city: None, + latitude: None, + longitude: None, + timezone: None, + })); + } + + { + let cache = self.cache.read().await; + if let Some(cached) = cache.peek(&ip) { + return Ok(cached.clone()); + } + } + + let reader = self.reader.clone(); + let result = task::spawn_blocking(move || { + Self::lookup_from_db_blocking(&reader, ip) + }) + .await + .map_err(|e| MaxMindDbError::InvalidDatabase { + message: format!("Task join error: {}", e), + offset: None, + })??; + + { + let mut cache = self.cache.write().await; + cache.put(ip, result.clone()); + } + + Ok(result) + } + + fn lookup_from_db_blocking( + reader: &Reader>, + ip: IpAddr, + ) -> Result, MaxMindDbError> { + let lookup_result = reader.lookup(ip)?; + let city_option: Option = lookup_result.decode()?; + + Ok(city_option.map(|city| { + let country_name = city.country.names.english + .map(|s| s.to_string()); + + let country_code = city.country.iso_code + .map(|s| s.to_string()); + + let city_name = city.city.names.english + .map(|s| s.to_string()); + + let latitude = city.location.latitude.or(Some(0.0)); + let longitude = city.location.longitude.or(Some(0.0)); + let timezone = city.location.time_zone.map(|s| s.to_string()); + + GeoLocation { + country: country_name, + country_code, + city: city_name, + latitude, + longitude, + timezone, + } + })) + } + + pub async fn cache_stats(&self) -> (usize, usize) { + let cache = self.cache.read().await; + (cache.len(), cache.cap().get()) + } +} \ No newline at end of file diff --git a/net-guardia/src/core/infrastructure/health.rs b/net-guardia/src/core/infrastructure/health.rs new file mode 100644 index 0000000..59e64eb --- /dev/null +++ b/net-guardia/src/core/infrastructure/health.rs @@ -0,0 +1,341 @@ +// net-guardia/src/core/ebpf/health.rs +use std::sync::Arc; +use std::time::Duration; + +use sysinfo::{Components, Networks, System}; +use tokio::sync::{broadcast, oneshot, RwLock}; +use tokio::time::interval; +use macros::log; + +use crate::core::infrastructure::app_config::AppConfig; +use crate::model::log::health::Health; +use crate::model::error::Error; +use crate::model::health::{ + ConfiguredNetworkStats, + CpuCoreInfo, + CpuDetails, + LoadAverage, + MemoryUsage, + NetworkStats, + SystemHealthMetrics, + SystemHealthStatus, + SystemInfo +}; + +pub struct SystemHealth { + system: RwLock, + networks: RwLock, + components: RwLock, + broadcast_tx: broadcast::Sender, + ingress_interface: String, + egress_interface: String, + // management_interface: String, +} + + +impl SystemHealth { + pub fn new(config: Arc) -> Result { + let (broadcast_tx, _) = broadcast::channel(100); + + let health = SystemHealth { + system: RwLock::new(System::new_all()), + networks: RwLock::new(Networks::new_with_refreshed_list()), + components: RwLock::new(Components::new_with_refreshed_list()), + broadcast_tx, + ingress_interface: config.ingress_ifname.clone(), + egress_interface: config.egress_ifname.clone(), + // management_interface: config.management_ifindex.clone(), + }; + + Ok(health) + } + + pub async fn run(self: Arc, monitoring_interval: Duration) -> oneshot::Sender<()> { + let (sender, mut receiver) = oneshot::channel(); + let health = self.clone(); + + tokio::spawn(async move { + let mut interval_timer = interval(monitoring_interval); + + loop { + tokio::select! { + biased; + _ = &mut receiver => { + break; + } + _ = interval_timer.tick() => { + health.refresh_and_broadcast().await; + } + } + } + }); + + sender + } + + async fn refresh_and_broadcast(&self) { + self.system.write().await.refresh_all(); + self.networks.write().await.refresh(true); + self.components.write().await.refresh(true); + + let system = self.system.read().await; + let networks = self.networks.read().await; + let components = self.components.read().await; + + let metrics = Self::collect_metrics( + &system, + &networks, + &components, + &self.ingress_interface, + &self.egress_interface, + // &self.management_interface, + ); + + drop(system); + drop(networks); + drop(components); + + if self.broadcast_tx.receiver_count() > 0 { + if let Err(e) = self.broadcast_tx.send(metrics) { + log!(Health::BroadcastFailed(e.to_string())); + } + } + } + + fn collect_metrics( + system: &System, + networks: &Networks, + components: &Components, + ingress_interface: &str, + egress_interface: &str, + // management_interface: &str, + ) -> SystemHealthMetrics { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + let boot_time = System::boot_time(); + let uptime_seconds = timestamp - boot_time; + + let system_info = Self::collect_system_info(system); + let cpu_details = Self::collect_cpu_details(system); + + let memory_usage = MemoryUsage { + total: system.total_memory(), + used: system.used_memory(), + available: system.available_memory(), + usage_percent: (system.used_memory() as f32 / system.total_memory() as f32) * 100.0, + swap_total: system.total_swap(), + swap_used: system.used_swap(), + }; + + let network_stats = Self::collect_configured_network_stats( + networks, + ingress_interface, + egress_interface, + // management_interface, + ); + + let load_average = System::load_average(); + let load_average = if load_average.one != 0.0 || load_average.five != 0.0 || load_average.fifteen != 0.0 { + Some(LoadAverage { + one_minute: load_average.one, + five_minute: load_average.five, + fifteen_minute: load_average.fifteen, + }) + } else { + None + }; + + let temperature = components + .iter() + .find(|component| { + let label = component.label().to_lowercase(); + label.contains("cpu") || label.contains("core") || label.contains("processor") + }) + .and_then(|component| component.temperature()); + + SystemHealthMetrics { + timestamp, + boot_time, + uptime_seconds, + system_info, + cpu_details, + memory_usage, + network_stats, + load_average, + temperature, + } + } + + fn collect_system_info(system: &System) -> SystemInfo { + SystemInfo { + kernel_version: System::kernel_version(), + os_name: System::name(), + os_version: System::os_version(), + architecture: std::env::consts::ARCH.to_string(), + total_processes: system.processes().len(), + } + } + + fn collect_cpu_details(system: &System) -> CpuDetails { + let cpus = system.cpus(); + + let cpu_usage = cpus.iter().map(|cpu| cpu.cpu_usage()).sum::() / cpus.len() as f32; + + let cores: Vec = cpus + .iter() + .enumerate() + .map(|(index, cpu)| CpuCoreInfo { + core_id: index, + usage_percent: cpu.cpu_usage(), + frequency: cpu.frequency(), + }) + .collect(); + + let cpu_brand = cpus + .first() + .map(|cpu| cpu.brand().to_string()) + .unwrap_or_else(|| "Unknown".to_string()); + + let avg_frequency = if !cores.is_empty() { + cores.iter().map(|core| core.frequency).sum::() / cores.len() as u64 + } else { + 0 + }; + + CpuDetails { + cpu_brand, + core_count: cores.len(), + cpu_usage, + cpu_frequency: avg_frequency, + cores, + } + } + + fn collect_configured_network_stats( + networks: &Networks, + ingress_interface: &str, + egress_interface: &str, + // management_interface: &str, + ) -> ConfiguredNetworkStats { + let create_network_stats = |interface_name: &str| -> Option { + networks.get(interface_name).map(|network| NetworkStats { + interface: interface_name.to_string(), + bytes_received: network.total_received(), + bytes_transmitted: network.total_transmitted(), + packets_received: network.total_packets_received(), + packets_transmitted: network.total_packets_transmitted(), + errors_received: network.total_errors_on_received(), + errors_transmitted: network.total_errors_on_transmitted(), + }) + }; + + let ingress = create_network_stats(ingress_interface); + let egress = create_network_stats(egress_interface); + // let management = create_network_stats(management_interface); + + if ingress.is_none() { + log!(Health::InterfaceNotFound("Ingress".to_string(), ingress_interface.to_string())); + } + if egress.is_none() { + log!(Health::InterfaceNotFound("Egress".to_string(), egress_interface.to_string())); + } + // if management.is_none() { + // warn!("Management interface '{}' not found", management_interface); + // } + + ConfiguredNetworkStats { + ingress, + egress, + // management, + } + } + + pub async fn get_current_metrics(&self) -> SystemHealthMetrics { + self.system.write().await.refresh_all(); + self.networks.write().await.refresh(true); + self.components.write().await.refresh(true); + + let system = self.system.read().await; + let networks = self.networks.read().await; + let components = self.components.read().await; + + Self::collect_metrics( + &system, + &networks, + &components, + &self.ingress_interface, + &self.egress_interface, + // &self.management_interface, + ) + } + + pub fn subscribe_to_metrics(&self) -> broadcast::Receiver { + self.broadcast_tx.subscribe() + } + + pub async fn is_system_healthy(&self) -> SystemHealthStatus { + let metrics = self.get_current_metrics().await; + + let mut status = SystemHealthStatus { + overall_healthy: true, + issues: Vec::new(), + warnings: Vec::new(), + }; + + if metrics.cpu_details.cpu_usage > 90.0 { + status.overall_healthy = false; + status + .issues + .push(format!("High CPU usage: {:.1}%", metrics.cpu_details.cpu_usage)); + } else if metrics.cpu_details.cpu_usage > 75.0 { + status + .warnings + .push(format!("Moderate CPU usage: {:.1}%", metrics.cpu_details.cpu_usage)); + } + + if metrics.memory_usage.usage_percent > 95.0 { + status.overall_healthy = false; + status.issues.push(format!( + "Critical memory usage: {:.1}%", + metrics.memory_usage.usage_percent + )); + } else if metrics.memory_usage.usage_percent > 80.0 { + status.warnings.push(format!( + "High memory usage: {:.1}%", + metrics.memory_usage.usage_percent + )); + } + + if let Some(temp) = metrics.temperature { + if temp > 80.0 { + status.overall_healthy = false; + status + .issues + .push(format!("High CPU temperature: {:.1}°C", temp)); + } else if temp > 70.0 { + status + .warnings + .push(format!("Elevated CPU temperature: {:.1}°C", temp)); + } + } + + if metrics.network_stats.ingress.is_none() { + status.overall_healthy = false; + status.issues.push("Ingress interface not available".to_string()); + } + if metrics.network_stats.egress.is_none() { + status.overall_healthy = false; + status.issues.push("Egress interface not available".to_string()); + } + // if metrics.network_stats.management.is_none() { + // status + // .warnings + // .push("Management interface not available".to_string()); + // } + + status + } +} \ No newline at end of file diff --git a/net-guardia/src/core/infrastructure/ml_alert.rs b/net-guardia/src/core/infrastructure/ml_alert.rs new file mode 100644 index 0000000..2cd058d --- /dev/null +++ b/net-guardia/src/core/infrastructure/ml_alert.rs @@ -0,0 +1,80 @@ +use serde::Serialize; +use tokio::sync::broadcast; +use tracing::error; + +use crate::model::ml_detection::DetectionResult; + +#[derive(Debug, Clone, Serialize)] +pub struct AlertMessage { + pub timestamp: u64, + pub flow_key: String, + pub src_ip: String, + pub dst_ip: String, + pub src_port: u16, + pub dst_port: u16, + pub protocol: u8, + pub is_attack: bool, + pub attack_type: Option, + pub confidence: f32, + pub ae_score: f32, +} + +impl AlertMessage { + pub fn from_detection_result(result: &DetectionResult) -> Self { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + Self { + timestamp, + flow_key: result.flow_key.clone(), + src_ip: result.flow_key_raw.src_ip.clone(), + dst_ip: result.flow_key_raw.dst_ip.clone(), + src_port: result.flow_key_raw.src_port, + dst_port: result.flow_key_raw.dst_port, + protocol: result.flow_key_raw.protocol, + is_attack: result.is_attack, + attack_type: result.attack_type.clone(), + confidence: result.confidence, + ae_score: result.ae_score, + } + } +} + +pub struct MLAlert { + broadcast_tx: broadcast::Sender, +} + +impl MLAlert { + pub fn new() -> Self { + let (broadcast_tx, _) = broadcast::channel(100); + + MLAlert { + broadcast_tx, + } + } + + pub fn subscribe_to_alerts(&self) -> broadcast::Receiver { + self.broadcast_tx.subscribe() + } + + pub fn broadcast_alert(&self, result: &DetectionResult) { + if self.broadcast_tx.receiver_count() > 0 { + let alert = AlertMessage::from_detection_result(result); + if let Err(e) = self.broadcast_tx.send(alert) { + error!("Failed to broadcast ML alert: {}", e); + } + } + } + + pub fn has_subscribers(&self) -> bool { + self.broadcast_tx.receiver_count() > 0 + } +} + +impl Default for MLAlert { + fn default() -> Self { + Self::new() + } +} \ No newline at end of file diff --git a/net-guardia/src/core/infrastructure/mod.rs b/net-guardia/src/core/infrastructure/mod.rs new file mode 100644 index 0000000..5f2b8c1 --- /dev/null +++ b/net-guardia/src/core/infrastructure/mod.rs @@ -0,0 +1,95 @@ +pub mod app_config; +pub mod health; +pub mod geoip; +pub mod ml_alert; + + +use std::sync::Arc; +use std::time::Duration; + +use crossbeam::queue::SegQueue; +use macros::log; +use tokio::sync::oneshot; + +use crate::core::infrastructure::app_config::AppConfig; +use crate::core::infrastructure::health::SystemHealth; +use crate::core::infrastructure::ml_alert::MLAlert; +use crate::ml::config_loader::InferenceConfig; +use crate::ml::engine::Engine; +use crate::ml::feature_extractor::FlowFeatures; +use crate::ml::model_loader::MLModels; +use crate::model::error::misc::MiscError; +use crate::model::error::system::SystemError; +use crate::model::error::Error; +use crate::model::log::system::SystemLog; +use crate::ml::traffic_logger::TrafficLogger; + +pub struct AppServices { + pub health: Arc, + pub ml_alert: Arc, + pub ml_models: Arc, + pub ml_engine: Arc, + shutdowns: SegQueue>, +} + +impl AppServices { + pub fn new(app_config: Arc, inference_config: Arc) -> Result { + let health = SystemHealth::new(app_config.clone())?; + + let ml_models = Arc::new(MLModels::load_models(&app_config, &inference_config)?); + let ml_alert = Arc::new(MLAlert::new()); + + let traffic_logger = if app_config.traffic_logging_mode { + let csv_path = app_config.traffic_log_csv_path.clone(); + let mut header = FlowFeatures::all_feature_names_owned(); + header.push("Label".to_string()); + let logger = TrafficLogger::new(&csv_path, header) + .map_err(|e| MiscError::TrafficLogCreateError(csv_path.clone(), e.to_string()))?; + log!(SystemLog::TrafficLoggingEnabled(csv_path)); + Some(Arc::new(logger)) + } else { + None + }; + + let ml_engine = Arc::new(Engine::new( + ml_models.clone(), + inference_config.clone(), + ml_alert.clone(), + app_config.max_concurrent_flows, + app_config.min_packets_for_inference, + app_config.inference_batch_size, + app_config.inference_interval_secs, + app_config.aggregator_window_secs, + traffic_logger, + )); + + Ok(Self { + health: Arc::new(health), + ml_alert, + ml_models, + ml_engine, + shutdowns: SegQueue::new(), + }) + } + + pub async fn run(&self) -> Result<(), Error> { + let health = self.health.clone(); + let ml_engine = self.ml_engine.clone(); + + let health_shutdown = health.run(Duration::from_secs(3)).await; + self.shutdowns.push(health_shutdown); + + let ml_shutdown = ml_engine.run().await; + self.shutdowns.push(ml_shutdown); + + Ok(()) + } + + pub fn terminate(&self) { + while let Some(shutdown) = self.shutdowns.pop() { + if shutdown.send(()).is_err() { + log!(SystemError::ShutdownSignalFailed); + } + } + } +} \ No newline at end of file diff --git a/net-guardia/src/core/mod.rs b/net-guardia/src/core/mod.rs new file mode 100644 index 0000000..18d067e --- /dev/null +++ b/net-guardia/src/core/mod.rs @@ -0,0 +1,3 @@ +pub mod ebpf; +pub mod infrastructure; +pub mod system; diff --git a/net-guardia/src/core/system.rs b/net-guardia/src/core/system.rs new file mode 100644 index 0000000..4e6d8fe --- /dev/null +++ b/net-guardia/src/core/system.rs @@ -0,0 +1,249 @@ +use std::sync::Arc; + +use actix_web::web::route; +use actix_web::{web, App, HttpServer}; +use aya::maps::{MapData, ProgramArray}; +use aya::programs::{Xdp, XdpFlags}; +use aya::Ebpf; +use aya_log::EbpfLogger; +use common::define::program_array::*; +use macros::log; + +use crate::core::ebpf::EbpfServices; +use crate::core::infrastructure::app_config::AppConfig; +use crate::core::infrastructure::AppServices; +use crate::ml::config_loader::InferenceConfig; +use crate::model::error::ebpf::EbpfError; +use crate::model::error::http::HttpError; +use crate::model::error::misc::MiscError; +use crate::model::error::Error; +use crate::model::log::ml::MLLog; +use crate::model::log::system::SystemLog; +use crate::utils::logging::Logging; +use crate::web::api::{control, default, health, misc, ml_alert}; + +pub struct System { + pub app_config: Arc, + pub inference_config: Arc, + pub ebpf_services: Arc, + pub app_services: Arc, + pub ingress_ebpf: Ebpf, + pub egress_ebpf: Ebpf, + #[allow(dead_code)] + ingress_program_array: ProgramArray, + #[allow(dead_code)] + egress_program_array: ProgramArray, +} + +impl System { + pub async fn new() -> Result { + let (mut ingress_ebpf, ingress_program_array) = System::get_ingress_ebpf()?; + let (mut egress_ebpf, egress_program_array) = System::get_egress_ebpf()?; + let app_config = Arc::new(AppConfig::new()?); + + let inference_config = Arc::new(InferenceConfig::load_file(&app_config.models_config_name)?); + + let ebpf_services = Arc::new(EbpfServices::new( + app_config.clone(), + &mut ingress_ebpf, + &mut egress_ebpf, + )?); + + let app_services = Arc::new(AppServices::new(app_config.clone(), inference_config.clone())?); + + let system = System { + app_config, + inference_config, + ebpf_services, + app_services, + ingress_ebpf, + egress_ebpf, + ingress_program_array, + egress_program_array, + }; + Ok(system) + } + + pub async fn run(&mut self) -> Result<(), Error> { + let ebpf_services = self.ebpf_services.clone(); + let app_services = self.app_services.clone(); + Logging::initialize()?; + log!(SystemLog::Initializing); + + log!(MLLog::ModelsLoaded( + self.app_services.ml_models.get_model_info("deep_autoencoder") + )); + log!(MLLog::ModelsLoaded( + self.app_services.ml_models.get_model_info("classifier") + )); + + log!(MLLog::ConfigLoaded { + features: self.inference_config.num_ae_features(), + attacks: self.inference_config.num_attack_types() + }); + + self.aya_log_init()?; + log!(SystemLog::InitializeComplete); + self.attach_ebpf()?; + + ebpf_services.run(app_services.ml_engine.clone()).await?; + app_services.run().await?; + self.run_http_server().await?; + Ok(()) + } + + pub async fn terminate(&self) -> Result<(), Error> { + let ebpf_services = self.ebpf_services.clone(); + let app_services = self.app_services.clone(); + log!(SystemLog::Terminating); + + ebpf_services.terminate(); + app_services.terminate(); + log!(SystemLog::TerminateComplete); + Ok(()) + } + + fn aya_log_init(&mut self) -> Result<(), Error> { + EbpfLogger::init(&mut self.ingress_ebpf).map_err(EbpfError::LoggerInitFailed)?; + EbpfLogger::init(&mut self.egress_ebpf).map_err(EbpfError::LoggerInitFailed)?; + Ok(()) + } + + fn attach_ebpf(&mut self) -> Result<(), Error> { + let config = self.app_config.config.clone(); + let ingress_ifname = config.ingress_ifname; + let egress_ifname = config.egress_ifname; + Self::set_memory_limit()?; + let ingress_xdp: &mut Xdp = self + .ingress_ebpf + .program_mut("net_guardia") + .ok_or(EbpfError::ProgramNotFound)? + .try_into() + .map_err(EbpfError::GetProgramFailed)?; + let egress_xdp: &mut Xdp = self + .egress_ebpf + .program_mut("net_guardia") + .ok_or(EbpfError::ProgramNotFound)? + .try_into() + .map_err(EbpfError::GetProgramFailed)?; + ingress_xdp.load().map_err(EbpfError::LoadProgramFailed)?; + ingress_xdp + .attach(&ingress_ifname, XdpFlags::DRV_MODE) + .map_err(EbpfError::AttachProgramFailed)?; + egress_xdp.load().map_err(EbpfError::LoadProgramFailed)?; + egress_xdp + .attach(&egress_ifname, XdpFlags::DRV_MODE) + .map_err(EbpfError::AttachProgramFailed)?; + Ok(()) + } + + async fn run_http_server(&self) -> Result<(), Error> { + let app_config = self.app_config.clone(); + let inference_config = self.inference_config.clone(); + let access_control = self.ebpf_services.access_control.clone(); + let service = self.ebpf_services.service.clone(); + let statistics = self.ebpf_services.statistics.clone(); + let health = self.app_services.health.clone(); + let ml_alert = self.app_services.ml_alert.clone(); + let port = self.app_config.http_server_bind_port; + HttpServer::new(move || { + let cors = actix_cors::Cors::default() + .allow_any_origin() + .allow_any_method() + .allow_any_header() + .max_age(3600); + App::new() + .wrap(cors) + .app_data(web::Data::from(app_config.clone())) + .app_data(web::Data::from(inference_config.clone())) + .app_data(web::Data::from(access_control.clone())) + .app_data(web::Data::from(service.clone())) + .app_data(web::Data::from(statistics.clone())) + .app_data(web::Data::from(health.clone())) + .app_data(web::Data::from(ml_alert.clone())) + .service(control::initialize()) + .service(ml_alert::initialize()) + .service(health::initialize()) + .service(misc::initialize()) + .default_service(route().to(default::default_route)) + }) + .bind(format!("0.0.0.0:{}", port)) + .map_err(HttpError::BindPortError)? + .run() + .await + .map_err(HttpError::ServerPanic)?; + Ok(()) + } + + fn get_ingress_ebpf() -> Result<(Ebpf, ProgramArray), Error> { + let mut ingress_ebpf = Ebpf::load(aya::include_bytes_aligned!(concat!( + env!("OUT_DIR"), + "/net-guardia-ingress" + ))) + .map_err(EbpfError::EbpfNotFound)?; + let program_array = ingress_ebpf.take_map("PROGRAM_ARRAY").ok_or(EbpfError::MapNotFound)?; + let mut program_array = ProgramArray::try_from(program_array).map_err(EbpfError::MapOperationError)?; + Self::load_program( + &mut ingress_ebpf, + &mut program_array, + "access_control", + ingress::ACCESS_CONTROL, + )?; + Self::load_program(&mut ingress_ebpf, &mut program_array, "service", ingress::SERVICE)?; + Self::load_program(&mut ingress_ebpf, &mut program_array, "statistics", ingress::STATISTICS)?; + Self::load_program( + &mut ingress_ebpf, + &mut program_array, + "transmission", + ingress::TRANSMISSION, + )?; + Ok((ingress_ebpf, program_array)) + } + + fn get_egress_ebpf() -> Result<(Ebpf, ProgramArray), Error> { + let mut egress_ebpf = Ebpf::load(aya::include_bytes_aligned!(concat!( + env!("OUT_DIR"), + "/net-guardia-egress" + ))) + .map_err(EbpfError::EbpfNotFound)?; + let program_array = egress_ebpf.take_map("PROGRAM_ARRAY").ok_or(EbpfError::MapNotFound)?; + let mut program_array = ProgramArray::try_from(program_array).map_err(EbpfError::MapOperationError)?; + Self::load_program(&mut egress_ebpf, &mut program_array, "statistics", egress::STATISTICS)?; + Self::load_program( + &mut egress_ebpf, + &mut program_array, + "transmission", + egress::TRANSMISSION, + )?; + Ok((egress_ebpf, program_array)) + } + + fn load_program( + ebpf: &mut Ebpf, + program_array: &mut ProgramArray, + function_name: &str, + index: u32, + ) -> Result<(), Error> { + let program: &mut Xdp = ebpf + .program_mut(function_name) + .ok_or(EbpfError::ProgramNotFound)? + .try_into() + .map_err(EbpfError::MapOperationError)?; + program.load().map_err(EbpfError::AttachProgramFailed)?; + let fd = program.fd().map_err(|_| EbpfError::UnknownError)?; + program_array.set(index, fd, 0).map_err(EbpfError::MapOperationError)?; + Ok(()) + } + + fn set_memory_limit() -> Result<(), Error> { + let rlim = libc::rlimit { + rlim_cur: libc::RLIM_INFINITY, + rlim_max: libc::RLIM_INFINITY, + }; + let ret = unsafe { libc::setrlimit(libc::RLIMIT_MEMLOCK, &rlim) }; + if ret != 0 { + Err(MiscError::RamLimitUnlockError(ret))? + } + Ok(()) + } +} diff --git a/net-guardia/src/main.rs b/net-guardia/src/main.rs new file mode 100644 index 0000000..d44d694 --- /dev/null +++ b/net-guardia/src/main.rs @@ -0,0 +1,16 @@ +mod core; +mod model; +mod utils; +mod web; +mod ml; + +use crate::core::system::System; +use crate::model::error::Error; + +#[actix_web::main] +async fn main() -> Result<(), Error> { + let mut system = System::new().await?; + system.run().await?; + system.terminate().await?; + Ok(()) +} diff --git a/net-guardia/src/ml/aggregator.rs b/net-guardia/src/ml/aggregator.rs new file mode 100644 index 0000000..172b4fb --- /dev/null +++ b/net-guardia/src/ml/aggregator.rs @@ -0,0 +1,51 @@ +use std::collections::HashMap; +use std::time::{Duration, Instant}; + +use crate::model::ml_detection::FlowKey; + +pub struct AttackAggregator { + detections: HashMap>, + window_duration: Duration, + min_detections: usize, + alert_threshold_multiplier: f32, +} + +impl AttackAggregator { + pub fn new(window_secs: u64, min_detections: usize) -> Self { + Self { + detections: HashMap::new(), + window_duration: Duration::from_secs(window_secs), + min_detections, + alert_threshold_multiplier: 1.2, + } + } + + pub fn should_alert(&mut self, flow_key: &FlowKey, score: f32, threshold: f32) -> bool { + let now = Instant::now(); + + let detections = self.detections.entry(flow_key.clone()).or_default(); + detections.retain(|(time, _)| now.duration_since(*time) < self.window_duration); + detections.push((now, score)); + + if detections.len() >= self.min_detections { + let avg_score: f32 = + detections.iter().map(|(_, s)| s).sum::() / detections.len() as f32; + + return avg_score > threshold * self.alert_threshold_multiplier; + } + + false + } + + pub fn cleanup(&mut self) { + let now = Instant::now(); + self.detections.retain(|_, detections| { + detections.retain(|(time, _)| now.duration_since(*time) < self.window_duration); + !detections.is_empty() + }); + } + + pub fn tracked_flows(&self) -> usize { + self.detections.len() + } +} \ No newline at end of file diff --git a/net-guardia/src/ml/config_loader.rs b/net-guardia/src/ml/config_loader.rs new file mode 100644 index 0000000..d0d161b --- /dev/null +++ b/net-guardia/src/ml/config_loader.rs @@ -0,0 +1,47 @@ +use std::collections::HashMap; +use std::fs; +use std::path::PathBuf; +use serde::{Deserialize, Serialize}; + +use crate::model::error::ml::MLError; +use crate::model::ml_detection::{AENormalization, ClipParams, PrecisionLevels}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InferenceConfig { + pub ae_feature_names: Vec, + pub ae_clip_params: HashMap, + pub ae_scaler_mean: Vec, + pub ae_scaler_std: Vec, + pub ae_post_clip_min: f64, + pub ae_post_clip_max: f64, + pub ae_threshold: f32, + pub classifier_feature_names: Vec, + pub attack_labels: HashMap, +} + +impl InferenceConfig { + pub fn load_file(file: &str) -> Result { + let path = PathBuf::from("models").join(file); + let content = fs::read_to_string(&path) + .map_err(|_| MLError::ConfigLoadFailed { path: path.to_path_buf() })?; + let config: InferenceConfig = serde_json::from_str(&content) + .map_err(|e| MLError::ConfigParseFailed { reason: e.to_string() })?; + Ok(config) + } + + pub fn num_ae_features(&self) -> usize { + self.ae_feature_names.len() + } + + pub fn num_classifier_features(&self) -> usize { + self.classifier_feature_names.len() + } + + pub fn num_attack_types(&self) -> usize { + self.attack_labels.len() + } + + pub fn get_attack_label(&self, id: usize) -> Option<&String> { + self.attack_labels.get(&id.to_string()) + } +} \ No newline at end of file diff --git a/net-guardia/src/ml/engine.rs b/net-guardia/src/ml/engine.rs new file mode 100644 index 0000000..0b67d21 --- /dev/null +++ b/net-guardia/src/ml/engine.rs @@ -0,0 +1,198 @@ +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use macros::log; +use tokio::sync::oneshot; +use tokio::time::interval; + +use super::aggregator::AttackAggregator; +use super::config_loader::InferenceConfig; +use super::feature_extractor::FlowFeatures; +use super::flow_tracker::FlowTracker; +use super::inference::Inference; +use super::model_loader::MLModels; +use super::traffic_logger::TrafficLogger; + +use crate::core::infrastructure::ml_alert::MLAlert; +use crate::model::log::ml::MLLog; +use crate::model::ml_detection::{EngineStats, InferenceStats}; +use crate::utils::packet_parser::parse_packet; + +pub struct Engine { + flow_tracker: Arc, + inference_pipeline: Arc, + aggregator: Arc>, + ml_alert: Arc, + min_packets: usize, + batch_size: usize, + inference_interval_secs: u64, + traffic_logger: Option>, +} + +impl Engine { + pub fn new( + models: Arc, + config: Arc, + ml_alert: Arc, + max_flows: usize, + min_packets: usize, + batch_size: usize, + interval_secs: u64, + window_secs: u64, + traffic_logger: Option>, + ) -> Self { + let flow_tracker = Arc::new(FlowTracker::new(max_flows)); + let inference_pipeline = Arc::new(Inference::new(models, config)); + + let min_detections = ((window_secs / interval_secs) / 2).max(1) as usize; + let aggregator = Arc::new(Mutex::new(AttackAggregator::new(window_secs, min_detections))); + + Self { + flow_tracker, + inference_pipeline, + aggregator, + ml_alert, + min_packets, + batch_size, + inference_interval_secs: interval_secs, + traffic_logger, + } + } + + pub async fn run(self: Arc) -> oneshot::Sender<()> { + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + tokio::spawn(async move { + self.run_inference_loop(shutdown_rx).await; + }); + shutdown_tx + } + + pub fn get_flow_tracker(&self) -> Arc { + self.flow_tracker.clone() + } + + async fn run_inference_loop(&self, mut shutdown_rx: oneshot::Receiver<()>) { + let mut ticker = interval(Duration::from_secs(self.inference_interval_secs)); + + loop { + tokio::select! { + _ = &mut shutdown_rx => break, + _ = ticker.tick() => {} + } + + let total_flows = self.flow_tracker.flow_count(); + let all_flows = self.flow_tracker.get_flows_snapshot(); + let packet_counts: Vec = all_flows.iter().map(|f| f.packet_count()).collect(); + + let flows = self.flow_tracker.get_flows_for_inference(self.min_packets); + + log!(MLLog::FlowStats( + total_flows, + flows.len(), + self.min_packets, + format!("{:?}", packet_counts) + )); + + if flows.is_empty() { + log!(MLLog::InferenceSkipped(format!( + "No flows with sufficient packets (total flows: {}, min packets: {})", + total_flows, self.min_packets + ))); + continue; + } + + if let Some(ref logger) = self.traffic_logger { + let feature_names = FlowFeatures::all_feature_names_owned(); + for flow in &flows { + let features = FlowFeatures::extract(flow, &feature_names); + logger.log_row(features.to_csv_record()); + } + self.flow_tracker.cleanup_old_flows(60_000_000); + continue; + } + + let batch = &flows[..flows.len().min(self.batch_size)]; + + log!(MLLog::RunningInference(batch.len())); + + let start = Instant::now(); + let results = self.inference_pipeline.infer_batch(batch); + let elapsed_us = start.elapsed().as_micros() as u64; + + let stats = InferenceStats::from_results(&results, elapsed_us); + + if results.len() != batch.len() { + log!(MLLog::InferenceResults(batch.len(), results.len())); + } + + log!(MLLog::InferenceCompleted( + stats.total_flows, + stats.malicious_flows, + stats.benign_flows, + (elapsed_us as f64 / 1000.0) as u32, + stats.flows_per_second + )); + + if let Ok(mut aggregator) = self.aggregator.lock() { + for result in &results { + if result.is_attack { + let should_alert = + aggregator.should_alert(&result.flow_key_raw, result.ae_score, result.threshold); + + if should_alert { + log!(MLLog::ThreatDetected( + format!("{:?}", result.direction), + result.flow_key.clone(), + result.attack_type.clone().unwrap_or_else(|| "UNKNOWN".to_string()), + result.confidence, + result.ae_score, + )); + + self.ml_alert.broadcast_alert(result); + } + } + } + + aggregator.cleanup(); + } + + self.flow_tracker.cleanup_old_flows(60_000_000); + } + } + + pub fn process_packet(&self, packet_data: &[u8], is_ingress: bool) { + match parse_packet(packet_data) { + Some((packet_info, payload_start)) => { + let payload = packet_data.get(payload_start..).unwrap_or(&[]); + self.flow_tracker.process_packet(packet_info, is_ingress, payload); + } + None => log!(MLLog::ParsePacketFailed(packet_data.len())), + } + } + + pub fn get_stats(&self) -> EngineStats { + EngineStats { + active_flows: self.flow_tracker.flow_count(), + } + } +} + +pub struct PacketProcessor { + ml_engine: Arc, +} + +impl PacketProcessor { + pub fn new(ml_engine: Arc) -> Self { + Self { ml_engine } + } + + pub fn process(&self, packet_data: &[u8], is_ingress: bool) { + self.ml_engine.process_packet(packet_data, is_ingress); + } + + pub fn process_batch(&self, packets: &[Vec], is_ingress: bool) { + for packet in packets { + self.process(packet, is_ingress); + } + } +} diff --git a/net-guardia/src/ml/feature_extractor.rs b/net-guardia/src/ml/feature_extractor.rs new file mode 100644 index 0000000..8727668 --- /dev/null +++ b/net-guardia/src/ml/feature_extractor.rs @@ -0,0 +1,359 @@ +use std::collections::HashMap; + +use super::flow_tracker::FlowData; +use crate::model::ml_detection::{ClipParams, PacketData}; + +#[derive(Debug, Clone)] +pub struct FlowFeatures { + pub features: Vec, + pub feature_num: usize, +} + +impl FlowFeatures { + pub fn extract(flow: &FlowData, feature_names: &[String]) -> Self { + let feature_num = feature_names.len(); + let mut features = Vec::with_capacity(feature_num); + + for name in feature_names { + let value = Self::get_feature_by_name(flow, name.trim()); + features.push(value); + } + + Self { features, feature_num } + } + + fn get_feature_by_name(flow: &FlowData, feature_name: &str) -> f64 { + let safe_div = |a: f64, b: f64| if b > 0.0 { a / b } else { 0.0 }; + + // 1-5 + let fwd_count = flow.fwd_packets.len() as f64; + let bwd_count = flow.bwd_packets.len() as f64; + let total_count = fwd_count + bwd_count; + + let duration_us = flow.duration_us() as f64; + let duration_s = duration_us / 1_000_000.0; + let duration_s = if duration_s > 0.0 { duration_s } else { 1e-6 }; + + // 6-9 + let fwd_lengths: Vec = flow.fwd_packets.iter().map(|p| p.length as f64).collect(); + let (fwd_max, fwd_min, fwd_mean, fwd_std) = compute_stats(&fwd_lengths); + + // 10-13 + let bwd_lengths: Vec = flow.bwd_packets.iter().map(|p| p.length as f64).collect(); + let (bwd_max, bwd_min, bwd_mean, bwd_std) = compute_stats(&bwd_lengths); + + // 14-15 + let total_bytes = (flow.fwd_total_bytes + flow.bwd_total_bytes) as f64; + + // 16-19 + let flow_iats = compute_flow_iats(&flow.fwd_packets, &flow.bwd_packets); + let (flow_iat_max, flow_iat_min, flow_iat_mean, flow_iat_std) = compute_stats(&flow_iats); + + // 20-24 + let fwd_iats = compute_iats(&flow.fwd_packets); + let fwd_iat_total: f64 = fwd_iats.iter().sum(); + let (fwd_iat_max, fwd_iat_min, fwd_iat_mean, fwd_iat_std) = compute_stats(&fwd_iats); + + // 25-29 + let bwd_iats = compute_iats(&flow.bwd_packets); + let bwd_iat_total: f64 = bwd_iats.iter().sum(); + let (bwd_iat_max, bwd_iat_min, bwd_iat_mean, bwd_iat_std) = compute_stats(&bwd_iats); + + // 30-37 + let fwd_psh = flow.fwd_packets.iter().filter(|p| p.flags.psh).count() as f64; + let bwd_psh = flow.bwd_packets.iter().filter(|p| p.flags.psh).count() as f64; + let fwd_urg = flow.fwd_packets.iter().filter(|p| p.flags.urg).count() as f64; + let bwd_urg = flow.bwd_packets.iter().filter(|p| p.flags.urg).count() as f64; + + // 38-55 + let all_lengths: Vec = flow + .fwd_packets + .iter() + .chain(flow.bwd_packets.iter()) + .map(|p| p.length as f64) + .collect(); + + let (max_len, min_len, mean_len, std_len) = compute_stats(&all_lengths); + + // 56-67 + let fwd_bulk = &flow.fwd_bulk_state; + let bwd_bulk = &flow.bwd_bulk_state; + + // 68-69 + let fwd_seg_sizes: Vec = flow + .fwd_packets + .iter() + .filter(|p| p.payload_length > 0) + .map(|p| p.header_length as f64) + .collect(); + + // 70-73 + let (active_max, active_min, active_mean, active_std) = + compute_stats(&flow.active_periods.iter().map(|&x| x as f64).collect::>()); + + // 74-77 + let (idle_max, idle_min, idle_mean, idle_std) = + compute_stats(&flow.idle_periods.iter().map(|&x| x as f64).collect::>()); + + match feature_name { + "Destination Port" | "Dst Port" | "dst_port" => flow.flow_key.dst_port as f64, + "Protocol" | "protocol" => flow.flow_key.protocol as f64, + "Flow Duration" | "flow_duration" => duration_us, + "Total Fwd Packets" | "Tot Fwd Pkts" | "fwd_packets" => fwd_count, + "Total Backward Packets" | "Tot Bwd Pkts" | "bwd_packets" => bwd_count, + "Total Length of Fwd Packets" | "TotLen Fwd Pkts" | "fwd_bytes" => flow.fwd_total_bytes as f64, + "Total Length of Bwd Packets" | "TotLen Bwd Pkts" | "bwd_bytes" => flow.bwd_total_bytes as f64, + "Fwd Packet Length Max" => fwd_max, + "Fwd Packet Length Min" => fwd_min, + "Fwd Packet Length Mean" | "Fwd Pkt Len Mean" | "fwd_pkt_len_mean" => fwd_mean, + "Fwd Packet Length Std" | "Fwd Pkt Len Std" | "fwd_pkt_len_std" => fwd_std, + "Bwd Packet Length Max" => bwd_max, + "Bwd Packet Length Min" => bwd_min, + "Bwd Packet Length Mean" | "Bwd Pkt Len Mean" | "bwd_pkt_len_mean" => bwd_mean, + "Bwd Packet Length Std" | "Bwd Pkt Len Std" | "bwd_pkt_len_std" => bwd_std, + "Flow Bytes/s" | "Flow Byts/s" | "flow_bytes_per_sec" => safe_div(total_bytes, duration_s), + "Flow Packets/s" | "Flow Pkts/s" | "flow_pkts_per_sec" => safe_div(total_count, duration_s), + "Flow IAT Mean" | "flow_iat_mean" => flow_iat_mean, + "Flow IAT Std" => flow_iat_std, + "Flow IAT Max" => flow_iat_max, + "Flow IAT Min" => flow_iat_min, + "Fwd IAT Total" => fwd_iat_total, + "Fwd IAT Mean" | "fwd_iat_mean" => fwd_iat_mean, + "Fwd IAT Std" => fwd_iat_std, + "Fwd IAT Max" => fwd_iat_max, + "Fwd IAT Min" => fwd_iat_min, + "Bwd IAT Total" => bwd_iat_total, + "Bwd IAT Mean" | "bwd_iat_mean" => bwd_iat_mean, + "Bwd IAT Std" => bwd_iat_std, + "Bwd IAT Max" => bwd_iat_max, + "Bwd IAT Min" => bwd_iat_min, + "Fwd PSH Flags" => fwd_psh, + "Bwd PSH Flags" => bwd_psh, + "Fwd URG Flags" => fwd_urg, + "Bwd URG Flags" => bwd_urg, + "Fwd Header Length" => flow.fwd_header_bytes as f64, + "Bwd Header Length" => flow.bwd_header_bytes as f64, + "Fwd Packets/s" => safe_div(fwd_count, duration_s), + "Bwd Packets/s" => safe_div(bwd_count, duration_s), + "Min Packet Length" => min_len, + "Max Packet Length" => max_len, + "Packet Length Mean" | "Pkt Len Mean" | "pkt_len_mean" => mean_len, + "Packet Length Std" | "Pkt Len Std" | "pkt_len_std" => std_len, + "Packet Length Variance" => std_len * std_len, + "FIN Flag Count" | "FIN Flag Cnt" | "fin_flag_cnt" => flow.fin_count as f64, + "SYN Flag Count" | "SYN Flag Cnt" | "syn_flag_cnt" => flow.syn_count as f64, + "RST Flag Count" | "RST Flag Cnt" | "rst_flag_cnt" => flow.rst_count as f64, + "PSH Flag Count" | "PSH Flag Cnt" | "psh_flag_cnt" => flow.psh_count as f64, + "ACK Flag Count" | "ACK Flag Cnt" | "ack_flag_cnt" => flow.ack_count as f64, + "URG Flag Count" => flow.urg_count as f64, + "CWE Flag Count" => flow.cwe_count as f64, + "ECE Flag Count" => flow.ece_count as f64, + "Down/Up Ratio" => safe_div(bwd_count, fwd_count), + "Average Packet Size" => safe_div(total_bytes, total_count), + "Avg Fwd Segment Size" => safe_div(flow.fwd_total_bytes as f64, fwd_count), + "Avg Bwd Segment Size" => safe_div(flow.bwd_total_bytes as f64, bwd_count), + "Fwd Header Length.1" => flow.fwd_header_bytes as f64, + "Fwd Avg Bytes/Bulk" => safe_div(fwd_bulk.total_bytes as f64, fwd_bulk.bulk_count as f64), + "Fwd Avg Packets/Bulk" => safe_div(fwd_bulk.total_packets as f64, fwd_bulk.bulk_count as f64), + "Fwd Avg Bulk Rate" => safe_div( + fwd_bulk.total_bytes as f64, + fwd_bulk.total_duration_us as f64 / 1_000_000.0, + ), + "Bwd Avg Bytes/Bulk" => safe_div(bwd_bulk.total_bytes as f64, bwd_bulk.bulk_count as f64), + "Bwd Avg Packets/Bulk" => safe_div(bwd_bulk.total_packets as f64, bwd_bulk.bulk_count as f64), + "Bwd Avg Bulk Rate" => safe_div( + bwd_bulk.total_bytes as f64, + bwd_bulk.total_duration_us as f64 / 1_000_000.0, + ), + "Subflow Fwd Packets" => fwd_count, + "Subflow Fwd Bytes" => flow.fwd_total_bytes as f64, + "Subflow Bwd Packets" => bwd_count, + "Subflow Bwd Bytes" => flow.bwd_total_bytes as f64, + "Init_Win_bytes_forward" | "Init Fwd Win Byts" | "fwd_win_bytes" => flow.init_win_bytes_fwd as f64, + "Init_Win_bytes_backward" | "Init Bwd Win Byts" | "bwd_win_bytes" => flow.init_win_bytes_bwd as f64, + "act_data_pkt_fwd" | "Fwd Act Data Pkts" | "fwd_act_data_pkts" => fwd_seg_sizes.len() as f64, + "min_seg_size_forward" | "Fwd Seg Size Min" | "fwd_seg_size_min" => fwd_seg_sizes + .iter() + .min_by(|a, b| a.total_cmp(b)) + .copied() + .unwrap_or(0.0), + "Active Mean" => active_mean, + "Active Std" => active_std, + "Active Max" => active_max, + "Active Min" => active_min, + "Idle Mean" => idle_mean, + "Idle Std" => idle_std, + "Idle Max" => idle_max, + "Idle Min" => idle_min, + + _ => 0.0, + } + } + + pub fn normalize(&mut self, means: &[f64], stds: &[f64]) { + for i in 0..self.feature_num { + if stds[i] > 0.0 { + self.features[i] = (self.features[i] - means[i]) / stds[i]; + } else { + self.features[i] = 0.0; + } + } + } + + pub fn clip(&mut self, clip_min: f64, clip_max: f64) { + for i in 0..self.feature_num { + self.features[i] = self.features[i].max(clip_min).min(clip_max); + } + } + + pub fn winsorize(&mut self, clip_params: &HashMap, feature_names: &[String]) { + for (i, feature_name) in feature_names.iter().enumerate() { + if i < self.feature_num { + if let Some(params) = clip_params.get(feature_name) { + self.features[i] = self.features[i].clamp(params.lower, params.upper); + } + } + } + } + + pub fn get_features_content(&self) -> &Vec { + &self.features + } + + pub fn all_feature_names() -> Vec<&'static str> { + vec![ + "Destination Port", + "Protocol", + "Flow Duration", + "Total Fwd Packets", + "Total Backward Packets", + "Total Length of Fwd Packets", + "Total Length of Bwd Packets", + "Fwd Packet Length Max", + "Fwd Packet Length Min", + "Fwd Packet Length Mean", + "Fwd Packet Length Std", + "Bwd Packet Length Max", + "Bwd Packet Length Min", + "Bwd Packet Length Mean", + "Bwd Packet Length Std", + "Flow Bytes/s", + "Flow Packets/s", + "Flow IAT Mean", + "Flow IAT Std", + "Flow IAT Max", + "Flow IAT Min", + "Fwd IAT Total", + "Fwd IAT Mean", + "Fwd IAT Std", + "Fwd IAT Max", + "Fwd IAT Min", + "Bwd IAT Total", + "Bwd IAT Mean", + "Bwd IAT Std", + "Bwd IAT Max", + "Bwd IAT Min", + "Fwd PSH Flags", + "Bwd PSH Flags", + "Fwd URG Flags", + "Bwd URG Flags", + "Fwd Header Length", + "Bwd Header Length", + "Fwd Packets/s", + "Bwd Packets/s", + "Min Packet Length", + "Max Packet Length", + "Packet Length Mean", + "Packet Length Std", + "Packet Length Variance", + "FIN Flag Count", + "SYN Flag Count", + "RST Flag Count", + "PSH Flag Count", + "ACK Flag Count", + "URG Flag Count", + "CWE Flag Count", + "ECE Flag Count", + "Down/Up Ratio", + "Average Packet Size", + "Avg Fwd Segment Size", + "Avg Bwd Segment Size", + "Fwd Header Length.1", + "Fwd Avg Bytes/Bulk", + "Fwd Avg Packets/Bulk", + "Fwd Avg Bulk Rate", + "Bwd Avg Bytes/Bulk", + "Bwd Avg Packets/Bulk", + "Bwd Avg Bulk Rate", + "Subflow Fwd Packets", + "Subflow Fwd Bytes", + "Subflow Bwd Packets", + "Subflow Bwd Bytes", + "Init_Win_bytes_forward", + "Init_Win_bytes_backward", + "act_data_pkt_fwd", + "min_seg_size_forward", + "Active Mean", + "Active Std", + "Active Max", + "Active Min", + "Idle Mean", + "Idle Std", + "Idle Max", + "Idle Min", + ] + } + + pub fn all_feature_names_owned() -> Vec { + Self::all_feature_names().iter().map(|s| s.to_string()).collect() + } + + pub fn to_csv_record(&self) -> Vec { + let mut record: Vec = self.features.iter().map(|f| f.to_string()).collect(); + record.push("BENIGN".to_string()); + record + } +} + +fn compute_stats(values: &[f64]) -> (f64, f64, f64, f64) { + if values.is_empty() { + return (0.0, 0.0, 0.0, 0.0); + } + + let n = values.len() as f64; + let sum: f64 = values.iter().sum(); + let mean = sum / n; + + let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max); + let min = values.iter().cloned().fold(f64::INFINITY, f64::min); + + let variance: f64 = values.iter().map(|&x| (x - mean).powi(2)).sum::() / n; + let std = variance.sqrt(); + + (max, min, mean, std) +} + +fn compute_iats(packets: &[PacketData]) -> Vec { + if packets.len() < 2 { + return vec![0.0]; + } + + packets + .windows(2) + .map(|w| (w[1].timestamp_us - w[0].timestamp_us) as f64) + .collect() +} + +fn compute_flow_iats(fwd_packets: &[PacketData], bwd_packets: &[PacketData]) -> Vec { + let mut all_packets: Vec<&PacketData> = fwd_packets.iter().chain(bwd_packets.iter()).collect(); + all_packets.sort_by_key(|p| p.timestamp_us); + + if all_packets.len() < 2 { + return vec![0.0]; + } + + all_packets + .windows(2) + .map(|w| (w[1].timestamp_us - w[0].timestamp_us) as f64) + .collect() +} diff --git a/net-guardia/src/ml/flow_tracker.rs b/net-guardia/src/ml/flow_tracker.rs new file mode 100644 index 0000000..1e76163 --- /dev/null +++ b/net-guardia/src/ml/flow_tracker.rs @@ -0,0 +1,352 @@ +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::time; + +use common::model::event::Event; + +use crate::model::direction::Direction; +use crate::model::ml_detection::{BulkState, FlowKey, PacketData}; + +#[derive(Debug, Clone)] +pub struct FlowData { + pub flow_key: FlowKey, + pub direction: Direction, + pub start_time_us: u64, + pub last_time_us: u64, + pub fwd_packets: Vec, + pub fwd_total_bytes: u64, + pub fwd_header_bytes: u64, + pub bwd_packets: Vec, + pub bwd_total_bytes: u64, + pub bwd_header_bytes: u64, + pub fin_count: u32, + pub syn_count: u32, + pub rst_count: u32, + pub psh_count: u32, + pub ack_count: u32, + pub urg_count: u32, + pub cwe_count: u32, + pub ece_count: u32, + pub init_win_bytes_fwd: u16, + pub init_win_bytes_bwd: u16, + pub active_periods: Vec, + pub idle_periods: Vec, + pub last_packet_time: u64, + pub fwd_bulk_state: BulkState, + pub bwd_bulk_state: BulkState, +} + +impl FlowData { + pub fn new(flow_key: FlowKey, first_packet: &Event, direction: Direction) -> Self { + Self { + flow_key, + direction, + start_time_us: first_packet.timestamp_us(), + last_time_us: first_packet.timestamp_us(), + fwd_packets: Vec::new(), + fwd_total_bytes: 0, + fwd_header_bytes: 0, + bwd_packets: Vec::new(), + bwd_total_bytes: 0, + bwd_header_bytes: 0, + fin_count: 0, + syn_count: 0, + rst_count: 0, + psh_count: 0, + ack_count: 0, + urg_count: 0, + cwe_count: 0, + ece_count: 0, + init_win_bytes_fwd: if first_packet.is_forward() { + first_packet.tcp_window_size() + } else { + 0 + }, + init_win_bytes_bwd: if !first_packet.is_forward() { + first_packet.tcp_window_size() + } else { + 0 + }, + active_periods: Vec::new(), + idle_periods: Vec::new(), + last_packet_time: first_packet.timestamp_us(), + fwd_bulk_state: BulkState::default(), + bwd_bulk_state: BulkState::default(), + } + } + + pub fn add_packet(&mut self, packet: &Event) { + let packet_data = PacketData { + timestamp_us: packet.timestamp_us(), + length: packet.packet_length(), + header_length: packet.header_length(), + payload_length: packet.payload_length(), + flags: packet.tcp_flags().clone(), + }; + + if packet.tcp_flags().fin { + self.fin_count += 1; + } + if packet.tcp_flags().syn { + self.syn_count += 1; + } + if packet.tcp_flags().rst { + self.rst_count += 1; + } + if packet.tcp_flags().psh { + self.psh_count += 1; + } + if packet.tcp_flags().ack { + self.ack_count += 1; + } + if packet.tcp_flags().urg { + self.urg_count += 1; + } + if packet.tcp_flags().cwr { + self.cwe_count += 1; + } + if packet.tcp_flags().ece { + self.ece_count += 1; + } + + let iat = packet.timestamp_us().saturating_sub(self.last_packet_time); + const IDLE_THRESHOLD_US: u64 = 1_000_000; + + if iat > IDLE_THRESHOLD_US { + self.idle_periods.push(iat); + } else if iat > 0 { + self.active_periods.push(iat); + } + + self.last_packet_time = packet.timestamp_us(); + self.last_time_us = packet.timestamp_us(); + + if packet.is_forward() { + self.fwd_packets.push(packet_data.clone()); + self.fwd_total_bytes += packet.packet_length() as u64; + self.fwd_header_bytes += packet.header_length() as u64; + + if self.init_win_bytes_fwd == 0 { + self.init_win_bytes_fwd = packet.tcp_window_size(); + } + + Self::update_bulk_state(&mut self.fwd_bulk_state, &packet_data); + } else { + self.bwd_packets.push(packet_data.clone()); + self.bwd_total_bytes += packet.packet_length() as u64; + self.bwd_header_bytes += packet.header_length() as u64; + + if self.init_win_bytes_bwd == 0 { + self.init_win_bytes_bwd = packet.tcp_window_size(); + } + + Self::update_bulk_state(&mut self.bwd_bulk_state, &packet_data); + } + } + + fn update_bulk_state(bulk_state: &mut BulkState, packet: &PacketData) { + const BULK_MIN_PACKETS: u64 = 4; + const BULK_MIN_BYTES: u64 = 1000; + + if packet.payload_length > 0 { + if !bulk_state.in_bulk { + bulk_state.in_bulk = true; + bulk_state.last_bulk_bytes = packet.length as u64; + bulk_state.last_bulk_packets = 1; + bulk_state.last_bulk_start_us = packet.timestamp_us; + bulk_state.last_bulk_packet_us = packet.timestamp_us; + } else { + bulk_state.last_bulk_bytes += packet.length as u64; + bulk_state.last_bulk_packets += 1; + bulk_state.last_bulk_packet_us = packet.timestamp_us; + } + } else { + if bulk_state.in_bulk + && bulk_state.last_bulk_packets >= BULK_MIN_PACKETS + && bulk_state.last_bulk_bytes >= BULK_MIN_BYTES + { + bulk_state.bulk_count += 1; + bulk_state.total_bytes += bulk_state.last_bulk_bytes; + bulk_state.total_packets += bulk_state.last_bulk_packets; + bulk_state.total_duration_us += bulk_state + .last_bulk_packet_us + .saturating_sub(bulk_state.last_bulk_start_us); + } + bulk_state.in_bulk = false; + bulk_state.last_bulk_bytes = 0; + bulk_state.last_bulk_packets = 0; + bulk_state.last_bulk_start_us = 0; + bulk_state.last_bulk_packet_us = 0; + } + } + + pub fn duration_us(&self) -> u64 { + self.last_time_us.saturating_sub(self.start_time_us) + } + + pub fn packet_count(&self) -> usize { + self.fwd_packets.len() + self.bwd_packets.len() + } +} + +pub struct FlowTracker { + flows: Arc>>, + max_flows: usize, +} + +impl FlowTracker { + pub fn new(max_flows: usize) -> Self { + Self { + flows: Arc::new(Mutex::new(HashMap::new())), + max_flows, + } + } + + pub fn process_packet(&self, mut packet: Event, is_ingress: bool, payload: &[u8]) { + let direction = if is_ingress { + Direction::Ingress + } else { + Direction::Egress + }; + let packet_key = FlowKey::from_packet(&packet); + let proto = packet_key.protocol; + let src_port = packet_key.src_port; + let dst_port = packet_key.dst_port; + let reversed_key = packet_key.clone().reverse(); + + let Ok(mut flows) = self.flows.lock() else { + return; + }; + + // Try-both: canonical key is whichever orientation already exists in the flow table. + // For new flows, identify the initiator using (in priority order): + // 1. TCP SYN / SYN+ACK flags + // 2. DPI: TLS ClientHello/ServerHello, HTTP request/response, DNS QR bit + // 3. Best effort: use packet as-is + let (actual_key, is_forward) = if flows.contains_key(&packet_key) { + (packet_key, true) + } else if flows.contains_key(&reversed_key) { + (reversed_key, false) + } else { + let flags = packet.tcp_flags(); + if flags.syn && flags.ack { + // Normal: Server (egress side) sends SYN+ACK, packet arrives on ingress → reverse + // Bot attack: Client (egress side) sends SYN+ACK, packet arrives on egress → keep as-is + if is_ingress { + (reversed_key, false) + } else { + (packet_key, true) + } + } else if flags.syn { + (packet_key, true) + } else { + match detect_initiator(payload, proto, src_port, dst_port) { + Some(true) => (packet_key, true), + Some(false) => (reversed_key, false), + None => (packet_key, true), + } + } + }; + + packet.set_is_forward(is_forward); + + // `direction` should reflect the initiator's interface. + // If this packet is backward (is_forward = false), the initiator is on the opposite side. + let initiator_direction = if is_forward { direction } else { direction.flip() }; + + let flow = flows + .entry(actual_key.clone()) + .or_insert_with(|| FlowData::new(actual_key, &packet, initiator_direction)); + + flow.add_packet(&packet); + + if flows.len() > self.max_flows { + if let Some(key) = flows.keys().next().cloned() { + flows.remove(&key); + } + } + } + + pub fn get_flows_snapshot(&self) -> Vec { + let Ok(flows) = self.flows.lock() else { + return Vec::new(); + }; + flows.values().cloned().collect() + } + + pub fn get_flows_for_inference(&self, min_packets: usize) -> Vec { + let Ok(flows) = self.flows.lock() else { + return Vec::new(); + }; + flows + .values() + .filter(|flow| flow.packet_count() >= min_packets) + .cloned() + .collect() + } + + pub fn cleanup_old_flows(&self, max_age_us: u64) { + let now = time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .map(|d| d.as_micros() as u64) + .unwrap_or(0); + + let Ok(mut flows) = self.flows.lock() else { + return; + }; + flows.retain(|_, flow| now.saturating_sub(flow.last_time_us) < max_age_us); + } + + pub fn flow_count(&self) -> usize { + let Ok(flows) = self.flows.lock() else { + return 0; + }; + flows.len() + } +} + +/// Inspect payload bytes to determine which side is the flow initiator. +/// Returns Some(true) if this packet is from the initiator, Some(false) if from the responder, +/// or None if the payload gives no useful signal. +fn detect_initiator(payload: &[u8], protocol: u8, src_port: u16, dst_port: u16) -> Option { + if payload.is_empty() { + return None; + } + + // TLS: record type 0x16 (Handshake), byte 5 = handshake type + // 0x01 = ClientHello → this side is the initiator + // 0x02 = ServerHello → this side is the responder + if payload.len() >= 6 && payload[0] == 0x16 { + return match payload[5] { + 0x01 => Some(true), + 0x02 => Some(false), + _ => None, + }; + } + + // HTTP: request line starts with a method verb (initiator), + // response starts with "HTTP/" (responder) + if payload.len() >= 5 { + if payload.starts_with(b"GET ") + || payload.starts_with(b"POST ") + || payload.starts_with(b"PUT ") + || payload.starts_with(b"HEAD ") + || payload.starts_with(b"DELETE ") + || payload.starts_with(b"OPTIONS ") + || payload.starts_with(b"PATCH ") + { + return Some(true); + } + if payload.starts_with(b"HTTP/") { + return Some(false); + } + } + + // DNS over UDP (port 53): flags byte 2, MSB = QR bit + // 0 = query (initiator), 1 = response (responder) + if protocol == 17 && (src_port == 53 || dst_port == 53) && payload.len() >= 3 { + return Some((payload[2] >> 7) == 0); + } + + None +} diff --git a/net-guardia/src/ml/inference.rs b/net-guardia/src/ml/inference.rs new file mode 100644 index 0000000..ecdf1a4 --- /dev/null +++ b/net-guardia/src/ml/inference.rs @@ -0,0 +1,140 @@ +use std::sync::{Arc, Mutex}; + +use macros::log; +use tract_onnx::prelude::*; + +use super::config_loader::InferenceConfig; +use super::feature_extractor::FlowFeatures; +use super::flow_tracker::FlowData; +use super::model_loader::MLModels; +use crate::model::log::ml::MLLog; +use crate::model::ml_detection::DetectionResult; + +pub struct Inference { + pub models: Arc, + pub config: Arc, +} + +impl Inference { + pub fn new(models: Arc, config: Arc) -> Self { + Self { models, config } + } + + pub fn infer_batch(&self, flows: &[FlowData]) -> Vec { + flows.iter().filter_map(|flow| self.infer_single(flow)).collect() + } + + pub fn infer_single(&self, flow: &FlowData) -> Option { + // extract + let ae_features = self.preprocess_ae_features(flow); + + // 2. Deep Autoencoder + let ae_input = Self::vec_to_array2(&ae_features); + let ae_score = match self.run_autoencoder(&ae_input) { + Ok(score) => score, + Err(e) => { + log!(MLLog::InferenceFailed("DeepAutoEncoder".to_string(), e.to_string())); + return None; + } + }; + + let cls_input = self.build_classifier_input(&ae_features, ae_score); + + let (attack_type, confidence) = match self.run_classifier(&cls_input) { + Ok(result) => result, + Err(e) => { + log!(MLLog::InferenceFailed("LightGBM".to_string(), e.to_string())); + return None; + } + }; + + let is_attack = ae_score >= self.config.ae_threshold; + + let flow_key = format!( + "{}:{} -> {}:{} (proto {}) [{}]", + flow.flow_key.src_ip, + flow.flow_key.src_port, + flow.flow_key.dst_ip, + flow.flow_key.dst_port, + flow.flow_key.protocol, + flow.direction + ); + + Some(DetectionResult { + flow_key, + flow_key_raw: flow.flow_key.clone(), + direction: flow.direction, + is_attack, + attack_type: if is_attack { Some(attack_type) } else { None }, + confidence, + ae_score, + threshold: self.config.ae_threshold, + }) + } + + fn preprocess_ae_features(&self, flow: &FlowData) -> Vec { + let mut features = FlowFeatures::extract(flow, &self.config.ae_feature_names); + features.winsorize(&self.config.ae_clip_params, &self.config.ae_feature_names); + features.normalize(&self.config.ae_scaler_mean, &self.config.ae_scaler_std); + features.clip(self.config.ae_post_clip_min, self.config.ae_post_clip_max); + features.features.iter().map(|&x| x as f32).collect() + } + + fn vec_to_array2(v: &[f32]) -> tract_ndarray::Array2 { + tract_ndarray::Array2::from_shape_fn((1, v.len()), |(_, j)| v[j]) + } + + /// Classifier 輸入 = 已預處理的 ae_features ++ [ae_anomaly_score] + fn build_classifier_input(&self, ae_features: &[f32], ae_score: f32) -> tract_ndarray::Array2 { + let n = ae_features.len() + 1; + tract_ndarray::Array2::from_shape_fn((1, n), |(_, j)| { + if j < ae_features.len() { + ae_features[j] + } else { + ae_score + } + }) + } + + fn run_autoencoder(&self, input: &tract_ndarray::Array2) -> TractResult { + let result = self + .models + .deep_autoencoder + .run(tvec![input.clone().into_tensor().into()])?; + + let output = result[0] + .to_array_view::()? + .into_dimensionality::()?; + + let diff = input - &output; + let mse = (&diff * &diff).sum() / self.config.ae_feature_names.len() as f32; + + Ok(mse) + } + + fn run_classifier(&self, input: &tract_ndarray::Array2) -> TractResult<(String, f32)> { + let input_tensor = input.clone().into_tensor(); + let result = self.models.classifier.run(tvec![input_tensor.into()])?; + + let output = result[0].to_array_view::()?; + + let mut max_prob: f32 = 0.0; + let mut predicted_class: usize = 0; + + for (i, &prob) in output.iter().enumerate() { + if prob > max_prob { + max_prob = prob; + predicted_class = i; + } + } + + let attack_type = self + .config + .attack_labels + .get(&predicted_class.to_string()) + .cloned() + .unwrap_or_else(|| "UNKNOWN".to_string()); + + Ok((attack_type, max_prob)) + } +} diff --git a/net-guardia/src/ml/mod.rs b/net-guardia/src/ml/mod.rs new file mode 100644 index 0000000..9935a76 --- /dev/null +++ b/net-guardia/src/ml/mod.rs @@ -0,0 +1,8 @@ +pub mod model_loader; +pub mod config_loader; +pub mod flow_tracker; +pub mod feature_extractor; +pub mod inference; +pub mod engine; +pub mod aggregator; +pub mod traffic_logger; \ No newline at end of file diff --git a/net-guardia/src/ml/model_loader.rs b/net-guardia/src/ml/model_loader.rs new file mode 100644 index 0000000..7761262 --- /dev/null +++ b/net-guardia/src/ml/model_loader.rs @@ -0,0 +1,60 @@ +use tract_onnx::prelude::*; +use std::path::PathBuf; + +use crate::core::infrastructure::app_config::AppConfig; +use crate::model::error::ml::MLError; +use crate::model::ml_detection::RunnableModel; + +use super::config_loader::InferenceConfig; + +pub struct MLModels { + pub deep_autoencoder: RunnableModel, + pub classifier: RunnableModel, +} +impl MLModels { + pub fn load_models(app_config: &Arc, inference_config: &Arc) -> Result { + Ok(Self { + deep_autoencoder: Self::loader(&app_config.deep_autoencoder_name, inference_config.num_ae_features())?, + classifier: Self::loader(&app_config.classifier_name, inference_config.num_classifier_features())? + }) + } + + pub fn loader(model: &str, features: usize) -> Result { + let model_path = PathBuf::from("models").join(model); + + let mut model = onnx() + .model_for_path(&model_path) + .map_err(|_| { + MLError::ModelLoadFailed { path: model_path.clone() } + })?; + + model.set_input_fact(0, f32::fact(&[1, features]).into()) + .map_err(|_| { + MLError::ModelLoadFailed { path: model_path.clone() } + })?; + + let runnable_model = model + .into_optimized() + .map_err(|_| { + MLError::ModelLoadFailed { path: model_path.clone() } + })? + .into_runnable() + .map_err(|_| { + MLError::ModelLoadFailed { path: model_path } + })?; + + Ok(runnable_model) + } + + pub fn get_model_info(&self, name: &str) -> String { + let model = match name { + "deep_autoencoder" => &self.deep_autoencoder, + "classifier" => &self.classifier, + _ => return "unknown model".to_string(), + }; + + let inputs = model.model().inputs.len(); + let outputs = model.model().outputs.len(); + format!("{}: inputs: {}, outputs: {}", name, inputs, outputs) + } +} \ No newline at end of file diff --git a/net-guardia/src/ml/traffic_logger.rs b/net-guardia/src/ml/traffic_logger.rs new file mode 100644 index 0000000..d027a22 --- /dev/null +++ b/net-guardia/src/ml/traffic_logger.rs @@ -0,0 +1,46 @@ +use std::fs::OpenOptions; +use std::io::{BufWriter, Write}; +use std::thread; + +use crossbeam::channel::{bounded, Sender, TrySendError}; + +pub struct TrafficLogger { + sender: Sender>, +} + +impl TrafficLogger { + pub fn new(csv_path: &str, header: Vec) -> Result { + let file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(csv_path)?; + + let mut writer = BufWriter::new(file); + writeln!(writer, "{}", header.join(","))?; + writer.flush()?; + + let (sender, receiver) = bounded::>(65536); + + thread::Builder::new() + .name("traffic-logger".to_string()) + .spawn(move || { + for record in receiver { + if let Err(e) = writeln!(writer, "{}", record.join(",")) { + eprintln!("[traffic-logger] write error: {}", e); + } + } + let _ = writer.flush(); + })?; + + Ok(Self { sender }) + } + + pub fn log_row(&self, record: Vec) { + match self.sender.try_send(record) { + Ok(_) => {} + Err(TrySendError::Full(_)) => {} + Err(TrySendError::Disconnected(_)) => {} + } + } +} diff --git a/net-guardia/src/model/config.rs b/net-guardia/src/model/config.rs new file mode 100644 index 0000000..12223ed --- /dev/null +++ b/net-guardia/src/model/config.rs @@ -0,0 +1,34 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize)] +pub struct ConfigTable { + #[serde(rename = "Config")] + pub config: Config, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Config { + pub ingress_ifname: String, + pub egress_ifname: String, + pub geoip_db_name: String, + pub deep_autoencoder_name: String, + pub classifier_name: String, + pub models_config_name: String, + pub combined_queue_count: u32, + pub channel_size: usize, + pub fill_queue_size: u32, + pub comp_queue_size: u32, + pub tx_queue_size: u32, + pub rx_queue_size: u32, + pub frame_size: u32, + pub frame_count: u32, + pub refresh_interval: u64, + pub http_server_bind_port: u16, + pub max_concurrent_flows: usize, + pub min_packets_for_inference: usize, + pub inference_interval_secs: u64, + pub aggregator_window_secs: u64, + pub inference_batch_size: usize, + pub traffic_logging_mode: bool, + pub traffic_log_csv_path: String, +} \ No newline at end of file diff --git a/net-guardia/src/model/direction.rs b/net-guardia/src/model/direction.rs new file mode 100644 index 0000000..74874dd --- /dev/null +++ b/net-guardia/src/model/direction.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)] +#[serde(rename_all = "lowercase")] +pub enum Direction { + Ingress, + Egress, +} + +impl Direction { + pub fn flip(self) -> Self { + match self { + Direction::Ingress => Direction::Egress, + Direction::Egress => Direction::Ingress, + } + } +} + +impl std::fmt::Display for Direction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Direction::Ingress => write!(f, "Ingress"), + Direction::Egress => write!(f, "Egress"), + } + } +} + +#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash)] +#[serde(rename_all = "lowercase")] +pub enum FlowDirection { + Source, + Destination, +} diff --git a/net-guardia/src/model/error/ebpf.rs b/net-guardia/src/model/error/ebpf.rs new file mode 100644 index 0000000..f39002f --- /dev/null +++ b/net-guardia/src/model/error/ebpf.rs @@ -0,0 +1,69 @@ +use macros::traceable; +use tracing; + +traceable! { + EbpfError { + #[error("Failed to initialize eBPF logger")] + LoggerInitFailed => tracing::Level::ERROR, + + #[error("Ebpf program not found")] + EbpfNotFound => tracing::Level::ERROR, + + #[no_source] + #[error("Failed to load XDP program")] + ProgramNotFound => tracing::Level::ERROR, + + #[error("Failed to load XDP program")] + GetProgramFailed => tracing::Level::ERROR, + + #[error("Failed to load XDP program")] + LoadProgramFailed => tracing::Level::ERROR, + + #[error("Failed to attach the XDP program")] + AttachProgramFailed => tracing::Level::ERROR, + + #[error("Failed to set umem")] + UmemSetFailed => tracing::Level::ERROR, + + #[error("Failed to set AF_XDP socket")] + SocketSetFailed => tracing::Level::ERROR, + + #[error("Failed to set AF_XDP")] + AfXdpSetFailed => tracing::Level::ERROR, + + #[error("Failed to wakeup TX")] + WakeupTXFailed => tracing::Level::ERROR, + + #[no_source] + #[error("Map not found")] + MapNotFound => tracing::Level::ERROR, + + #[error("An error occurred during map operation")] + MapOperationError => tracing::Level::ERROR, + + #[no_source] + #[error("The ip required for operation does not exist")] + IpDoesNotExist => tracing::Level::ERROR, + + #[no_source] + #[error("Amount of rules has reached the upper limit")] + RuleReachLimit => tracing::Level::ERROR, + + #[no_source] + #[error("Unknown error")] + UnknownError => tracing::Level::ERROR, + + #[error("Failed to spawn XSK thread")] + ThreadSpawnFailed => tracing::Level::ERROR, + + #[error("Completion queue processing failed")] + CompQueueError => tracing::Level::ERROR, + + #[error("RX queue processing failed")] + RXQueueError => tracing::Level::ERROR, + + #[error("TX queue processing failed")] + TXQueueError => tracing::Level::ERROR, + } +} + diff --git a/net-guardia/src/model/error/http.rs b/net-guardia/src/model/error/http.rs new file mode 100644 index 0000000..af167e5 --- /dev/null +++ b/net-guardia/src/model/error/http.rs @@ -0,0 +1,14 @@ +use macros::traceable; + +traceable! { + HttpError { + #[error("Bind port error")] + BindPortError => tracing::Level::ERROR, + + #[error("Http Server panic")] + ServerPanic => tracing::Level::ERROR, + + #[error("WebSocket error")] + WebSocketError => tracing::Level::ERROR, + } +} diff --git a/net-guardia/src/model/error/io.rs b/net-guardia/src/model/error/io.rs new file mode 100644 index 0000000..e46cc3c --- /dev/null +++ b/net-guardia/src/model/error/io.rs @@ -0,0 +1,10 @@ +use std::path::PathBuf; + +use macros::traceable; + +traceable! { + IOError { + #[error("Failed to create directory: {path}")] + CreateDirectoryFailed { path: PathBuf } => tracing::Level::ERROR, + } +} diff --git a/net-guardia/src/model/error/misc.rs b/net-guardia/src/model/error/misc.rs new file mode 100644 index 0000000..cf7db5a --- /dev/null +++ b/net-guardia/src/model/error/misc.rs @@ -0,0 +1,29 @@ +use macros::traceable; + +traceable! { + MiscError { + #[no_source] + #[error("Failed to remove limit on locked memory, ret is: {ret}")] + RamLimitUnlockError { ret: i32 } => tracing::Level::ERROR, + + #[error("Failed to send message to receiver")] + SendMessageError => tracing::Level::ERROR, + + #[error("Failed to serialize data")] + SerializeError => tracing::Level::ERROR, + + #[error("Failed to deserialize data")] + DeserializeError => tracing::Level::ERROR, + + #[no_source] + #[error("Network interface '{interface}' not found")] + NetworkInterfaceNotFound { interface: String } => tracing::Level::ERROR, + + #[error("Invalid GeoIP configuration")] + InvalidGeoIPConfiguration => tracing::Level::ERROR, + + #[no_source] + #[error("Failed to create traffic log file '{path}': {reason}")] + TrafficLogCreateError { path: String, reason: String } => tracing::Level::ERROR, + } +} \ No newline at end of file diff --git a/net-guardia/src/model/error/ml.rs b/net-guardia/src/model/error/ml.rs new file mode 100644 index 0000000..b342b45 --- /dev/null +++ b/net-guardia/src/model/error/ml.rs @@ -0,0 +1,23 @@ +use std::path::PathBuf; + +use macros::traceable; + +traceable! { + MLError { + #[no_source] + #[error("Initialize Machine Learning detection failed")] + InitializeFailed => tracing::Level::ERROR, + + #[no_source] + #[error("Failed to load ONNX model from: {path:?}")] + ModelLoadFailed { path: PathBuf } => tracing::Level::ERROR, + + #[no_source] + #[error("Failed to load inference configuration from: {path:?}")] + ConfigLoadFailed { path: PathBuf } => tracing::Level::ERROR, + + #[no_source] + #[error("Failed to parse inference configuration: {reason}")] + ConfigParseFailed { reason: String } => tracing::Level::ERROR, + } +} \ No newline at end of file diff --git a/net-guardia/src/model/error/mod.rs b/net-guardia/src/model/error/mod.rs new file mode 100644 index 0000000..94f94bf --- /dev/null +++ b/net-guardia/src/model/error/mod.rs @@ -0,0 +1,67 @@ +pub mod ebpf; +pub mod http; +pub mod io; +pub mod misc; +pub mod ml; +pub mod system; + +use serde::{Deserialize, Serialize}; + +use crate::model::error::ebpf::EbpfError; +use crate::model::error::http::HttpError; +use crate::model::error::io::IOError; +use crate::model::error::misc::MiscError; +use crate::model::error::ml::MLError; +use crate::model::error::system::SystemError; + +#[derive(Clone, Debug, thiserror::Error, Serialize, Deserialize)] +pub enum Error { + #[error("{0}")] + Ebpf(EbpfError), + #[error("{0}")] + Http(HttpError), + #[error("{0}")] + ML(MLError), + #[error("{0}")] + IO(IOError), + #[error("{0}")] + Misc(MiscError), + #[error("{0}")] + System(SystemError), +} + +impl From for Error { + fn from(error: EbpfError) -> Self { + Self::Ebpf(error) + } +} + +impl From for Error { + fn from(error: HttpError) -> Self { + Self::Http(error) + } +} + +impl From for Error { + fn from(error: IOError) -> Self { + Self::IO(error) + } +} + +impl From for Error { + fn from(error: MiscError) -> Self { + Self::Misc(error) + } +} + +impl From for Error { + fn from(error: SystemError) -> Self { + Self::System(error) + } +} + +impl From for Error { + fn from(error: MLError) -> Self { + Self::ML(error) + } +} \ No newline at end of file diff --git a/net-guardia/src/model/error/system.rs b/net-guardia/src/model/error/system.rs new file mode 100644 index 0000000..9069824 --- /dev/null +++ b/net-guardia/src/model/error/system.rs @@ -0,0 +1,33 @@ +use macros::traceable; + +traceable! { + SystemError { + #[no_source] + #[error("Unable to run as administrator")] + RunAsAdminFailed => tracing::Level::ERROR, + + #[no_source] + #[error("Invalid configuration")] + InvalidConfig => tracing::Level::ERROR, + + #[error("Configuration not found")] + ConfigNotFound => tracing::Level::ERROR, + + #[error("Failed to terminate instance")] + TerminateError => tracing::Level::ERROR, + + #[no_source] + #[error("Failed to send shutdown signal")] + ShutdownSignalFailed => tracing::Level::ERROR, + + #[error("Unexcepted thread panic")] + ThreadPanic => tracing::Level::ERROR, + + #[error("Unexcepted error")] + UnexpectError => tracing::Level::ERROR, + + #[no_source] + #[error("Unknown error")] + UnknownError => tracing::Level::ERROR, + } +} diff --git a/net-guardia/src/model/geo_stats.rs b/net-guardia/src/model/geo_stats.rs new file mode 100644 index 0000000..4bf850e --- /dev/null +++ b/net-guardia/src/model/geo_stats.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; +use common::model::flow_stats::FlowStats; + +#[derive(Debug, Clone, Serialize)] +pub struct FlowStatsWithGeo { + #[serde(flatten)] + pub stats: FlowStats, + pub geo: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GeoLocation { + pub country: Option, + pub country_code: Option, + pub city: Option, + pub latitude: Option, + pub longitude: Option, + pub timezone: Option, +} diff --git a/net-guardia/src/model/health.rs b/net-guardia/src/model/health.rs new file mode 100644 index 0000000..15b9584 --- /dev/null +++ b/net-guardia/src/model/health.rs @@ -0,0 +1,81 @@ +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] +pub struct SystemHealthMetrics { + pub timestamp: u64, + pub boot_time: u64, + pub uptime_seconds: u64, + pub system_info: SystemInfo, + pub cpu_details: CpuDetails, + pub memory_usage: MemoryUsage, + pub network_stats: ConfiguredNetworkStats, + pub load_average: Option, + pub temperature: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct SystemInfo { + pub kernel_version: Option, + pub os_name: Option, + pub os_version: Option, + pub architecture: String, + pub total_processes: usize, +} + +#[derive(Debug, Clone, Serialize)] +pub struct CpuDetails { + pub cpu_brand: String, + pub core_count: usize, + pub cpu_usage: f32, + pub cpu_frequency: u64, + pub cores: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct CpuCoreInfo { + pub core_id: usize, + pub usage_percent: f32, + pub frequency: u64, +} + +#[derive(Debug, Clone, Serialize)] +pub struct MemoryUsage { + pub total: u64, + pub used: u64, + pub available: u64, + pub usage_percent: f32, + pub swap_total: u64, + pub swap_used: u64, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ConfiguredNetworkStats { + pub ingress: Option, + pub egress: Option, + // pub management: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct NetworkStats { + pub interface: String, + pub bytes_received: u64, + pub bytes_transmitted: u64, + pub packets_received: u64, + pub packets_transmitted: u64, + pub errors_received: u64, + pub errors_transmitted: u64, +} + +#[derive(Debug, Clone, Serialize)] +pub struct LoadAverage { + pub one_minute: f64, + pub five_minute: f64, + pub fifteen_minute: f64, +} + +#[derive(Debug, Clone, Serialize)] +pub struct SystemHealthStatus { + pub overall_healthy: bool, + pub issues: Vec, + pub warnings: Vec, +} diff --git a/net-guardia/src/model/ip_address.rs b/net-guardia/src/model/ip_address.rs new file mode 100644 index 0000000..488a082 --- /dev/null +++ b/net-guardia/src/model/ip_address.rs @@ -0,0 +1,72 @@ +use std::hash::Hash; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; + +use common::model::ip_address::*; + +pub trait NativeConvert: Copy { + type Native: Eq + PartialEq + Hash; + fn into_native(self) -> Self::Native; + fn from_native(native: Self::Native) -> Self; +} + +impl NativeConvert for IPv4 { + type Native = Ipv4Addr; + + fn into_native(self) -> Self::Native { + Ipv4Addr::from(u32::from_be(self)) + } + + fn from_native(native: Self::Native) -> Self { + native.to_bits().to_be() + } +} + +impl NativeConvert for IPv6 { + type Native = Ipv6Addr; + + fn into_native(self) -> Self::Native { + Ipv6Addr::from(u128::from_be(self)) + } + + fn from_native(native: Self::Native) -> Self { + native.to_bits().to_be() + } +} + +impl NativeConvert for AddrPortV4 { + type Native = SocketAddrV4; + + fn into_native(self) -> Self::Native { + SocketAddrV4::new( + Ipv4Addr::from(u32::from_be(self.ip())), + self.port() + ) + } + + fn from_native(native: Self::Native) -> Self { + AddrPortV4::new( + native.ip().to_bits().to_be(), + native.port() + ) + } +} + +impl NativeConvert for AddrPortV6 { + type Native = SocketAddrV6; + + fn into_native(self) -> Self::Native { + SocketAddrV6::new( + Ipv6Addr::from(u128::from_be(self.ip())), + self.port(), + 0, + 0 + ) + } + + fn from_native(native: Self::Native) -> Self { + AddrPortV6::new( + native.ip().to_bits().to_be(), + native.port() + ) + } +} \ No newline at end of file diff --git a/net-guardia/src/model/list_type.rs b/net-guardia/src/model/list_type.rs new file mode 100644 index 0000000..e830910 --- /dev/null +++ b/net-guardia/src/model/list_type.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash)] +#[serde(rename_all = "lowercase")] +pub enum ListType { + #[serde(rename = "white_list")] + White, + #[serde(rename = "black_list")] + Black, +} diff --git a/net-guardia/src/model/log/ebpf.rs b/net-guardia/src/model/log/ebpf.rs new file mode 100644 index 0000000..3f6c6dc --- /dev/null +++ b/net-guardia/src/model/log/ebpf.rs @@ -0,0 +1,54 @@ +use macros::loggable; +use tracing; + +loggable! { + EbpfLog { + #[error("Attach XDP program success")] + AttachProgramSuccess => tracing::Level::INFO, + + #[error("Queue initialization incomplete")] + QueueInitIncomplete => tracing::Level::WARN, + + #[error("Queue refill incomplete")] + QueueRefillIncomplete => tracing::Level::WARN, + + #[error("No frames submit to queue")] + NoFrameSubmit => tracing::Level::WARN, + + #[error("Queue pair {queue_id} started successfully")] + QueuePairStarted { queue_id: u32 } => tracing::Level::INFO, + + #[error("XSK thread shutting down")] + XSKShutdown => tracing::Level::INFO, + + #[error("Frame pool exhausted! Pending TX: {send_len} packets")] + FramePoolExhausted { send_len: usize } => tracing::Level::WARN, + + #[error("No frames available for TX")] + NoFramesAvailable => tracing::Level::WARN, + + #[error("TX wakeup failed: {error}")] + TXWakeupFailed { error: String } => tracing::Level::WARN, + + #[error("Completion queue processing error: {error}")] + CompQueueError { error: String } => tracing::Level::ERROR, + + #[error("RX queue processing error: {error}")] + RXQueueError { error: String } => tracing::Level::ERROR, + + #[error("TX queue processing error: {error}")] + TXQueueError { error: String } => tracing::Level::ERROR, + + #[error("Failed to spawn thread '{thread_name}': {error}")] + ThreadSpawnFailed { thread_name: String, error: String } => tracing::Level::ERROR, + + #[error("Forward channel full, dropping packet")] + ForwardChannelFull => tracing::Level::WARN, + + #[error("Forward channel disconnected")] + ForwardChannelDisconnected => tracing::Level::ERROR, + + #[error("Fill queue incomplete: produced {produced}, expected {expected}")] + FillQueueIncomplete { produced: usize, expected: usize } => tracing::Level::WARN, + } +} \ No newline at end of file diff --git a/net-guardia/src/model/log/health.rs b/net-guardia/src/model/log/health.rs new file mode 100644 index 0000000..5302f19 --- /dev/null +++ b/net-guardia/src/model/log/health.rs @@ -0,0 +1,11 @@ +use macros::loggable; + +loggable! { + Health { + #[error("{ifname} interface '{interface}' not found")] + InterfaceNotFound { ifname: String, interface: String } => tracing::Level::WARN, + + #[error("Failed to broadcast system health metrics: {error}")] + BroadcastFailed { error: String } => tracing::Level::ERROR, + } +} diff --git a/net-guardia/src/model/log/http.rs b/net-guardia/src/model/log/http.rs new file mode 100644 index 0000000..f3235c7 --- /dev/null +++ b/net-guardia/src/model/log/http.rs @@ -0,0 +1,9 @@ +use macros::loggable; +use tracing; + +loggable! { + HttpLog { + #[error("Health WebSocket lagged, skipped {skipped} messages")] + WebSocketLaged { skipped: u64 } => tracing::Level::WARN, + } +} diff --git a/net-guardia/src/model/log/misc.rs b/net-guardia/src/model/log/misc.rs new file mode 100644 index 0000000..00d5332 --- /dev/null +++ b/net-guardia/src/model/log/misc.rs @@ -0,0 +1,9 @@ +use macros::loggable; +use tracing; + +loggable! { + MiscLog { + #[error("GeoIP features will be disabled")] + GeoIPDisabled => tracing::Level::WARN, + } +} diff --git a/net-guardia/src/model/log/ml.rs b/net-guardia/src/model/log/ml.rs new file mode 100644 index 0000000..76f69dd --- /dev/null +++ b/net-guardia/src/model/log/ml.rs @@ -0,0 +1,72 @@ +use macros::loggable; +use tracing; + +loggable! { + MLLog { + #[error("Initializing Machine Learning with inference URL: {url}")] + Initializing { url: String } => tracing::Level::INFO, + + #[error("Continuing without Machine Learning detection")] + Skiped => tracing::Level::WARN, + + #[error("Machine Learning detection is disabled (no ml_inference_url configured)")] + Disabled => tracing::Level::INFO, + + #[error("Machine Learning detection starting")] + Starting => tracing::Level::INFO, + + #[error("Machine Learning detection ready")] + Ready => tracing::Level::INFO, + + #[error("Machine Learning channel disconnected")] + ChannelDisconnected => tracing::Level::WARN, + + #[error("Failed to forward packet: {error}")] + ForwardPacketFailed { error: String } => tracing::Level::WARN, + + #[error("Attach XDP program success")] + AttachProgramSuccess => tracing::Level::INFO, + + #[error("Queue initialization incomplete")] + QueueInitIncomplete => tracing::Level::WARN, + + #[error("Queue refill incomplete")] + QueueRefillIncomplete => tracing::Level::WARN, + + #[error("No frames submit to queue")] + NoFrameSubmit => tracing::Level::WARN, + + #[error("Queue pair {queue_id} started successfully")] + QueuePairStarted { queue_id: u32 } => tracing::Level::INFO, + + #[error("ML models loaded - {info}")] + ModelsLoaded { info: String } => tracing::Level::INFO, + + #[error("Inference configuration loaded: {features} features, {attacks} attack types")] + ConfigLoaded { features: usize, attacks: usize } => tracing::Level::INFO, + + #[error("Inference completed: {total_flows} flows ({anomaly} anomaly, {benign} benign) in {duration_ms}ms ({throughput:.1} flows/s)")] + InferenceCompleted { total_flows: usize, anomaly: usize, benign: usize, duration_ms: u32, throughput: f32 } => tracing::Level::INFO, + + #[error("Inference skipped: {reason}")] + InferenceSkipped { reason: String } => tracing::Level::INFO, + + #[error("Threat detected [{direction}]: {flow} -> {attack_type} (confidence: {confidence:.2}, ae_score: {ae_score:.4})")] + ThreatDetected { direction: String, flow: String, attack_type: String, confidence: f32, ae_score: f32 } => tracing::Level::WARN, + + #[error("Flow stats: total={total_flows}, qualified={flows_len}, min_packets={min_packets}, packet_counts: {counts}")] + FlowStats { total_flows: usize, flows_len: usize, min_packets: usize, counts: String } => tracing::Level::INFO, + + #[error("Running inference on {size} flows")] + RunningInference { size: usize } => tracing::Level::INFO, + + #[error("Inference returned fewer results: expected {size}, got {len}")] + InferenceResults { size: usize, len: usize } => tracing::Level::INFO, + + #[error("{model} inference failed: {error}")] + InferenceFailed { model: String, error: String } => tracing::Level::INFO, + + #[error("Failed to parse packet (length: {len})")] + ParsePacketFailed { len: usize } => tracing::Level::INFO, + } +} \ No newline at end of file diff --git a/net-guardia/src/model/log/mod.rs b/net-guardia/src/model/log/mod.rs new file mode 100644 index 0000000..9efbdfb --- /dev/null +++ b/net-guardia/src/model/log/mod.rs @@ -0,0 +1,6 @@ +pub mod ebpf; +pub mod http; +pub mod ml; +pub mod system; +pub mod misc; +pub mod health; diff --git a/net-guardia/src/model/log/system.rs b/net-guardia/src/model/log/system.rs new file mode 100644 index 0000000..aa1d76e --- /dev/null +++ b/net-guardia/src/model/log/system.rs @@ -0,0 +1,30 @@ +use macros::loggable; +use tracing; + +loggable! { + SystemLog { + #[error("Online now")] + Online => tracing::Level::INFO, + + #[error("Initializing")] + Initializing => tracing::Level::INFO, + + #[error("Initialization completed")] + InitializeComplete => tracing::Level::INFO, + + #[error("Termination in process")] + Terminating => tracing::Level::INFO, + + #[error("Termination completed")] + TerminateComplete => tracing::Level::INFO, + + #[error("Invalid configuration")] + InvalidConfig => tracing::Level::INFO, + + #[error("Configuration not found")] + ConfigNotFound => tracing::Level::INFO, + + #[error("Traffic logging mode enabled — writing packets to: {path}")] + TrafficLoggingEnabled { path: String } => tracing::Level::INFO, + } +} \ No newline at end of file diff --git a/net-guardia/src/model/ml_detection.rs b/net-guardia/src/model/ml_detection.rs new file mode 100644 index 0000000..970b331 --- /dev/null +++ b/net-guardia/src/model/ml_detection.rs @@ -0,0 +1,143 @@ +use common::model::event::{Event, TcpFlags}; +use serde::{Deserialize, Serialize}; +use tract_onnx::prelude::{Graph, SimplePlan, TypedFact, TypedOp}; + +use crate::model::direction::Direction; +use crate::utils::packet_parser::{format_ipv4, format_ipv6}; + +pub type RunnableModel = SimplePlan, Graph>>; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClipParams { + pub lower: f64, + pub upper: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AENormalization { + pub min: f64, + pub max: f64, + pub norm_max: f64, + pub mean: f64, + pub std: f64, + pub median: f64, + pub p90: f64, + pub p95: f64, + pub p99: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PrecisionLevels { + pub threshold: f64, + pub precision: f64, +} + +#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct FlowKey { + pub src_ip: String, + pub dst_ip: String, + pub src_port: u16, + pub dst_port: u16, + pub protocol: u8, +} + +impl FlowKey { + pub fn from_packet(packet: &Event) -> Self { + match packet { + Event::IPv4(ipv4) => Self { + src_ip: format_ipv4(ipv4.src_ip), + dst_ip: format_ipv4(ipv4.dst_ip), + src_port: ipv4.src_port, + dst_port: ipv4.dst_port, + protocol: ipv4.protocol as u8, + }, + Event::IPv6(ipv6) => Self { + src_ip: format_ipv6(ipv6.src_ip), + dst_ip: format_ipv6(ipv6.dst_ip), + src_port: ipv6.src_port, + dst_port: ipv6.dst_port, + protocol: ipv6.protocol as u8, + }, + } + } + + pub fn reverse(&self) -> Self { + Self { + src_ip: self.dst_ip.clone(), + dst_ip: self.src_ip.clone(), + src_port: self.dst_port, + dst_port: self.src_port, + protocol: self.protocol, + } + } +} + +#[derive(Debug, Clone)] +pub struct PacketData { + pub timestamp_us: u64, + pub length: u32, + pub header_length: u16, + pub payload_length: u32, + pub flags: TcpFlags, +} + +#[derive(Debug, Clone, Default)] +pub struct BulkState { + pub bulk_count: u32, + pub total_bytes: u64, + pub total_packets: u64, + pub total_duration_us: u64, + pub last_bulk_bytes: u64, + pub last_bulk_packets: u64, + pub last_bulk_start_us: u64, + pub last_bulk_packet_us: u64, + pub in_bulk: bool, +} + +#[derive(Debug, Clone)] +pub struct DetectionResult { + pub flow_key: String, + pub flow_key_raw: FlowKey, + pub direction: Direction, + pub is_attack: bool, + pub attack_type: Option, + pub confidence: f32, + pub ae_score: f32, + pub threshold: f32, +} + +#[derive(Debug, Clone, Default)] +pub struct InferenceStats { + pub total_flows: usize, + pub malicious_flows: usize, + pub benign_flows: usize, + pub inference_time_us: u64, + pub flows_per_second: f32, +} + +impl InferenceStats { + pub fn from_results(results: &[DetectionResult], elapsed_us: u64) -> Self { + let total = results.len(); + let malicious = results.iter().filter(|r| r.is_attack).count(); + let benign = total - malicious; + + let fps = if elapsed_us > 0 { + (total as f64 / (elapsed_us as f64 / 1_000_000.0)) as f32 + } else { + 0.0 + }; + + Self { + total_flows: total, + malicious_flows: malicious, + benign_flows: benign, + inference_time_us: elapsed_us, + flows_per_second: fps, + } + } +} + +#[derive(Debug, Clone)] +pub struct EngineStats { + pub active_flows: usize, +} diff --git a/net-guardia/src/model/mod.rs b/net-guardia/src/model/mod.rs new file mode 100644 index 0000000..a58d474 --- /dev/null +++ b/net-guardia/src/model/mod.rs @@ -0,0 +1,10 @@ +pub mod config; +pub mod direction; +pub mod error; +pub mod geo_stats; +pub mod ip_address; +pub mod list_type; +pub mod log; +pub mod time_type; +pub mod ml_detection; +pub mod health; diff --git a/net-guardia/src/model/time_type.rs b/net-guardia/src/model/time_type.rs new file mode 100644 index 0000000..83dce19 --- /dev/null +++ b/net-guardia/src/model/time_type.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash)] +#[serde(rename_all = "lowercase")] +pub enum TimeType { + #[serde(rename = "1min")] + _1Min = 60 * 1_000_000_000, + #[serde(rename = "10min")] + _10Min = 600 * 1_000_000_000, + #[serde(rename = "1hour")] + _1Hour = 3600 * 1_000_000_000, +} + +impl TimeType { + #[inline] + pub fn duration(&self) -> u64 { + *self as u64 + } +} diff --git a/net-guardia/src/utils/boot_time.rs b/net-guardia/src/utils/boot_time.rs new file mode 100644 index 0000000..86b9cdb --- /dev/null +++ b/net-guardia/src/utils/boot_time.rs @@ -0,0 +1,5 @@ +use sysinfo::System as SystemInfo; + +pub fn boot_time() -> u64 { + SystemInfo::boot_time() * 1_000_000_000 +} diff --git a/net-guardia/src/utils/ip_address.rs b/net-guardia/src/utils/ip_address.rs new file mode 100644 index 0000000..2203b23 --- /dev/null +++ b/net-guardia/src/utils/ip_address.rs @@ -0,0 +1,29 @@ +use std::net::IpAddr; + +use common::define::setting::MAX_RULES_PORT; +use common::model::ip_address::Port; + +pub fn convert_ports_to_vec(ports: [u16; MAX_RULES_PORT]) -> Vec { + let mut filtered_ports: Vec = ports.into_iter().filter(|&port| port != 0).collect(); + if filtered_ports.is_empty() { + filtered_ports.push(0); + } + filtered_ports +} + +pub fn is_private_ip(ip: &IpAddr) -> bool { + match ip { + IpAddr::V4(v4) => { + v4.is_private() + || v4.is_loopback() + || v4.is_link_local() + || v4.is_broadcast() + } + IpAddr::V6(v6) => { + v6.is_loopback() + || v6.is_unique_local() // fc00::/7 + || v6.is_unspecified() + || v6.is_multicast() + } + } +} \ No newline at end of file diff --git a/net-guardia/src/utils/logging.rs b/net-guardia/src/utils/logging.rs new file mode 100644 index 0000000..c755fb4 --- /dev/null +++ b/net-guardia/src/utils/logging.rs @@ -0,0 +1,50 @@ +use std::fs; +use tracing::Level; +use tracing_appender::rolling::{RollingFileAppender, Rotation}; +use tracing_subscriber::filter::EnvFilter; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; + +use crate::model::error::io::IOError; +use crate::model::error::Error; + +pub struct Logging; + +impl Logging { + pub fn initialize() -> Result<(), Error> { + let log_directory = "logs"; + fs::create_dir_all(log_directory) + .map_err(|err| IOError::CreateDirectoryFailed(log_directory, err))?; + + let file_appender = RollingFileAppender::new(Rotation::DAILY, log_directory, "NetGuardia"); + + let stdout_layer = tracing_subscriber::fmt::layer() + .with_file(true) + .with_line_number(true) + .with_thread_ids(true) + .with_target(false) + .with_ansi(true); + + let file_layer = tracing_subscriber::fmt::layer() + .with_file(false) + .with_line_number(false) + .with_thread_ids(false) + .with_target(true) + .with_ansi(false) + .with_writer(file_appender); + + let level = if cfg!(debug_assertions) { + Level::DEBUG + } else { + Level::INFO + }; + + tracing_subscriber::registry() + .with(stdout_layer) + .with(file_layer) + .with(EnvFilter::from_default_env().add_directive(level.into())) + .init(); + + Ok(()) + } +} diff --git a/net-guardia/src/utils/mod.rs b/net-guardia/src/utils/mod.rs new file mode 100644 index 0000000..ac28f95 --- /dev/null +++ b/net-guardia/src/utils/mod.rs @@ -0,0 +1,6 @@ +pub mod logging; +pub mod static_files; +pub mod boot_time; +pub mod packet_parser; + +pub mod ip_address; \ No newline at end of file diff --git a/net-guardia/src/utils/packet_parser.rs b/net-guardia/src/utils/packet_parser.rs new file mode 100644 index 0000000..be4f2a9 --- /dev/null +++ b/net-guardia/src/utils/packet_parser.rs @@ -0,0 +1,187 @@ +use std::mem; +use std::time; + +use common::model::event::{Event, IPv4Event, IPv6Event, TcpFlags}; +use network_types::ip::IpProto; + +pub fn parse_packet(packet_data: &[u8]) -> Option<(Event, usize)> { + if packet_data.len() < 14 { + return None; + } + + let eth_type = u16::from_be_bytes([packet_data[12], packet_data[13]]); + + let timestamp_us = time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .ok()? + .as_micros() as u64; + + match eth_type { + 0x0800 => parse_ipv4(packet_data, timestamp_us), + 0x86DD => parse_ipv6(packet_data, timestamp_us), + _ => None, + } +} + +fn parse_ipv4(packet_data: &[u8], timestamp_us: u64) -> Option<(Event, usize)> { + if packet_data.len() < 34 { + return None; + } + + let ip_header = &packet_data[14..]; + + let protocol_byte = ip_header[9]; + let protocol = unsafe { mem::transmute::(protocol_byte) }; + + let src_ip = u32::from_be_bytes([ip_header[12], ip_header[13], ip_header[14], ip_header[15]]); + let dst_ip = u32::from_be_bytes([ip_header[16], ip_header[17], ip_header[18], ip_header[19]]); + + if protocol_byte != 6 && protocol_byte != 17 { + return None; + } + + let ihl = (ip_header[0] & 0x0F) as usize * 4; + let total_len = u16::from_be_bytes([ip_header[2], ip_header[3]]) as u32; + + if packet_data.len() < 14 + ihl + 4 { + return None; + } + + let transport_header = &ip_header[ihl..]; + let src_port = u16::from_be_bytes([transport_header[0], transport_header[1]]); + let dst_port = u16::from_be_bytes([transport_header[2], transport_header[3]]); + + let (tcp_flags, tcp_window_size, header_length) = if protocol_byte == 6 { + if packet_data.len() < 14 + ihl + 20 { + return None; + } + + let data_offset = (transport_header[12] >> 4) as u16 * 4; + let flags = TcpFlags::from_byte(transport_header[13]); + let window = u16::from_be_bytes([transport_header[14], transport_header[15]]); + + (flags, window, data_offset) + } else if protocol_byte == 17 { + (TcpFlags::default(), 0, 8) + } else { + (TcpFlags::default(), 0, 0) + }; + + let payload_length = total_len.saturating_sub(ihl as u32 + header_length as u32); + let payload_start = 14 + ihl + header_length as usize; + + let event = IPv4Event { + protocol, + src_ip, + dst_ip, + src_port, + dst_port, + packet_length: total_len, + payload_length, + header_length, + timestamp_us, + tcp_flags, + tcp_window_size, + is_forward: false, + }; + + Some((Event::IPv4(event), payload_start)) +} + +fn parse_ipv6(packet_data: &[u8], timestamp_us: u64) -> Option<(Event, usize)> { + if packet_data.len() < 54 { + return None; + } + + let ip_header = &packet_data[14..]; + + let protocol_byte = ip_header[6]; + let protocol = unsafe { mem::transmute::(protocol_byte) }; + + let mut source_ip_bytes = [0u8; 16]; + source_ip_bytes.copy_from_slice(&ip_header[8..24]); + let src_ip = u128::from_be_bytes(source_ip_bytes); + + let mut dest_ip_bytes = [0u8; 16]; + dest_ip_bytes.copy_from_slice(&ip_header[24..40]); + let dst_ip = u128::from_be_bytes(dest_ip_bytes); + + if protocol_byte != 6 && protocol_byte != 17 { + return None; + } + + let payload_len = u16::from_be_bytes([ip_header[4], ip_header[5]]) as u32; + let total_len = payload_len + 40; + + if packet_data.len() < 54 + 4 { + return None; + } + + let transport_header = &ip_header[40..]; + let src_port = u16::from_be_bytes([transport_header[0], transport_header[1]]); + let dst_port = u16::from_be_bytes([transport_header[2], transport_header[3]]); + + let (tcp_flags, tcp_window_size, header_length) = if protocol_byte == 6 { + if packet_data.len() < 54 + 20 { + return None; + } + + let data_offset = (transport_header[12] >> 4) as u16 * 4; + let flags = TcpFlags::from_byte(transport_header[13]); + let window = u16::from_be_bytes([transport_header[14], transport_header[15]]); + + (flags, window, data_offset) + } else if protocol_byte == 17 { + (TcpFlags::default(), 0, 8) + } else { + (TcpFlags::default(), 0, 0) + }; + + let payload_length = total_len.saturating_sub(40 + header_length as u32); + let payload_start = 14 + 40 + header_length as usize; + + let event = IPv6Event { + protocol, + src_ip, + dst_ip, + src_port, + dst_port, + packet_length: total_len, + payload_length, + header_length, + timestamp_us, + tcp_flags, + tcp_window_size, + is_forward: false, + }; + + Some((Event::IPv6(event), payload_start)) +} + +pub fn format_ipv4(addr: u32) -> String { + let bytes = addr.to_be_bytes(); + format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3],) +} + +pub fn format_ipv6(addr: u128) -> String { + let bytes = addr.to_be_bytes(); + format!( + "{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}", + bytes[0], + bytes[1], + bytes[2], + bytes[3], + bytes[4], + bytes[5], + bytes[6], + bytes[7], + bytes[8], + bytes[9], + bytes[10], + bytes[11], + bytes[12], + bytes[13], + bytes[14], + bytes[15] + ) +} diff --git a/net-guardia/src/utils/static_files.rs b/net-guardia/src/utils/static_files.rs new file mode 100644 index 0000000..641ff0d --- /dev/null +++ b/net-guardia/src/utils/static_files.rs @@ -0,0 +1,5 @@ +use rust_embed::RustEmbed; + +#[derive(RustEmbed)] +#[folder = "static"] +pub struct StaticFiles; diff --git a/net-guardia/src/web/api/control/access_control.rs b/net-guardia/src/web/api/control/access_control.rs new file mode 100644 index 0000000..37bd1a4 --- /dev/null +++ b/net-guardia/src/web/api/control/access_control.rs @@ -0,0 +1,93 @@ +use std::net::{SocketAddrV4, SocketAddrV6}; + +use actix_web::{delete, get, put, web, HttpResponse, Responder, Scope}; + +use crate::core::ebpf::access_control::AccessControl; +use crate::model::direction::FlowDirection; +use crate::model::list_type::ListType; + +pub fn initialize() -> Scope { + web::scope("/access_control") + .service(get_ipv4_list) + .service(get_ipv6_list) + .service(add_ipv4_list) + .service(add_ipv6_list) + .service(remove_ipv4_list) + .service(remove_ipv6_list) +} + +#[get("/ipv4/{direction}/{list_type}")] +async fn get_ipv4_list( + path: web::Path<(FlowDirection, ListType)>, + access_control: web::Data, +) -> impl Responder { + let (direction, list_type) = path.into_inner(); + let list = access_control.get_ipv4_list(direction, list_type).await; + HttpResponse::Ok().json(list) +} + +#[get("/ipv6/{direction}/{list_type}")] +async fn get_ipv6_list( + path: web::Path<(FlowDirection, ListType)>, + access_control: web::Data, +) -> impl Responder { + let (direction, list_type) = path.into_inner(); + let list = access_control.get_ipv6_list(direction, list_type).await; + HttpResponse::Ok().json(list) +} + +#[put("/ipv4/{direction}/{list_type}")] +async fn add_ipv4_list( + address: web::Json, + path: web::Path<(FlowDirection, ListType)>, + access_control: web::Data, +) -> impl Responder { + let address = address.into_inner(); + let (direction, list_type) = path.into_inner(); + match access_control.add_ipv4_list(direction, list_type, address).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[put("/ipv6/{direction}/{list_type}")] +async fn add_ipv6_list( + address: web::Json, + path: web::Path<(FlowDirection, ListType)>, + access_control: web::Data, +) -> impl Responder { + let address = address.into_inner(); + let (direction, list_type) = path.into_inner(); + match access_control.add_ipv6_list(direction, list_type, address).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[delete("/ipv4/{direction}/{list_type}")] +async fn remove_ipv4_list( + address: web::Json, + path: web::Path<(FlowDirection, ListType)>, + access_control: web::Data, +) -> impl Responder { + let address = address.into_inner(); + let (direction, list_type) = path.into_inner(); + match access_control.remove_ipv4_list(direction, list_type, address).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[delete("/ipv6/{direction}/{list_type}")] +async fn remove_ipv6_list( + address: web::Json, + path: web::Path<(FlowDirection, ListType)>, + access_control: web::Data, +) -> impl Responder { + let address = address.into_inner(); + let (direction, list_type) = path.into_inner(); + match access_control.remove_ipv6_list(direction, list_type, address).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} diff --git a/net-guardia/src/web/api/control/mod.rs b/net-guardia/src/web/api/control/mod.rs new file mode 100644 index 0000000..d3036e2 --- /dev/null +++ b/net-guardia/src/web/api/control/mod.rs @@ -0,0 +1,12 @@ +pub mod access_control; +pub mod service; +pub mod statistics; + +use actix_web::{web, Scope}; + +pub fn initialize() -> Scope { + web::scope("/ebpf") + .service(access_control::initialize()) + .service(service::initialize()) + .service(statistics::initialize()) +} diff --git a/net-guardia/src/web/api/control/service.rs b/net-guardia/src/web/api/control/service.rs new file mode 100644 index 0000000..3b1df29 --- /dev/null +++ b/net-guardia/src/web/api/control/service.rs @@ -0,0 +1,251 @@ +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; + +use actix_web::{delete, get, post, put, web, HttpResponse, Responder, Scope}; +use common::model::http_method::HttpMethod; + +use crate::core::ebpf::service::Service; + +pub fn initialize() -> Scope { + web::scope("/service") + .service(get_ipv4_http_service) + .service(get_ipv6_http_service) + .service(add_ipv4_http_service) + .service(add_ipv6_http_service) + .service(remove_ipv4_http_service) + .service(remove_ipv6_http_service) + .service(is_ssh_white_list_enable) + .service(enable_ssh_white_list) + .service(disable_ssh_white_list) + .service(get_ipv4_ssh_service) + .service(get_ipv6_ssh_service) + .service(add_ipv4_ssh_service) + .service(add_ipv6_ssh_service) + .service(remove_ipv4_ssh_service) + .service(remove_ipv6_ssh_service) + .service(get_ipv4_ssh_white_list) + .service(get_ipv6_ssh_white_list) + .service(add_ipv4_ssh_white_list) + .service(add_ipv6_ssh_white_list) + .service(remove_ipv4_ssh_white_list) + .service(remove_ipv6_ssh_white_list) + .service(get_ipv4_ssh_black_list) + .service(get_ipv6_ssh_black_list) + .service(add_ipv4_ssh_black_list) + .service(add_ipv6_ssh_black_list) + .service(remove_ipv4_ssh_black_list) + .service(remove_ipv6_ssh_black_list) +} + +#[get("/ipv4/http_service")] +async fn get_ipv4_http_service(service: web::Data) -> impl Responder { + let list = service.get_ipv4_http_service().await; + HttpResponse::Ok().json(web::Json(list)) +} + +#[get("/ipv6/http_service")] +async fn get_ipv6_http_service(service: web::Data) -> impl Responder { + let list = service.get_ipv6_http_service().await; + HttpResponse::Ok().json(web::Json(list)) +} + +#[put("/ipv4/http_service")] +async fn add_ipv4_http_service( + payload: web::Json<(SocketAddrV4, Vec)>, + service: web::Data, +) -> impl Responder { + let (addr, methods) = payload.into_inner(); + match service.add_ipv4_http_service(addr, methods).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[put("/ipv6/http_service")] +async fn add_ipv6_http_service( + payload: web::Json<(SocketAddrV6, Vec)>, + service: web::Data, +) -> impl Responder { + let (addr, methods) = payload.into_inner(); + match service.add_ipv6_http_service(addr, methods).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[delete("/ipv4/http_service")] +async fn remove_ipv4_http_service( + payload: web::Json<(SocketAddrV4, Vec)>, + service: web::Data, +) -> impl Responder { + let (addr, methods) = payload.into_inner(); + match service.remove_ipv4_http_service(addr, methods).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[delete("/ipv6/http_service")] +async fn remove_ipv6_http_service( + payload: web::Json<(SocketAddrV6, Vec)>, + service: web::Data, +) -> impl Responder { + let (addr, methods) = payload.into_inner(); + match service.remove_ipv6_http_service(addr, methods).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[get("/ssh_white_list")] +async fn is_ssh_white_list_enable(service: web::Data) -> impl Responder { + let enabled = service.is_ssh_white_list_enable().await; + HttpResponse::Ok().json(enabled) +} + +#[post("/ssh_white_list/enable")] +async fn enable_ssh_white_list(service: web::Data) -> impl Responder { + match service.enable_ssh_white_list().await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[post("/ssh_white_list/disable")] +async fn disable_ssh_white_list(service: web::Data) -> impl Responder { + match service.disable_ssh_white_list().await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[get("/ipv4/ssh_service")] +async fn get_ipv4_ssh_service(service: web::Data) -> impl Responder { + let list = service.get_ipv4_ssh_service().await; + HttpResponse::Ok().json(web::Json(list)) +} + +#[get("/ipv6/ssh_service")] +async fn get_ipv6_ssh_service(service: web::Data) -> impl Responder { + let list = service.get_ipv6_ssh_service().await; + HttpResponse::Ok().json(web::Json(list)) +} + +#[put("/ipv4/ssh_service")] +async fn add_ipv4_ssh_service(ip_addr: web::Json, service: web::Data) -> impl Responder { + match service.add_ipv4_ssh_service(ip_addr.into_inner()).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[put("/ipv6/ssh_service")] +async fn add_ipv6_ssh_service(ip_addr: web::Json, service: web::Data) -> impl Responder { + match service.add_ipv6_ssh_service(ip_addr.into_inner()).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[delete("/ipv4/ssh_service")] +async fn remove_ipv4_ssh_service(ip_addr: web::Json, service: web::Data) -> impl Responder { + match service.remove_ipv4_ssh_service(ip_addr.into_inner()).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[delete("/ipv6/ssh_service")] +async fn remove_ipv6_ssh_service(ip_addr: web::Json, service: web::Data) -> impl Responder { + match service.remove_ipv6_ssh_service(ip_addr.into_inner()).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[get("/ipv4/ssh_white_list")] +async fn get_ipv4_ssh_white_list(service: web::Data) -> impl Responder { + let list = service.get_ipv4_ssh_white_list().await; + HttpResponse::Ok().json(web::Json(list)) +} + +#[get("/ipv6/ssh_white_list")] +async fn get_ipv6_ssh_white_list(service: web::Data) -> impl Responder { + let list = service.get_ipv6_ssh_white_list().await; + HttpResponse::Ok().json(web::Json(list)) +} + +#[put("/ipv4/ssh_white_list")] +async fn add_ipv4_ssh_white_list(ip_addr: web::Json, service: web::Data) -> impl Responder { + match service.add_ipv4_ssh_white_list(ip_addr.into_inner()).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[put("/ipv6/ssh_white_list")] +async fn add_ipv6_ssh_white_list(ip_addr: web::Json, service: web::Data) -> impl Responder { + match service.add_ipv6_ssh_white_list(ip_addr.into_inner()).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[delete("/ipv4/ssh_white_list")] +async fn remove_ipv4_ssh_white_list(ip_addr: web::Json, service: web::Data) -> impl Responder { + match service.remove_ipv4_ssh_white_list(ip_addr.into_inner()).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[delete("/ipv6/ssh_white_list")] +async fn remove_ipv6_ssh_white_list(ip_addr: web::Json, service: web::Data) -> impl Responder { + match service.remove_ipv6_ssh_white_list(ip_addr.into_inner()).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[get("/ipv4/ssh_black_list")] +async fn get_ipv4_ssh_black_list(service: web::Data) -> impl Responder { + let list = service.get_ipv4_ssh_black_list().await; + HttpResponse::Ok().json(web::Json(list)) +} + +#[get("/ipv6/ssh_black_list")] +async fn get_ipv6_ssh_black_list(service: web::Data) -> impl Responder { + let list = service.get_ipv6_ssh_black_list().await; + HttpResponse::Ok().json(web::Json(list)) +} + +#[put("/ipv4/ssh_black_list")] +async fn add_ipv4_ssh_black_list(ip_addr: web::Json, service: web::Data) -> impl Responder { + match service.add_ipv4_ssh_black_list(ip_addr.into_inner()).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[put("/ipv6/ssh_black_list")] +async fn add_ipv6_ssh_black_list(ip_addr: web::Json, service: web::Data) -> impl Responder { + match service.add_ipv6_ssh_black_list(ip_addr.into_inner()).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[delete("/ipv4/ssh_black_list")] +async fn remove_ipv4_ssh_black_list(ip_addr: web::Json, service: web::Data) -> impl Responder { + match service.remove_ipv4_ssh_black_list(ip_addr.into_inner()).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} + +#[delete("/ipv6/ssh_black_list")] +async fn remove_ipv6_ssh_black_list(ip_addr: web::Json, service: web::Data) -> impl Responder { + match service.remove_ipv6_ssh_black_list(ip_addr.into_inner()).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } +} diff --git a/net-guardia/src/web/api/control/statistics.rs b/net-guardia/src/web/api/control/statistics.rs new file mode 100644 index 0000000..b74f1a4 --- /dev/null +++ b/net-guardia/src/web/api/control/statistics.rs @@ -0,0 +1,69 @@ +use std::sync::Arc; + +use actix_web::{get, web, HttpRequest, HttpResponse, Responder, Scope}; + +use crate::core::ebpf::statistics::Statistics; +use crate::core::infrastructure::app_config::AppConfig; +use crate::model::direction::{Direction, FlowDirection}; +use crate::model::time_type::TimeType; +use crate::web::websocket::flow_websocket; + +pub fn initialize() -> Scope { + web::scope("/statistics") + .service(get_ipv4_flow) + .service(get_ipv6_flow) + .service(websocket_ipv4) + .service(websocket_ipv6) +} + +#[get("/get/ipv4/{direction}/{flow_direction}/{time_type}")] +async fn get_ipv4_flow( + path: web::Path<(Direction, FlowDirection, TimeType)>, + statistics: web::Data>, +) -> impl Responder { + let (direction, flow_direction, time_type) = path.into_inner(); + let flow_data = statistics + .get_ipv4_flow_data(direction, flow_direction, time_type) + .await; + HttpResponse::Ok().json(web::Json(flow_data)) +} + +#[get("/get/ipv6/{direction}/{flow_direction}/{time_type}")] +async fn get_ipv6_flow( + path: web::Path<(Direction, FlowDirection, TimeType)>, + statistics: web::Data>, +) -> impl Responder { + let (direction, flow_direction, time_type) = path.into_inner(); + let flow_data = statistics + .get_ipv6_flow_data(direction, flow_direction, time_type) + .await; + HttpResponse::Ok().json(web::Json(flow_data)) +} + +#[get("/websocket/ipv4/{direction}/{flow_direction}/{time_type}")] +async fn websocket_ipv4( + req: HttpRequest, + stream: web::Payload, + path: web::Path<(Direction, FlowDirection, TimeType)>, + app_config: web::Data, + statistics: web::Data, +) -> impl Responder { + match flow_websocket::websocket_ipv4_flow(req, stream, path, app_config, statistics).await { + Ok(response) => response, + Err(err) => HttpResponse::InternalServerError().body(format!("WebSocket error: {}", err)), + } +} + +#[get("/websocket/ipv6/{direction}/{flow_direction}/{time_type}")] +async fn websocket_ipv6( + req: HttpRequest, + stream: web::Payload, + path: web::Path<(Direction, FlowDirection, TimeType)>, + app_config: web::Data, + statistics: web::Data, +) -> impl Responder { + match flow_websocket::websocket_ipv6_flow(req, stream, path, app_config, statistics).await { + Ok(response) => response, + Err(err) => HttpResponse::InternalServerError().body(format!("WebSocket error: {}", err)), + } +} diff --git a/net-guardia/src/web/api/default.rs b/net-guardia/src/web/api/default.rs new file mode 100644 index 0000000..b46f256 --- /dev/null +++ b/net-guardia/src/web/api/default.rs @@ -0,0 +1,42 @@ +use actix_web::{HttpRequest, HttpResponse, Responder}; +use mime_guess::from_path; + +use crate::utils::static_files::StaticFiles; + +pub async fn default_route(req: HttpRequest) -> impl Responder { + let request_path = req.path(); + + let file_system_path = if request_path == "/" { + "web/index.html".to_string() + } else { + format!("web{}", request_path) + }; + + if let Some(content) = StaticFiles::get(&file_system_path) { + let mime_type = from_path(&file_system_path).first_or_octet_stream(); + return HttpResponse::Ok() + .content_type(mime_type.as_ref()) + .body(content.data.into_owned()); + } + + let html_path = format!("{}.html", file_system_path); + if let Some(content) = StaticFiles::get(&html_path) { + return HttpResponse::Ok() + .content_type("text/html") + .body(content.data.into_owned()); + } + + let index_path = format!("{}/index.html", file_system_path); + if let Some(content) = StaticFiles::get(&index_path) { + return HttpResponse::Ok() + .content_type("text/html") + .body(content.data.into_owned()); + } + + match StaticFiles::get("web/404.html") { + Some(page) => HttpResponse::NotFound() + .content_type("text/html") + .body(page.data.into_owned()), + None => HttpResponse::NotFound().body("404 Not Found"), + } +} \ No newline at end of file diff --git a/net-guardia/src/web/api/health.rs b/net-guardia/src/web/api/health.rs new file mode 100644 index 0000000..e4c2537 --- /dev/null +++ b/net-guardia/src/web/api/health.rs @@ -0,0 +1,35 @@ +use actix_web::{get, web, HttpRequest, HttpResponse, Responder, Scope}; + +use crate::core::infrastructure::health::SystemHealth; +use crate::web::websocket::health_websocket; + +pub fn initialize() -> Scope { + web::scope("/health") + .service(get_current_metrics) + .service(get_health_status) + .service(websocket_metrics) +} + +#[get("/metrics")] +async fn get_current_metrics(health: web::Data) -> impl Responder { + let metrics = health.get_current_metrics().await; + HttpResponse::Ok().json(metrics) +} + +#[get("/status")] +async fn get_health_status(health: web::Data) -> impl Responder { + let status = health.is_system_healthy().await; + HttpResponse::Ok().json(status) +} + +#[get("/websocket/metrics")] +async fn websocket_metrics( + req: HttpRequest, + stream: web::Payload, + health: web::Data, +) -> impl Responder { + match health_websocket::websocket_system_health(req, stream, health).await { + Ok(response) => response, + Err(err) => HttpResponse::InternalServerError().body(format!("WebSocket error: {}", err)), + } +} \ No newline at end of file diff --git a/net-guardia/src/web/api/misc.rs b/net-guardia/src/web/api/misc.rs new file mode 100644 index 0000000..1682654 --- /dev/null +++ b/net-guardia/src/web/api/misc.rs @@ -0,0 +1,14 @@ +use actix_web::{get, web, HttpResponse, Responder, Scope}; + +use crate::utils::boot_time::boot_time; + +pub fn initialize() -> Scope { + web::scope("/misc") + .service(get_boot_time) +} + +#[get("/boot_time")] +async fn get_boot_time() -> impl Responder { + let boot_time = boot_time(); + HttpResponse::Ok().json(boot_time) +} diff --git a/net-guardia/src/web/api/ml_alert.rs b/net-guardia/src/web/api/ml_alert.rs new file mode 100644 index 0000000..766a686 --- /dev/null +++ b/net-guardia/src/web/api/ml_alert.rs @@ -0,0 +1,22 @@ +use crate::core::infrastructure::ml_alert::MLAlert; +use crate::web::websocket::alert_websocket; +use actix_web::{get, web, HttpRequest, HttpResponse, Responder, Scope}; + +pub fn initialize() -> Scope { + web::scope("/ml") + .service(websocket_alert) +} + +#[get("/websocket/alert")] +async fn websocket_alert( + req: HttpRequest, + stream: web::Payload, + ai: web::Data, +) -> impl Responder { + match alert_websocket::websocket_alert(req, stream, ai).await { + Ok(response) => response, + Err(err) => { + HttpResponse::InternalServerError().body(format!("WebSocket error: {}", err)) + } + } +} \ No newline at end of file diff --git a/net-guardia/src/web/api/mod.rs b/net-guardia/src/web/api/mod.rs new file mode 100644 index 0000000..df7dce0 --- /dev/null +++ b/net-guardia/src/web/api/mod.rs @@ -0,0 +1,5 @@ +pub mod control; +pub mod default; +pub mod misc; +pub mod ml_alert; +pub mod health; diff --git a/net-guardia/src/web/mod.rs b/net-guardia/src/web/mod.rs new file mode 100644 index 0000000..a3766ff --- /dev/null +++ b/net-guardia/src/web/mod.rs @@ -0,0 +1,2 @@ +pub mod api; +pub mod websocket; diff --git a/net-guardia/src/web/websocket/alert_websocket.rs b/net-guardia/src/web/websocket/alert_websocket.rs new file mode 100644 index 0000000..f10e849 --- /dev/null +++ b/net-guardia/src/web/websocket/alert_websocket.rs @@ -0,0 +1,90 @@ +use actix_web::{web, HttpRequest, HttpResponse, Result}; +use actix_ws::{handle, Message, MessageStream, Session}; +use futures_util::StreamExt; +use macros::log; +use tokio::sync::broadcast; + +use crate::core::infrastructure::ml_alert::{MLAlert, AlertMessage}; +use crate::model::error::http::HttpError; +use crate::model::error::misc::MiscError; +use crate::model::log::http::HttpLog; + +pub async fn websocket_alert( + req: HttpRequest, + body: web::Payload, + ai: web::Data, +) -> Result { + let (response, session, msg_stream) = handle(&req, body)?; + + let broadcast_rx = ai.subscribe_to_alerts(); + + actix_web::rt::spawn(async move { + handle_alert_connection(session, msg_stream, broadcast_rx).await; + }); + + Ok(response) +} + +async fn handle_alert_connection( + mut session: Session, + mut msg_stream: MessageStream, + mut broadcast_rx: broadcast::Receiver, +) { + loop { + tokio::select! { + msg_result = msg_stream.next() => { + if !handle_client_message(&mut session, msg_result).await { + break; + } + }, + broadcast_result = broadcast_rx.recv() => { + match broadcast_result { + Ok(alert) => { + if !send_alert(&mut session, &alert).await { + break; + } + } + Err(broadcast::error::RecvError::Lagged(skipped)) => { + log!(HttpLog::WebSocketLaged(skipped)); + continue; + } + Err(broadcast::error::RecvError::Closed) => { + break; + } + } + }, + } + } + + let _ = session.close(None).await; +} + +async fn handle_client_message( + session: &mut Session, + msg_result: Option>, +) -> bool { + match msg_result { + Some(Ok(Message::Text(_))) => true, + Some(Ok(Message::Ping(bytes))) => session.pong(&bytes).await.is_ok(), + Some(Ok(Message::Close(reason))) => { + let _ = (session.clone()).close(reason).await; + false + } + Some(Err(err)) => { + log!(HttpError::WebSocketError(err)); + false + } + None => false, + _ => true, + } +} + +async fn send_alert(session: &mut Session, alert: &AlertMessage) -> bool { + match serde_json::to_string(alert) { + Ok(json) => session.text(json).await.is_ok(), + Err(err) => { + log!(MiscError::SerializeError(err)); + false + } + } +} \ No newline at end of file diff --git a/net-guardia/src/web/websocket/flow_websocket.rs b/net-guardia/src/web/websocket/flow_websocket.rs new file mode 100644 index 0000000..434ace5 --- /dev/null +++ b/net-guardia/src/web/websocket/flow_websocket.rs @@ -0,0 +1,190 @@ +use std::sync::Arc; + +use actix_web::{web, HttpRequest, HttpResponse, Result}; +use actix_ws::{handle, Message, MessageStream, Session}; +use futures_util::StreamExt; +use macros::log; +use tokio::time::{interval, Duration}; + +use crate::core::ebpf::statistics::Statistics; +use crate::core::infrastructure::app_config::AppConfig; +use crate::model::direction::{Direction, FlowDirection}; +use crate::model::error::http::HttpError; +use crate::model::error::misc::MiscError; +use crate::model::time_type::TimeType; + +pub async fn websocket_ipv4_flow( + req: HttpRequest, + body: web::Payload, + path: web::Path<(Direction, FlowDirection, TimeType)>, + app_config: web::Data, + statistics: web::Data, +) -> Result { + let app_config = app_config.into_inner(); + let statistics = statistics.into_inner(); + let (direction, flow_direction, time_type) = path.into_inner(); + let (response, session, msg_stream) = handle(&req, body)?; + + actix_web::rt::spawn(async move { + handle_ipv4_flow_connection( + app_config, + statistics, + session, + msg_stream, + direction, + flow_direction, + time_type, + ) + .await; + }); + + Ok(response) +} + +pub async fn websocket_ipv6_flow( + req: HttpRequest, + body: web::Payload, + path: web::Path<(Direction, FlowDirection, TimeType)>, + app_config: web::Data, + statistics: web::Data, +) -> Result { + let app_config = app_config.into_inner(); + let statistics = statistics.into_inner(); + let (direction, flow_direction, time_type) = path.into_inner(); + let (response, session, msg_stream) = handle(&req, body)?; + + actix_web::rt::spawn(async move { + handle_ipv6_flow_connection( + app_config, + statistics, + session, + msg_stream, + direction, + flow_direction, + time_type, + ) + .await; + }); + + Ok(response) +} + +async fn handle_ipv4_flow_connection( + app_config: Arc, + statistics: Arc, + mut session: Session, + mut msg_stream: MessageStream, + direction: Direction, + flow_direction: FlowDirection, + time_type: TimeType, +) { + let config = app_config.config.clone(); + let refresh_interval = Duration::from_secs(config.refresh_interval); + let mut data_interval = interval(refresh_interval); + + loop { + tokio::select! { + msg_result = msg_stream.next() => { + if !handle_client_message(&mut session, msg_result).await { + break; + } + }, + _ = data_interval.tick() => { + if !send_ipv4_flow_data(&statistics, &mut session, direction, flow_direction, time_type).await { + break; + } + }, + } + } + + let _ = session.close(None).await; +} + +async fn handle_ipv6_flow_connection( + app_config: Arc, + statistics: Arc, + mut session: Session, + mut msg_stream: MessageStream, + direction: Direction, + flow_direction: FlowDirection, + time_type: TimeType, +) { + let config = app_config.config.clone(); + let refresh_interval = Duration::from_secs(config.refresh_interval); + let mut data_interval = interval(refresh_interval); + + loop { + tokio::select! { + msg_result = msg_stream.next() => { + if !handle_client_message(&mut session, msg_result).await { + break; + } + }, + _ = data_interval.tick() => { + if !send_ipv6_flow_data(&statistics, &mut session, direction, flow_direction, time_type).await { + break; + } + }, + } + } + + let _ = session.close(None).await; +} + +async fn handle_client_message( + session: &mut Session, + msg_result: Option>, +) -> bool { + match msg_result { + Some(Ok(Message::Text(_))) => true, + Some(Ok(Message::Ping(bytes))) => session.pong(&bytes).await.is_ok(), + Some(Ok(Message::Close(reason))) => { + let _ = (session.clone()).close(reason).await; + false + } + Some(Err(err)) => { + log!(HttpError::WebSocketError(err)); + false + } + None => false, + _ => true, + } +} + +async fn send_ipv4_flow_data( + statistics: &Arc, + session: &mut Session, + direction: Direction, + flow_direction: FlowDirection, + time_type: TimeType, +) -> bool { + let flow_data = statistics + .get_ipv4_flow_data(direction, flow_direction, time_type) + .await; + match serde_json::to_string(&flow_data) { + Ok(json) => session.text(json).await.is_ok(), + Err(err) => { + log!(MiscError::SerializeError(err)); + true + } + } +} + +async fn send_ipv6_flow_data( + statistics: &Arc, + session: &mut Session, + direction: Direction, + flow_direction: FlowDirection, + time_type: TimeType, +) -> bool { + let flow_data = statistics + .get_ipv6_flow_data(direction, flow_direction, time_type) + .await; + match serde_json::to_string(&flow_data) { + Ok(json) => session.text(json).await.is_ok(), + Err(err) => { + log!(MiscError::SerializeError(err)); + true + } + } +} diff --git a/net-guardia/src/web/websocket/health_websocket.rs b/net-guardia/src/web/websocket/health_websocket.rs new file mode 100644 index 0000000..7997571 --- /dev/null +++ b/net-guardia/src/web/websocket/health_websocket.rs @@ -0,0 +1,91 @@ +use actix_web::{web, HttpRequest, HttpResponse, Result}; +use actix_ws::{handle, Message, MessageStream, Session}; +use futures_util::StreamExt; +use macros::log; +use tokio::sync::broadcast; + +use crate::core::infrastructure::health::SystemHealth; +use crate::model::error::http::HttpError; +use crate::model::error::misc::MiscError; +use crate::model::log::http::HttpLog; +use crate::model::health::SystemHealthMetrics; + +pub async fn websocket_system_health( + req: HttpRequest, + body: web::Payload, + health: web::Data, +) -> Result { + let (response, session, msg_stream) = handle(&req, body)?; + + let broadcast_rx = health.subscribe_to_metrics(); + + actix_web::rt::spawn(async move { + handle_health_connection(session, msg_stream, broadcast_rx).await; + }); + + Ok(response) +} + +async fn handle_health_connection( + mut session: Session, + mut msg_stream: MessageStream, + mut broadcast_rx: broadcast::Receiver, +) { + loop { + tokio::select! { + msg_result = msg_stream.next() => { + if !handle_client_message(&mut session, msg_result).await { + break; + } + }, + broadcast_result = broadcast_rx.recv() => { + match broadcast_result { + Ok(metrics) => { + if !send_metrics(&mut session, &metrics).await { + break; + } + } + Err(broadcast::error::RecvError::Lagged(skipped)) => { + log!(HttpLog::WebSocketLaged(skipped)); + continue; + } + Err(broadcast::error::RecvError::Closed) => { + break; + } + } + }, + } + } + + let _ = session.close(None).await; +} + +async fn handle_client_message( + session: &mut Session, + msg_result: Option>, +) -> bool { + match msg_result { + Some(Ok(Message::Text(_))) => true, + Some(Ok(Message::Ping(bytes))) => session.pong(&bytes).await.is_ok(), + Some(Ok(Message::Close(reason))) => { + let _ = (session.clone()).close(reason).await; + false + } + Some(Err(err)) => { + log!(HttpError::WebSocketError(err)); + false + } + None => false, + _ => true, + } +} + +async fn send_metrics(session: &mut Session, metrics: &SystemHealthMetrics) -> bool { + match serde_json::to_string(metrics) { + Ok(json) => session.text(json).await.is_ok(), + Err(err) => { + log!(MiscError::SerializeError(err)); + false + } + } +} \ No newline at end of file diff --git a/net-guardia/src/web/websocket/mod.rs b/net-guardia/src/web/websocket/mod.rs new file mode 100644 index 0000000..e8139e1 --- /dev/null +++ b/net-guardia/src/web/websocket/mod.rs @@ -0,0 +1,3 @@ +pub mod flow_websocket; +pub mod health_websocket; +pub mod alert_websocket; diff --git a/net-guardia/static/geo/GeoLite2-City.mmdb b/net-guardia/static/geo/GeoLite2-City.mmdb new file mode 100644 index 0000000..2df0b8b Binary files /dev/null and b/net-guardia/static/geo/GeoLite2-City.mmdb differ diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..4f97237 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,6 @@ +group_imports = "StdExternalCrate" +reorder_imports = true +unstable_features = true +max_width = 120 +hard_tabs = false +tab_spaces = 4