From 6101de688783c9fa80927688a15d4d73b8afc5dc Mon Sep 17 00:00:00 2001 From: Jirayu Date: Wed, 15 Apr 2026 09:10:52 +0700 Subject: [PATCH] Implement XDP firewall with real-time TUI monitoring Features: - High-performance packet filtering via eBPF/XDP - Instant blocklist with dynamic CLI management - Exact-match rules with Drop/Pass/Log actions - CIDR-based IP range dropping via LPM trie - Token-bucket rate limiting (IP-based and flow-based) - Auto temp bans for rate limit violators - Real-time event logging via BPF ring buffer - Interactive TUI monitor with live stats Co-Authored-By: Claude Opus 4.6 --- .cargo/config.toml | 2 + .gitignore | 5 + Cargo.lock | 1339 ++++++++++++++++++++++++++ Cargo.toml | 7 + scripts/setup-veth.sh | 18 + xdp-firewall-common/Cargo.toml | 10 + xdp-firewall-common/src/lib.rs | 103 ++ xdp-firewall-ebpf/.cargo/config.toml | 3 + xdp-firewall-ebpf/Cargo.lock | 346 +++++++ xdp-firewall-ebpf/Cargo.toml | 16 + xdp-firewall-ebpf/src/main.rs | 257 +++++ xdp-firewall/Cargo.toml | 21 + xdp-firewall/build.rs | 22 + xdp-firewall/src/cli.rs | 118 +++ xdp-firewall/src/firewall.rs | 108 +++ xdp-firewall/src/logger.rs | 24 + xdp-firewall/src/main.rs | 159 +++ xdp-firewall/src/maps.rs | 55 ++ xdp-firewall/src/monitor/app.rs | 84 ++ xdp-firewall/src/monitor/mod.rs | 260 +++++ xdp-firewall/src/monitor/ui.rs | 203 ++++ xdp-firewall/src/stats.rs | 15 + xtask/Cargo.toml | 9 + xtask/src/main.rs | 51 + 24 files changed, 3235 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100755 scripts/setup-veth.sh create mode 100644 xdp-firewall-common/Cargo.toml create mode 100644 xdp-firewall-common/src/lib.rs create mode 100644 xdp-firewall-ebpf/.cargo/config.toml create mode 100644 xdp-firewall-ebpf/Cargo.lock create mode 100644 xdp-firewall-ebpf/Cargo.toml create mode 100644 xdp-firewall-ebpf/src/main.rs create mode 100644 xdp-firewall/Cargo.toml create mode 100644 xdp-firewall/build.rs create mode 100644 xdp-firewall/src/cli.rs create mode 100644 xdp-firewall/src/firewall.rs create mode 100644 xdp-firewall/src/logger.rs create mode 100644 xdp-firewall/src/main.rs create mode 100644 xdp-firewall/src/maps.rs create mode 100644 xdp-firewall/src/monitor/app.rs create mode 100644 xdp-firewall/src/monitor/mod.rs create mode 100644 xdp-firewall/src/monitor/ui.rs create mode 100644 xdp-firewall/src/stats.rs create mode 100644 xtask/Cargo.toml create mode 100644 xtask/src/main.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..a6b014e --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target-dir = "target" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84b8c03 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target +*.swp +*.swo +*~ +.DS_Store diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d5d0497 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1339 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[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-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-obj" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51b96c5a8ed8705b40d655273bc4212cbbf38d4e3be2788f36306f154523ec7" +dependencies = [ + "bytes", + "core-error", + "hashbrown 0.15.5", + "log", + "object", + "thiserror 1.0.69", +] + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[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 = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "derive_more", + "document-features", + "futures-core", + "mio", + "parking_lot", + "rustix 1.1.4", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "duct" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ab5718d1224b63252cd0c6f74f6480f9ffeb117438a2e0f5cf6d9a4798929c" +dependencies = [ + "libc", + "once_cell", + "os_pipe", + "shared_child", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ipnetwork" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm 0.28.1", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[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.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[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 = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "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.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[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 = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[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-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "xdp-firewall" +version = "0.1.0" +dependencies = [ + "anyhow", + "aya", + "aya-log", + "clap", + "crossterm 0.29.0", + "duct", + "ipnetwork", + "ratatui", + "thiserror 2.0.18", + "tokio", + "tracing", + "tracing-subscriber", + "xdp-firewall-common", +] + +[[package]] +name = "xdp-firewall-common" +version = "0.1.0" +dependencies = [ + "aya", +] + +[[package]] +name = "xtask" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "duct", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..574568f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[workspace] +resolver = "2" +members = ["xdp-firewall", "xdp-firewall-common", "xtask"] +exclude = ["xdp-firewall-ebpf"] + +[profile.release] +lto = true diff --git a/scripts/setup-veth.sh b/scripts/setup-veth.sh new file mode 100755 index 0000000..5b41659 --- /dev/null +++ b/scripts/setup-veth.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +ip link del veth0 2>/dev/null || true +ip netns del testns 2>/dev/null || true + +ip link add veth0 type veth peer name veth1 +ip addr add 10.200.1.1/24 dev veth0 +ip link set veth0 up + +ip netns add testns +ip link set veth1 netns testns +ip netns exec testns ip addr add 10.200.1.2/24 dev veth1 +ip netns exec testns ip link set veth1 up +ip netns exec testns ip link set lo up + +echo "veth0 (host) = 10.200.1.1" +echo "veth1 (testns) = 10.200.1.2" diff --git a/xdp-firewall-common/Cargo.toml b/xdp-firewall-common/Cargo.toml new file mode 100644 index 0000000..bd98b9b --- /dev/null +++ b/xdp-firewall-common/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "xdp-firewall-common" +version = "0.1.0" +edition = "2021" + +[dependencies] +aya = { version = "0.13.1", optional = true } + +[features] +userspace = ["aya"] diff --git a/xdp-firewall-common/src/lib.rs b/xdp-firewall-common/src/lib.rs new file mode 100644 index 0000000..e21a495 --- /dev/null +++ b/xdp-firewall-common/src/lib.rs @@ -0,0 +1,103 @@ +#![no_std] + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct RuleKey { + pub src_ip: u32, + pub dst_ip: u32, + pub src_port: u16, + pub dst_port: u16, + pub proto: u8, + pub _pad: [u8; 3], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct RuleValue { + pub action: u8, + pub rule_id: u32, + pub _pad: [u8; 3], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct RateLimitKey { + pub key_type: u8, + pub _pad: [u8; 3], + pub src_ip: u32, + pub dst_ip: u32, + pub proto: u8, + pub _pad2: [u8; 3], + pub src_port: u16, + pub dst_port: u16, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct RateLimitState { + pub tokens: u64, + pub last_update_ns: u64, + pub violations: u32, + pub _pad: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct RateLimitConfig { + pub rate_per_sec: u64, + pub burst: u64, + pub ban_threshold: u32, + pub ban_duration_sec: u32, + pub enabled: u8, + pub _pad: [u8; 3], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Stats { + pub packets: u64, + pub bytes: u64, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct FirewallEvent { + pub ts_ns: u64, + pub src_ip: u32, + pub dst_ip: u32, + pub src_port: u16, + pub dst_port: u16, + pub proto: u8, + pub action: u8, + pub rule_id: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Ipv4LpmKey { + pub prefix_len: u32, + pub addr: u32, +} + +pub const ACTION_DROP: u8 = 0; +pub const ACTION_PASS: u8 = 1; +pub const ACTION_LOG: u8 = 2; +pub const ACTION_RATELIMIT_DROP: u8 = 3; +pub const ACTION_BLOCKLIST_DROP: u8 = 4; +pub const ACTION_RANGE_DROP: u8 = 5; + +pub const RL_TYPE_IP: u8 = 0; +pub const RL_TYPE_FLOW: u8 = 1; + +#[cfg(feature = "userspace")] +mod pod { + use super::*; + unsafe impl aya::Pod for RuleKey {} + unsafe impl aya::Pod for RuleValue {} + unsafe impl aya::Pod for RateLimitKey {} + unsafe impl aya::Pod for RateLimitState {} + unsafe impl aya::Pod for RateLimitConfig {} + unsafe impl aya::Pod for Stats {} + unsafe impl aya::Pod for FirewallEvent {} + unsafe impl aya::Pod for Ipv4LpmKey {} +} diff --git a/xdp-firewall-ebpf/.cargo/config.toml b/xdp-firewall-ebpf/.cargo/config.toml new file mode 100644 index 0000000..c668257 --- /dev/null +++ b/xdp-firewall-ebpf/.cargo/config.toml @@ -0,0 +1,3 @@ +[target.bpfel-unknown-none] +linker = "bpf-linker" +rustflags = ["-C", "link-arg=--btf"] diff --git a/xdp-firewall-ebpf/Cargo.lock b/xdp-firewall-ebpf/Cargo.lock new file mode 100644 index 0000000..a7ea603 --- /dev/null +++ b/xdp-firewall-ebpf/Cargo.lock @@ -0,0 +1,346 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aya-build" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59bc42f3c5ddacc34eca28a420b47e3cbb3f0f484137cb2bf1ad2153d0eae52a" +dependencies = [ + "anyhow", + "cargo_metadata", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71ee8e6a617f040d8da7565ec4010aea75e33cda4662f64c019c66ee97d17889" +dependencies = [ + "aya-build", + "aya-ebpf-cty", +] + +[[package]] +name = "aya-ebpf-cty" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f33396742e7fd0f519c1e0de5141d84e1a8df69146a557c08cc222b0ceace4" +dependencies = [ + "aya-build", +] + +[[package]] +name = "aya-ebpf-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96fd02363736177e7e91d6c95d7effbca07be87502c7b5b32fc194aed8b177a0" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a10bbadd0829895a91eb1cd2bb02d7af145704087f03812bed60cb9fe65dbb3" +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", +] + +[[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 = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a0c0e6148f11f01f32650a2ea02d532b2ad4e81d8bd41e6e565b5adc5e6082" +dependencies = [ + "serde", + "serde_core", +] + +[[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", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "network-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f06f1863cb5565864300c6bfb012312969908878d2ca5881eaf0bbdb8b519c23" +dependencies = [ + "memoffset", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[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", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "xdp-firewall-common" +version = "0.1.0" + +[[package]] +name = "xdp-firewall-ebpf" +version = "0.1.0" +dependencies = [ + "aya-ebpf", + "aya-log-ebpf", + "network-types", + "xdp-firewall-common", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/xdp-firewall-ebpf/Cargo.toml b/xdp-firewall-ebpf/Cargo.toml new file mode 100644 index 0000000..8db51b6 --- /dev/null +++ b/xdp-firewall-ebpf/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "xdp-firewall-ebpf" +version = "0.1.0" +edition = "2021" + +[dependencies] +aya-ebpf = "0.1.1" +aya-log-ebpf = "0.1.0" +network-types = "0.1.0" +xdp-firewall-common = { path = "../xdp-firewall-common" } + +[[bin]] +name = "xdp-firewall" +path = "src/main.rs" + +[workspace] diff --git a/xdp-firewall-ebpf/src/main.rs b/xdp-firewall-ebpf/src/main.rs new file mode 100644 index 0000000..6757ac1 --- /dev/null +++ b/xdp-firewall-ebpf/src/main.rs @@ -0,0 +1,257 @@ +#![no_std] +#![no_main] + +use aya_ebpf::{ + bindings::xdp_action::{XDP_ABORTED, XDP_DROP, XDP_PASS}, + macros::{map, xdp}, + maps::{Array, HashMap, LpmTrie, RingBuf}, + maps::lpm_trie::Key as LpmKey, + programs::XdpContext, + helpers::gen::bpf_ktime_get_ns, +}; +use network_types::{ + eth::{EthHdr, EtherType}, + ip::{IpProto, Ipv4Hdr}, + tcp::TcpHdr, + udp::UdpHdr, +}; +use xdp_firewall_common::*; + +#[map(name = "BLOCKLIST")] +static BLOCKLIST: HashMap = HashMap::pinned(1024, 0); + +#[map(name = "IP_RANGES")] +static IP_RANGES: LpmTrie = LpmTrie::pinned(256, 0); + +#[map(name = "RULES")] +static RULES: HashMap = HashMap::pinned(1024, 0); + +#[map(name = "RATE_LIMIT_STATE")] +static RATE_LIMIT_STATE: HashMap = HashMap::pinned(8192, 0); + +#[map(name = "RATE_LIMIT_CONFIG")] +static RATE_LIMIT_CONFIG: HashMap = HashMap::pinned(1, 0); + +#[map(name = "STATS")] +static STATS: HashMap = HashMap::pinned(2048, 0); + +#[map(name = "GLOBAL_STATS")] +static GLOBAL_STATS: Array = Array::pinned(1, 0); + +#[map(name = "EVENTS")] +static EVENTS: RingBuf = RingBuf::pinned(256 * 1024, 0); + +const FIXED_POINT: u64 = 1_000_000; + +#[inline(always)] +fn ptr_at(ctx: &XdpContext, offset: usize) -> Option<*const T> { + let start = ctx.data(); + let end = ctx.data_end(); + if start + offset + core::mem::size_of::() > end { + return None; + } + Some((start + offset) as *const T) +} + +#[inline(always)] +unsafe fn update_stats(rule_id: u32, bytes: u64) { + if let Some(s) = STATS.get_ptr_mut(&rule_id) { + let s = &mut *s; + s.packets += 1; + s.bytes += bytes; + } + if let Some(g) = GLOBAL_STATS.get_ptr_mut(0) { + let g = &mut *g; + g.packets += 1; + g.bytes += bytes; + } +} + +#[inline(always)] +unsafe fn emit_event(_ctx: &XdpContext, action: u8, src_ip: u32, dst_ip: u32, proto: u8, src_port: u16, dst_port: u16, rule_id: u32) { + if let Some(mut entry) = EVENTS.reserve::(0) { + let ev = FirewallEvent { + ts_ns: bpf_ktime_get_ns(), + src_ip, + dst_ip, + src_port, + dst_port, + proto, + action, + rule_id, + }; + entry.write(ev); + entry.submit(0); + } +} + +#[inline(always)] +unsafe fn check_ratelimit(key: &RateLimitKey, bytes: u64, src_ip: u32) -> bool { + let config = match RATE_LIMIT_CONFIG.get(&0u8) { + Some(c) if c.enabled != 0 => c, + _ => return false, + }; + + let now = bpf_ktime_get_ns(); + let state = match RATE_LIMIT_STATE.get_ptr_mut(key) { + Some(s) => { + let s = &mut *s; + let elapsed = now - s.last_update_ns; + let added = elapsed * config.rate_per_sec / 1_000_000_000; + let max_tokens = config.burst; + s.tokens = if s.tokens + added > max_tokens { max_tokens } else { s.tokens + added }; + s.last_update_ns = now; + s + } + None => { + let new = RateLimitState { + tokens: config.burst, + last_update_ns: now, + violations: 0, + _pad: 0, + }; + let _ = RATE_LIMIT_STATE.insert(key, &new, 0); + return false; + } + }; + + if state.tokens >= FIXED_POINT { + state.tokens -= FIXED_POINT; + return false; + } + + state.violations += 1; + update_stats(0xFFFF_FFFF, bytes); + + if state.violations >= config.ban_threshold { + let ban_ns = (config.ban_duration_sec as u64) * 1_000_000_000; + let expiry = now + ban_ns; + let _ = BLOCKLIST.insert(&src_ip, &expiry, 0); + } + + true +} + +#[xdp] +pub fn xdp_firewall(ctx: XdpContext) -> u32 { + match try_xdp_firewall(ctx) { + Ok(ret) => ret, + Err(_) => XDP_ABORTED, + } +} + +fn try_xdp_firewall(ctx: XdpContext) -> Result { + let eth_hdr: *const EthHdr = ptr_at(&ctx, 0).ok_or(())?; + let eth = unsafe { &*eth_hdr }; + + if eth.ether_type != EtherType::Ipv4 as u16 { + return Ok(XDP_PASS); + } + + let ipv4_hdr: *const Ipv4Hdr = ptr_at(&ctx, core::mem::size_of::()).ok_or(())?; + let ipv4 = unsafe { &*ipv4_hdr }; + + let src_ip = u32::from_be_bytes(ipv4.src_addr); + let dst_ip = u32::from_be_bytes(ipv4.dst_addr); + let proto = ipv4.proto as u8; + let ihl = ipv4.ihl() as usize; + let l4_offset = core::mem::size_of::() + ihl; + let pkt_len = (u16::from_be_bytes(ipv4.tot_len) as usize).saturating_sub(ihl); + let bytes = (core::mem::size_of::() + ihl + pkt_len) as u64; + + let (src_port, dst_port) = match ipv4.proto { + IpProto::Tcp => { + let tcp: *const TcpHdr = ptr_at(&ctx, l4_offset).ok_or(())?; + let t = unsafe { &*tcp }; + (u16::from_be_bytes(t.source), u16::from_be_bytes(t.dest)) + } + IpProto::Udp => { + let udp: *const UdpHdr = ptr_at(&ctx, l4_offset).ok_or(())?; + let u = unsafe { &*udp }; + (u16::from_be_bytes(u.src), u16::from_be_bytes(u.dst)) + } + _ => (0, 0), + }; + + let now = unsafe { bpf_ktime_get_ns() }; + + unsafe { + if let Some(expiry) = BLOCKLIST.get(&src_ip) { + if now < *expiry { + update_stats(0xFFFF_FFFE, bytes); + emit_event(&ctx, ACTION_BLOCKLIST_DROP, src_ip, dst_ip, proto, src_port, dst_port, 0); + return Ok(XDP_DROP); + } + } + + let lpm_key = LpmKey::new(32, src_ip.to_be()); + if let Some(action) = IP_RANGES.get(&lpm_key) { + if *action == ACTION_DROP { + update_stats(0xFFFF_FFFD, bytes); + emit_event(&ctx, ACTION_RANGE_DROP, src_ip, dst_ip, proto, src_port, dst_port, 0); + return Ok(XDP_DROP); + } + } + + let rl_ip_key = RateLimitKey { + key_type: RL_TYPE_IP, + _pad: [0; 3], + src_ip, + dst_ip: 0, + proto: 0, + _pad2: [0; 3], + src_port: 0, + dst_port: 0, + }; + if check_ratelimit(&rl_ip_key, bytes, src_ip) { + emit_event(&ctx, ACTION_RATELIMIT_DROP, src_ip, dst_ip, proto, src_port, dst_port, 0); + return Ok(XDP_DROP); + } + + let rl_flow_key = RateLimitKey { + key_type: RL_TYPE_FLOW, + _pad: [0; 3], + src_ip, + dst_ip, + proto, + _pad2: [0; 3], + src_port, + dst_port, + }; + if check_ratelimit(&rl_flow_key, bytes, src_ip) { + emit_event(&ctx, ACTION_RATELIMIT_DROP, src_ip, dst_ip, proto, src_port, dst_port, 0); + return Ok(XDP_DROP); + } + + let rule_key = RuleKey { + src_ip, + dst_ip, + src_port, + dst_port, + proto, + _pad: [0; 3], + }; + + if let Some(rule) = RULES.get(&rule_key) { + update_stats(rule.rule_id, bytes); + match rule.action { + ACTION_DROP => { + emit_event(&ctx, ACTION_DROP, src_ip, dst_ip, proto, src_port, dst_port, rule.rule_id); + Ok(XDP_DROP) + } + ACTION_LOG => { + emit_event(&ctx, ACTION_LOG, src_ip, dst_ip, proto, src_port, dst_port, rule.rule_id); + Ok(XDP_PASS) + } + _ => Ok(XDP_PASS), + } + } else { + Ok(XDP_PASS) + } + } +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { core::hint::unreachable_unchecked() } +} diff --git a/xdp-firewall/Cargo.toml b/xdp-firewall/Cargo.toml new file mode 100644 index 0000000..2da3c5f --- /dev/null +++ b/xdp-firewall/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "xdp-firewall" +version = "0.1.0" +edition = "2021" + +[dependencies] +aya = { version = "0.13.1", features = ["async_tokio"] } +aya-log = "0.2.1" +clap = { version = "4.5", features = ["derive"] } +tokio = { version = "1.44", features = ["macros", "rt", "rt-multi-thread", "time", "sync", "signal"] } +ratatui = "0.29" +crossterm = { version = "0.29", features = ["event-stream"] } +anyhow = "1.0" +thiserror = "2.0" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +ipnetwork = "0.21" +xdp-firewall-common = { path = "../xdp-firewall-common", features = ["userspace"] } + +[build-dependencies] +duct = "0.13" diff --git a/xdp-firewall/build.rs b/xdp-firewall/build.rs new file mode 100644 index 0000000..1f8ebfb --- /dev/null +++ b/xdp-firewall/build.rs @@ -0,0 +1,22 @@ +use std::{env, path::PathBuf}; + +fn main() { + let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + let root = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()) + .parent() + .unwrap() + .to_path_buf(); + let ebpf_target = root.join("target/bpfel-unknown-none/release/xdp-firewall"); + + if !ebpf_target.exists() { + let status = std::process::Command::new("cargo") + .args(["run", "--package", "xtask", "--", "build-ebpf"]) + .current_dir(&root) + .status() + .expect("failed to build eBPF program"); + assert!(status.success(), "eBPF build failed"); + } + + std::fs::copy(&ebpf_target, out_dir.join("xdp-firewall.o")).unwrap(); + println!("cargo:rerun-if-changed={}", ebpf_target.display()); +} diff --git a/xdp-firewall/src/cli.rs b/xdp-firewall/src/cli.rs new file mode 100644 index 0000000..4949ae8 --- /dev/null +++ b/xdp-firewall/src/cli.rs @@ -0,0 +1,118 @@ +use clap::{Parser, Subcommand, ValueEnum}; + +#[derive(Parser)] +#[command(name = "xdp-firewall")] +#[command(about = "High-performance XDP firewall with real-time monitoring")] +pub struct Cli { + #[command(subcommand)] + pub command: Commands, +} + +#[derive(Subcommand)] +pub enum Commands { + Load { + #[arg(short, long)] + iface: String, + #[arg(short, long)] + config: Option, + }, + Unload { + #[arg(short, long)] + iface: String, + }, + Monitor { + #[arg(short, long)] + iface: String, + }, + Block { + #[arg(short, long)] + iface: String, + #[arg(short, long)] + ip: String, + #[arg(short, long, default_value = "0")] + duration: u64, + }, + Unblock { + #[arg(short, long)] + iface: String, + #[arg(short, long)] + ip: String, + }, + AddRule { + #[arg(short, long)] + iface: String, + #[arg(long, default_value = "0.0.0.0")] + src_ip: String, + #[arg(long, default_value = "0.0.0.0")] + dst_ip: String, + #[arg(long, default_value = "0")] + proto: u8, + #[arg(long, default_value = "0")] + src_port: u16, + #[arg(long, default_value = "0")] + dst_port: u16, + #[arg(short, long, value_enum)] + action: ActionArg, + }, + DelRule { + #[arg(short, long)] + iface: String, + #[arg(short, long)] + id: u32, + }, + ListRules { + #[arg(short, long)] + iface: String, + }, + AddRange { + #[arg(short, long)] + iface: String, + #[arg(short, long)] + cidr: String, + #[arg(short, long, value_enum, default_value = "drop")] + action: ActionArg, + }, + DelRange { + #[arg(short, long)] + iface: String, + #[arg(short, long)] + cidr: String, + }, + SetRatelimit { + #[arg(short, long)] + iface: String, + #[arg(long)] + rate: u64, + #[arg(long)] + burst: u64, + #[arg(long)] + ban_threshold: u32, + #[arg(long)] + ban_duration: u32, + #[arg(long, value_enum)] + mode: RatelimitModeArg, + }, +} + +#[derive(Clone, Copy, Debug, ValueEnum)] +pub enum ActionArg { + Drop, + Pass, + Log, +} + +impl ActionArg { + pub fn as_u8(&self) -> u8 { + match self { + ActionArg::Drop => xdp_firewall_common::ACTION_DROP, + ActionArg::Pass => xdp_firewall_common::ACTION_PASS, + ActionArg::Log => xdp_firewall_common::ACTION_LOG, + } + } +} + +#[derive(Clone, Copy, Debug, ValueEnum)] +pub enum RatelimitModeArg { + Ip, + Flow, +} diff --git a/xdp-firewall/src/firewall.rs b/xdp-firewall/src/firewall.rs new file mode 100644 index 0000000..9caa81b --- /dev/null +++ b/xdp-firewall/src/firewall.rs @@ -0,0 +1,108 @@ +use std::process::Command; + +use aya::{ + maps::{Array, HashMap, lpm_trie::LpmTrie, MapData, RingBuf}, + programs::{Xdp, xdp::XdpFlags}, + EbpfLoader, +}; +use aya_log::EbpfLogger; + +use xdp_firewall_common::*; + +pub struct FirewallHandle { + pub bpf: aya::Ebpf, +} + +const BPF_ELF: &[u8] = include_bytes!("../../target/bpfel-unknown-none/release/xdp-firewall"); + +const PIN_PATH: &str = "/sys/fs/bpf/xdp-firewall"; + +impl FirewallHandle { + pub fn load(iface: &str) -> anyhow::Result { + eprintln!("Step 1: create_dir_all {}", PIN_PATH); + std::fs::create_dir_all(PIN_PATH)?; + eprintln!("Step 2: EbpfLoader::load"); + let elf_bytes = BPF_ELF.to_vec(); + let mut bpf = EbpfLoader::new() + .map_pin_path(PIN_PATH) + .load(&elf_bytes) + .map_err(|e| { + eprintln!("EbpfLoader::load failed: {:?}", e); + e + })?; + let _ = EbpfLogger::init(&mut bpf); + + let prog: &mut Xdp = bpf.program_mut("xdp_firewall").unwrap().try_into()?; + prog.load().map_err(|e| { + eprintln!("prog.load() failed: {:?}", e); + e + })?; + prog.attach(iface, XdpFlags::default()).map_err(|e| { + eprintln!("prog.attach() failed: {:?}", e); + e + })?; + + let mut global: Array<_, Stats> = Array::try_from(bpf.map_mut("GLOBAL_STATS").unwrap())?; + global.set(0, Stats { packets: 0, bytes: 0 }, 0)?; + + Ok(Self { bpf }) + } +} + +pub fn unload(iface: &str) -> anyhow::Result<()> { + let status = Command::new("ip") + .args(["link", "set", "dev", iface, "xdp", "off"]) + .status()?; + if !status.success() { + anyhow::bail!("failed to detach XDP program from {}", iface); + } + Ok(()) +} + +pub fn open_blocklist() -> anyhow::Result> { + let data = MapData::from_pin(format!("{}/BLOCKLIST", PIN_PATH))?; + let map = aya::maps::Map::HashMap(data); + Ok(HashMap::try_from(map)?) +} + +pub fn open_ip_ranges() -> anyhow::Result> { + let data = MapData::from_pin(format!("{}/IP_RANGES", PIN_PATH))?; + let map = aya::maps::Map::LpmTrie(data); + Ok(LpmTrie::try_from(map)?) +} + +pub fn open_rules() -> anyhow::Result> { + let data = MapData::from_pin(format!("{}/RULES", PIN_PATH))?; + let map = aya::maps::Map::HashMap(data); + Ok(HashMap::try_from(map)?) +} + +pub fn open_rate_limit_state() -> anyhow::Result> { + let data = MapData::from_pin(format!("{}/RATE_LIMIT_STATE", PIN_PATH))?; + let map = aya::maps::Map::HashMap(data); + Ok(HashMap::try_from(map)?) +} + +pub fn open_rate_limit_config() -> anyhow::Result> { + let data = MapData::from_pin(format!("{}/RATE_LIMIT_CONFIG", PIN_PATH))?; + let map = aya::maps::Map::HashMap(data); + Ok(HashMap::try_from(map)?) +} + +pub fn open_stats() -> anyhow::Result> { + let data = MapData::from_pin(format!("{}/STATS", PIN_PATH))?; + let map = aya::maps::Map::HashMap(data); + Ok(HashMap::try_from(map)?) +} + +pub fn open_global_stats() -> anyhow::Result> { + let data = MapData::from_pin(format!("{}/GLOBAL_STATS", PIN_PATH))?; + let map = aya::maps::Map::Array(data); + Ok(Array::try_from(map)?) +} + +pub fn open_events() -> anyhow::Result> { + let data = MapData::from_pin(format!("{}/EVENTS", PIN_PATH))?; + let map = aya::maps::Map::RingBuf(data); + Ok(RingBuf::try_from(map)?) +} diff --git a/xdp-firewall/src/logger.rs b/xdp-firewall/src/logger.rs new file mode 100644 index 0000000..fe875a9 --- /dev/null +++ b/xdp-firewall/src/logger.rs @@ -0,0 +1,24 @@ +use std::borrow::Borrow; + +use aya::maps::MapData; +use tokio::io::unix::AsyncFd; + +use xdp_firewall_common::FirewallEvent; + +pub async fn run_logger>( + mut ring_buf: aya::maps::RingBuf, + tx: tokio::sync::mpsc::Sender, +) -> anyhow::Result<()> { + let mut async_fd = AsyncFd::new(ring_buf)?; + loop { + while let Some(item) = async_fd.get_mut().next() { + let data: &[u8] = &item; + if data.len() >= core::mem::size_of::() { + let ev = unsafe { *(data.as_ptr() as *const FirewallEvent) }; + let _ = tx.try_send(ev); + } + } + let mut guard = async_fd.readable().await?; + guard.clear_ready(); + } +} diff --git a/xdp-firewall/src/main.rs b/xdp-firewall/src/main.rs new file mode 100644 index 0000000..de0fcb5 --- /dev/null +++ b/xdp-firewall/src/main.rs @@ -0,0 +1,159 @@ +mod cli; +mod firewall; +mod logger; +mod maps; +mod monitor; +mod stats; + +use aya::maps::lpm_trie::Key as LpmKey; +use clap::Parser; +use xdp_firewall_common::*; + +use crate::cli::{ActionArg, Cli, Commands, RatelimitModeArg}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + let cli = Cli::parse(); + + match cli.command { + Commands::Load { iface, .. } => { + let _handle = firewall::FirewallHandle::load(&iface)?; + tracing::info!("XDP firewall loaded on {}", iface); + tokio::signal::ctrl_c().await?; + } + Commands::Unload { iface } => { + firewall::unload(&iface)?; + tracing::info!("XDP firewall unloaded from {}", iface); + } + Commands::Monitor { iface } => { + monitor::run(&iface).await?; + } + Commands::Block { ip, duration, .. } => { + let ip = maps::parse_ipv4(&ip)?; + let mut blocklist = firewall::open_blocklist()?; + let expiry = if duration == 0 { + u64::MAX + } else { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs() + + duration + }; + blocklist.insert(ip, expiry, 0)?; + println!("Blocked {} until epoch {}", maps::fmt_ipv4(ip), expiry); + } + Commands::Unblock { ip, .. } => { + let ip = maps::parse_ipv4(&ip)?; + let mut blocklist = firewall::open_blocklist()?; + blocklist.remove(&ip)?; + println!("Unblocked {}", maps::fmt_ipv4(ip)); + } + Commands::AddRule { src_ip, dst_ip, proto, src_port, dst_port, action, .. } => { + let mut rules = firewall::open_rules()?; + let mut stats = firewall::open_stats()?; + let rule_id = stats.iter().count() as u32; + let key = RuleKey { + src_ip: maps::parse_ipv4(&src_ip)?, + dst_ip: maps::parse_ipv4(&dst_ip)?, + src_port, + dst_port, + proto, + _pad: [0; 3], + }; + let value = RuleValue { + action: action.as_u8(), + rule_id, + _pad: [0; 3], + }; + rules.insert(key, value, 0)?; + stats.insert(rule_id, Stats::default(), 0)?; + println!("Added rule id={}", rule_id); + } + Commands::DelRule { id, .. } => { + let mut rules = firewall::open_rules()?; + let mut stats = firewall::open_stats()?; + let target = rules.iter().find(|r| { + if let Ok((_, v)) = r { + v.rule_id == id + } else { + false + } + }); + if let Some(Ok((k, _))) = target { + let key = k; + rules.remove(&key)?; + let _ = stats.remove(&id); + println!("Deleted rule id={}", id); + } else { + println!("Rule id={} not found", id); + } + } + Commands::ListRules { .. } => { + let rules = firewall::open_rules()?; + let stats = firewall::open_stats()?; + println!("{:<5} {:<15} {:<15} {:<6} {:<6} {:<6} {:<10} {:<12} {:<12}", + "ID", "SRC_IP", "DST_IP", "PROTO", "SPORT", "DPORT", "ACTION", "PACKETS", "BYTES"); + for item in rules.iter() { + let (k, v) = item?; + let s = stats.get(&v.rule_id, 0).unwrap_or_default(); + let action = match v.action { + ACTION_DROP => "drop", + ACTION_PASS => "pass", + ACTION_LOG => "log", + _ => "?", + }; + println!("{:<5} {:<15} {:<15} {:<6} {:<6} {:<6} {:<10} {:<12} {:<12}", + v.rule_id, + if k.src_ip == 0 { "*".to_string() } else { maps::fmt_ipv4(k.src_ip) }, + if k.dst_ip == 0 { "*".to_string() } else { maps::fmt_ipv4(k.dst_ip) }, + k.proto, + if k.src_port == 0 { "*".to_string() } else { k.src_port.to_string() }, + if k.dst_port == 0 { "*".to_string() } else { k.dst_port.to_string() }, + action, + s.packets, + s.bytes, + ); + } + } + Commands::AddRange { cidr, action, .. } => { + let net = cidr.parse::()?; + let prefix = net.prefix(); + let addr = u32::from_be_bytes(net.network().octets()); + let mut ranges = firewall::open_ip_ranges()?; + let key = LpmKey::new(prefix.into(), addr.to_be()); + ranges.insert(&key, action.as_u8(), 0)?; + println!("Added range {}/{} -> {:?}", maps::fmt_ipv4(addr), prefix, action); + } + Commands::DelRange { cidr, .. } => { + let net = cidr.parse::()?; + let prefix = net.prefix(); + let addr = u32::from_be_bytes(net.network().octets()); + let mut ranges = firewall::open_ip_ranges()?; + let key = LpmKey::new(prefix.into(), addr.to_be()); + ranges.remove(&key)?; + println!("Deleted range {}/{}", maps::fmt_ipv4(addr), prefix); + } + Commands::SetRatelimit { rate, burst, ban_threshold, ban_duration, mode, .. } => { + let mut config_map = firewall::open_rate_limit_config()?; + let fixed_rate = rate * 1_000_000; + let fixed_burst = burst * 1_000_000; + let config = RateLimitConfig { + rate_per_sec: fixed_rate, + burst: fixed_burst, + ban_threshold, + ban_duration_sec: ban_duration, + enabled: 1, + _pad: [0; 3], + }; + let key = match mode { + RatelimitModeArg::Ip => RL_TYPE_IP, + RatelimitModeArg::Flow => RL_TYPE_FLOW, + }; + config_map.insert(key, config, 0)?; + println!("Set rate limit: rate={}pps burst={} mode={:?}", rate, burst, mode); + } + } + + Ok(()) +} diff --git a/xdp-firewall/src/maps.rs b/xdp-firewall/src/maps.rs new file mode 100644 index 0000000..e21271b --- /dev/null +++ b/xdp-firewall/src/maps.rs @@ -0,0 +1,55 @@ +use aya::{ + maps::{Array, HashMap, lpm_trie::{LpmTrie, Key as LpmKey}, RingBuf}, + Ebpf, +}; +use xdp_firewall_common::*; + +pub type BpfBlocklist<'a> = HashMap<&'a mut aya::maps::MapData, u32, u64>; +pub type BpfIpRanges<'a> = LpmTrie<&'a mut aya::maps::MapData, u32, u8>; +pub type BpfRules<'a> = HashMap<&'a mut aya::maps::MapData, RuleKey, RuleValue>; +pub type BpfRateLimitState<'a> = HashMap<&'a mut aya::maps::MapData, RateLimitKey, RateLimitState>; +pub type BpfRateLimitConfig<'a> = HashMap<&'a mut aya::maps::MapData, u8, RateLimitConfig>; +pub type BpfStats<'a> = HashMap<&'a mut aya::maps::MapData, u32, Stats>; +pub type BpfGlobalStats<'a> = Array<&'a mut aya::maps::MapData, Stats>; +pub type BpfEvents<'a> = RingBuf<&'a mut aya::maps::MapData>; + +pub fn blocklist(bpf: &mut Ebpf) -> anyhow::Result { + Ok(HashMap::try_from(bpf.map_mut("BLOCKLIST").unwrap())?) +} + +pub fn ip_ranges(bpf: &mut Ebpf) -> anyhow::Result { + Ok(LpmTrie::try_from(bpf.map_mut("IP_RANGES").unwrap())?) +} + +pub fn rules(bpf: &mut Ebpf) -> anyhow::Result { + Ok(HashMap::try_from(bpf.map_mut("RULES").unwrap())?) +} + +pub fn rate_limit_state(bpf: &mut Ebpf) -> anyhow::Result { + Ok(HashMap::try_from(bpf.map_mut("RATE_LIMIT_STATE").unwrap())?) +} + +pub fn rate_limit_config(bpf: &mut Ebpf) -> anyhow::Result { + Ok(HashMap::try_from(bpf.map_mut("RATE_LIMIT_CONFIG").unwrap())?) +} + +pub fn stats(bpf: &mut Ebpf) -> anyhow::Result { + Ok(HashMap::try_from(bpf.map_mut("STATS").unwrap())?) +} + +pub fn global_stats(bpf: &mut Ebpf) -> anyhow::Result { + Ok(Array::try_from(bpf.map_mut("GLOBAL_STATS").unwrap())?) +} + +pub fn events(bpf: &mut Ebpf) -> anyhow::Result { + Ok(RingBuf::try_from(bpf.map_mut("EVENTS").unwrap())?) +} + +pub fn parse_ipv4(s: &str) -> anyhow::Result { + let addr: std::net::Ipv4Addr = s.parse()?; + Ok(u32::from_be_bytes(addr.octets())) +} + +pub fn fmt_ipv4(ip: u32) -> String { + std::net::Ipv4Addr::from(ip).to_string() +} diff --git a/xdp-firewall/src/monitor/app.rs b/xdp-firewall/src/monitor/app.rs new file mode 100644 index 0000000..265ef3d --- /dev/null +++ b/xdp-firewall/src/monitor/app.rs @@ -0,0 +1,84 @@ +use std::collections::VecDeque; + +use xdp_firewall_common::{FirewallEvent, RuleKey, RuleValue, Stats}; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Pane { + Rules, + Blocklist, + Logs, +} + +pub struct App { + pub iface: String, + pub quit: bool, + pub pane: Pane, + pub global_stats: Stats, + pub prev_global_stats: Stats, + pub rules: Vec<(RuleKey, RuleValue, Stats)>, + pub blocklist: Vec<(u32, u64)>, + pub logs: VecDeque, + pub max_logs: usize, + pub pps: u64, + pub bps: u64, + pub dialog: Dialog, + pub rules_selected: usize, + pub blocklist_selected: usize, +} + +#[derive(Clone, Default, PartialEq, Eq)] +pub enum Dialog { + #[default] + None, + BlockIp { input: String }, + AddRule { src_ip: String, dst_ip: String, proto: String, src_port: String, dst_port: String, action: String }, + ConfirmDelete, +} + +impl App { + pub fn new(iface: String) -> Self { + Self { + iface, + quit: false, + pane: Pane::Rules, + global_stats: Stats::default(), + prev_global_stats: Stats::default(), + rules: Vec::new(), + blocklist: Vec::new(), + logs: VecDeque::new(), + max_logs: 100, + pps: 0, + bps: 0, + dialog: Dialog::None, + rules_selected: 0, + blocklist_selected: 0, + } + } + + pub fn on_tick(&mut self, global: Stats, rules: Vec<(RuleKey, RuleValue, Stats)>, blocklist: Vec<(u32, u64)>) { + let elapsed = 0.1; + let pkts = global.packets.saturating_sub(self.prev_global_stats.packets); + let bytes = global.bytes.saturating_sub(self.prev_global_stats.bytes); + self.pps = (pkts as f64 / elapsed) as u64; + self.bps = (bytes as f64 / elapsed) as u64; + self.prev_global_stats = self.global_stats; + self.global_stats = global; + self.rules = rules; + self.blocklist = blocklist; + } + + pub fn add_log(&mut self, ev: FirewallEvent) { + if self.logs.len() >= self.max_logs { + self.logs.pop_back(); + } + self.logs.push_front(ev); + } + + pub fn selected_rule_id(&self) -> Option { + self.rules.get(self.rules_selected).map(|(_, v, _)| v.rule_id) + } + + pub fn selected_block_ip(&self) -> Option { + self.blocklist.get(self.blocklist_selected).map(|(ip, _)| *ip) + } +} diff --git a/xdp-firewall/src/monitor/mod.rs b/xdp-firewall/src/monitor/mod.rs new file mode 100644 index 0000000..b07f2d3 --- /dev/null +++ b/xdp-firewall/src/monitor/mod.rs @@ -0,0 +1,260 @@ +mod app; +mod ui; + +use std::{ + io, + time::{Duration, Instant}, +}; + +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::{ + backend::{Backend, CrosstermBackend}, + Terminal, +}; +use tokio::sync::mpsc; + +use crate::{ + firewall, logger, maps, + monitor::app::{App, Dialog, Pane}, +}; +use xdp_firewall_common::{FirewallEvent, Stats}; + +pub async fn run(iface: &str) -> anyhow::Result<()> { + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let res = run_app(&mut terminal, iface.to_string()).await; + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + res +} + +async fn run_app(terminal: &mut Terminal, iface: String) -> anyhow::Result<()> { + let mut app = App::new(iface.clone()); + let (log_tx, mut log_rx) = mpsc::channel::(1024); + + let ring_buf = firewall::open_events()?; + tokio::spawn(async move { + let _ = logger::run_logger(ring_buf, log_tx).await; + }); + + let mut last_tick = Instant::now(); + let tick_rate = Duration::from_millis(100); + + loop { + let timeout = tick_rate.saturating_sub(last_tick.elapsed()); + if crossterm::event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press { + handle_input(&mut app, key.code); + } + } + } + + while let Ok(ev) = log_rx.try_recv() { + app.add_log(ev); + } + + if last_tick.elapsed() >= tick_rate { + let (global, rules, blocklist) = poll_maps()?; + app.on_tick(global, rules, blocklist); + last_tick = Instant::now(); + } + + terminal.draw(|f| ui::draw(f, &app))?; + + if app.quit { + return Ok(()); + } + } +} + +fn poll_maps() -> anyhow::Result<(Stats, Vec<(xdp_firewall_common::RuleKey, xdp_firewall_common::RuleValue, Stats)>, Vec<(u32, u64)>)> { + let global = firewall::open_global_stats()?.get(&0, 0).unwrap_or_default(); + let rules_map = firewall::open_rules()?; + let stats_map = firewall::open_stats()?; + let mut rules = Vec::new(); + for item in rules_map.iter() { + let (k, v) = item?; + let s = stats_map.get(&v.rule_id, 0).unwrap_or_default(); + rules.push((k, v, s)); + } + let blocklist_map = firewall::open_blocklist()?; + let mut blocklist = Vec::new(); + for item in blocklist_map.iter() { + let (k, v) = item?; + blocklist.push((k, v)); + } + Ok((global, rules, blocklist)) +} + +fn handle_input(app: &mut App, key: KeyCode) { + match &mut app.dialog { + Dialog::None => match key { + KeyCode::Char('q') => app.quit = true, + KeyCode::Tab => { + app.pane = match app.pane { + Pane::Rules => Pane::Blocklist, + Pane::Blocklist => Pane::Logs, + Pane::Logs => Pane::Rules, + }; + } + KeyCode::Down => match app.pane { + Pane::Rules => { + if app.rules_selected + 1 < app.rules.len() { + app.rules_selected += 1; + } + } + Pane::Blocklist => { + if app.blocklist_selected + 1 < app.blocklist.len() { + app.blocklist_selected += 1; + } + } + _ => {} + }, + KeyCode::Up => match app.pane { + Pane::Rules => { + if app.rules_selected > 0 { + app.rules_selected -= 1; + } + } + Pane::Blocklist => { + if app.blocklist_selected > 0 { + app.blocklist_selected -= 1; + } + } + _ => {} + }, + KeyCode::Char('b') => { + app.dialog = Dialog::BlockIp { input: String::new() }; + } + KeyCode::Char('u') => { + if app.pane == Pane::Blocklist { + if let Some(ip) = app.selected_block_ip() { + let _ = unblock_ip(ip); + } + } + } + KeyCode::Char('a') => { + app.dialog = Dialog::AddRule { + src_ip: "0.0.0.0".into(), + dst_ip: "0.0.0.0".into(), + proto: "0".into(), + src_port: "0".into(), + dst_port: "0".into(), + action: "drop".into(), + }; + } + KeyCode::Char('d') => { + if app.pane == Pane::Rules && !app.rules.is_empty() { + app.dialog = Dialog::ConfirmDelete; + } + } + _ => {} + }, + Dialog::BlockIp { input } => match key { + KeyCode::Enter => { + if !input.is_empty() { + if let Ok(ip) = input.parse::() { + let _ = block_ip(u32::from_be_bytes(ip.octets())); + } + } + app.dialog = Dialog::None; + } + KeyCode::Esc => app.dialog = Dialog::None, + KeyCode::Char(c) => input.push(c), + KeyCode::Backspace => { input.pop(); } + _ => {} + }, + Dialog::AddRule { src_ip, dst_ip, proto, src_port, dst_port, action } => match key { + KeyCode::Enter => { + let _ = add_rule_from_dialog(src_ip, dst_ip, proto, src_port, dst_port, action); + app.dialog = Dialog::None; + } + KeyCode::Esc => app.dialog = Dialog::None, + _ => {} + }, + Dialog::ConfirmDelete => match key { + KeyCode::Enter => { + if let Some(id) = app.selected_rule_id() { + let _ = delete_rule(id); + } + app.dialog = Dialog::None; + } + KeyCode::Esc => app.dialog = Dialog::None, + _ => {} + }, + } +} + +fn block_ip(ip: u32) -> anyhow::Result<()> { + let mut blocklist = firewall::open_blocklist()?; + blocklist.insert(ip, u64::MAX, 0)?; + Ok(()) +} + +fn unblock_ip(ip: u32) -> anyhow::Result<()> { + let mut blocklist = firewall::open_blocklist()?; + blocklist.remove(&ip)?; + Ok(()) +} + +fn add_rule_from_dialog(src_ip: &str, dst_ip: &str, proto: &str, src_port: &str, dst_port: &str, action: &str) -> anyhow::Result<()> { + use xdp_firewall_common::*; + let mut rules = firewall::open_rules()?; + let mut stats = firewall::open_stats()?; + let rule_id = stats.iter().count() as u32; + let key = RuleKey { + src_ip: maps::parse_ipv4(src_ip)?, + dst_ip: maps::parse_ipv4(dst_ip)?, + src_port: src_port.parse()?, + dst_port: dst_port.parse()?, + proto: proto.parse()?, + _pad: [0; 3], + }; + let action_u8 = match action { + "pass" => ACTION_PASS, + "log" => ACTION_LOG, + _ => ACTION_DROP, + }; + let value = RuleValue { + action: action_u8, + rule_id, + _pad: [0; 3], + }; + rules.insert(key, value, 0)?; + stats.insert(rule_id, Stats::default(), 0)?; + Ok(()) +} + +fn delete_rule(id: u32) -> anyhow::Result<()> { + let mut rules = firewall::open_rules()?; + let mut stats = firewall::open_stats()?; + let target = rules.iter().find(|r| { + if let Ok((_, v)) = r { + v.rule_id == id + } else { + false + } + }); + if let Some(Ok((k, _))) = target { + let key = k; + rules.remove(&key)?; + let _ = stats.remove(&id); + } + Ok(()) +} diff --git a/xdp-firewall/src/monitor/ui.rs b/xdp-firewall/src/monitor/ui.rs new file mode 100644 index 0000000..49fa8fd --- /dev/null +++ b/xdp-firewall/src/monitor/ui.rs @@ -0,0 +1,203 @@ +use ratatui::{ + layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span, Text}, + widgets::{Block, Borders, Cell, Clear, Paragraph, Row, Table, Wrap}, + Frame, +}; + +use crate::maps::fmt_ipv4; +use crate::monitor::app::{App, Dialog, Pane}; +use xdp_firewall_common::*; + +pub fn draw(f: &mut Frame, app: &App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(3), Constraint::Min(0), Constraint::Length(1)]) + .split(f.area()); + + draw_header(f, app, chunks[0]); + draw_main(f, app, chunks[1]); + draw_footer(f, app, chunks[2]); + + if app.dialog != Dialog::None { + draw_dialog(f, app); + } +} + +fn draw_header(f: &mut Frame, app: &App, area: Rect) { + let header = Paragraph::new(format!( + "Interface: {} | Global Packets: {} | Global Bytes: {} | PPS: {} | BPS: {}", + app.iface, app.global_stats.packets, app.global_stats.bytes, app.pps, app.bps + )) + .block(Block::default().borders(Borders::ALL).title("XDP Firewall Monitor")) + .alignment(Alignment::Center); + f.render_widget(header, area); +} + +fn draw_main(f: &mut Frame, app: &App, area: Rect) { + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(area); + + draw_rules(f, app, chunks[0]); + draw_right(f, app, chunks[1]); +} + +fn draw_rules(f: &mut Frame, app: &App, area: Rect) { + let border_style = if app.pane == Pane::Rules { + Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD) + } else { + Style::default() + }; + let block = Block::default().borders(Borders::ALL).title("Rules").border_style(border_style); + let header = Row::new(vec!["ID", "SRC", "DST", "P", "SP", "DP", "ACT", "PKTS", "BYTES"]) + .style(Style::default().fg(Color::Yellow)); + let rows: Vec = app.rules.iter().enumerate().map(|(i, (k, v, s))| { + let style = if app.pane == Pane::Rules && i == app.rules_selected { + Style::default().bg(Color::DarkGray) + } else { + Style::default() + }; + let action = match v.action { + ACTION_DROP => "DR", + ACTION_PASS => "PS", + ACTION_LOG => "LG", + _ => "?", + }; + Row::new(vec![ + Cell::from(v.rule_id.to_string()), + Cell::from(if k.src_ip == 0 { "*".into() } else { fmt_ipv4(k.src_ip) }), + Cell::from(if k.dst_ip == 0 { "*".into() } else { fmt_ipv4(k.dst_ip) }), + Cell::from(k.proto.to_string()), + Cell::from(if k.src_port == 0 { "*".into() } else { k.src_port.to_string() }), + Cell::from(if k.dst_port == 0 { "*".into() } else { k.dst_port.to_string() }), + Cell::from(action), + Cell::from(s.packets.to_string()), + Cell::from(s.bytes.to_string()), + ]).style(style) + }).collect(); + let table = Table::new(rows, [ + Constraint::Length(4), + Constraint::Length(15), + Constraint::Length(15), + Constraint::Length(4), + Constraint::Length(6), + Constraint::Length(6), + Constraint::Length(5), + Constraint::Length(10), + Constraint::Length(10), + ]) + .header(header) + .block(block); + f.render_widget(table, area); +} + +fn draw_right(f: &mut Frame, app: &App, area: Rect) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(40), Constraint::Percentage(60)]) + .split(area); + + draw_blocklist(f, app, chunks[0]); + draw_logs(f, app, chunks[1]); +} + +fn draw_blocklist(f: &mut Frame, app: &App, area: Rect) { + let border_style = if app.pane == Pane::Blocklist { + Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD) + } else { + Style::default() + }; + let block = Block::default().borders(Borders::ALL).title("Blocklist").border_style(border_style); + let rows: Vec = app.blocklist.iter().enumerate().map(|(i, (ip, expiry))| { + let style = if app.pane == Pane::Blocklist && i == app.blocklist_selected { + Style::default().bg(Color::DarkGray) + } else { + Style::default() + }; + let exp = if *expiry == u64::MAX { + "permanent".into() + } else { + expiry.to_string() + }; + Row::new(vec![fmt_ipv4(*ip), exp]).style(style) + }).collect(); + let table = Table::new(rows, [Constraint::Length(16), Constraint::Min(0)]) + .header(Row::new(vec!["IP", "Expiry"]).style(Style::default().fg(Color::Yellow))) + .block(block); + f.render_widget(table, area); +} + +fn draw_logs(f: &mut Frame, app: &App, area: Rect) { + let border_style = if app.pane == Pane::Logs { + Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD) + } else { + Style::default() + }; + let block = Block::default().borders(Borders::ALL).title("Logs").border_style(border_style); + let lines: Vec = app.logs.iter().map(|ev| { + let action = match ev.action { + ACTION_DROP => "DROP", + ACTION_PASS => "PASS", + ACTION_LOG => "LOG", + ACTION_RATELIMIT_DROP => "RL_DROP", + ACTION_BLOCKLIST_DROP => "BL_DROP", + ACTION_RANGE_DROP => "RNG_DROP", + _ => "?", + }; + Line::from(vec![ + Span::raw(format!("{} ", action)), + Span::raw(format!("{}:{} -> {}:{} proto={}", + fmt_ipv4(ev.src_ip), ev.src_port, + fmt_ipv4(ev.dst_ip), ev.dst_port, ev.proto)), + ]) + }).collect(); + let para = Paragraph::new(Text::from(lines)).block(block).wrap(Wrap { trim: true }); + f.render_widget(para, area); +} + +fn draw_footer(f: &mut Frame, _app: &App, area: Rect) { + let text = "q:quit | Tab:switch | b:block | u:unblock | a:add-rule | d:del-rule"; + let para = Paragraph::new(text).alignment(Alignment::Center); + f.render_widget(para, area); +} + +fn draw_dialog(f: &mut Frame, app: &App) { + let area = centered_rect(60, 40, f.area()); + f.render_widget(Clear, area); + match &app.dialog { + Dialog::BlockIp { input } => { + let block = Block::default().borders(Borders::ALL).title("Block IP"); + let para = Paragraph::new(format!("IP: {}", input)).block(block); + f.render_widget(para, area); + } + Dialog::AddRule { src_ip, dst_ip, proto, src_port, dst_port, action } => { + let block = Block::default().borders(Borders::ALL).title("Add Rule"); + let text = format!( + "src_ip: {}\ndst_ip: {}\nproto: {}\nsrc_port: {}\ndst_port: {}\naction: {}", + src_ip, dst_ip, proto, src_port, dst_port, action + ); + let para = Paragraph::new(text).block(block); + f.render_widget(para, area); + } + Dialog::ConfirmDelete => { + let block = Block::default().borders(Borders::ALL).title("Confirm Delete"); + let para = Paragraph::new("Press Enter to confirm, Esc to cancel").block(block).alignment(Alignment::Center); + f.render_widget(para, area); + } + Dialog::None => {} + } +} + +fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage((100 - percent_y) / 2), Constraint::Percentage(percent_y), Constraint::Percentage((100 - percent_y) / 2)]) + .split(r); + Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage((100 - percent_x) / 2), Constraint::Percentage(percent_x), Constraint::Percentage((100 - percent_x) / 2)]) + .split(popup_layout[1])[1] +} diff --git a/xdp-firewall/src/stats.rs b/xdp-firewall/src/stats.rs new file mode 100644 index 0000000..fe3e3a3 --- /dev/null +++ b/xdp-firewall/src/stats.rs @@ -0,0 +1,15 @@ +use xdp_firewall_common::Stats; + +pub struct Rate { + pub pps: u64, + pub bps: u64, +} + +pub fn compute_rate(now: Stats, last: Stats, elapsed_secs: f64) -> Rate { + let pkts = now.packets.saturating_sub(last.packets); + let bytes = now.bytes.saturating_sub(last.bytes); + Rate { + pps: (pkts as f64 / elapsed_secs) as u64, + bps: (bytes as f64 / elapsed_secs) as u64, + } +} diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 0000000..d716048 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0" +duct = "0.13" +clap = { version = "4.5", features = ["derive"] } diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 0000000..03e4dd0 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,51 @@ +use std::{path::PathBuf, process::Command}; + +use clap::{Parser, Subcommand}; + +#[derive(Parser)] +#[command(about = "Build helpers for xdp-firewall")] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + BuildEbpf, +} + +fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + match cli.command { + Commands::BuildEbpf => build_ebpf(), + } +} + +fn build_ebpf() -> anyhow::Result<()> { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).parent().unwrap().to_path_buf(); + let ebpf_dir = root.join("xdp-firewall-ebpf"); + let target_dir = root.join("target"); + + let status = Command::new("rustup") + .args([ + "run", + "nightly", + "cargo", + "build", + "-Zbuild-std=core", + "--package", + "xdp-firewall-ebpf", + "--target", + "bpfel-unknown-none", + "--release", + ]) + .current_dir(&ebpf_dir) + .env("CARGO_TARGET_DIR", &target_dir) + .status()?; + + if !status.success() { + anyhow::bail!("eBPF build failed"); + } + + Ok(()) +}