diff --git a/.ccls b/.ccls deleted file mode 100644 index e69de29..0000000 diff --git a/.envrc b/.envrc deleted file mode 100644 index 4a4726a..0000000 --- a/.envrc +++ /dev/null @@ -1 +0,0 @@ -use_nix diff --git a/.gitignore b/.gitignore index 07180cd..0f1d2c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,13 @@ /reaction -reaction.db -reaction.db.old -/data +/ip46tables +/nft46 +/reaction*.db /reaction*.sock /result /wiki +/deb *.deb *.minisig *.qcow2 +debian-packaging/* *.swp -/target -/local -.ccls-cache -.direnv diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..78d7601 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,15 @@ +--- +image: golang:1.20-bookworm +stages: + - build + +variables: + DEBIAN_FRONTEND: noninteractive + +test_building: + stage: build + before_script: + - apt-get -qq -y update + - apt-get -qq -y install build-essential devscripts debhelper quilt wget + script: + - make reaction ip46tables nft46 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index 4289e65..0000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,81 +0,0 @@ -# Architecture - -Here is a high-level overview of the codebase. - -*Don't hesitate to create an issue or a merge request if something is unclear, missing or outdated.* - -## Build - -- `bench/`: Configuration that spawns a very high load on reaction. Useful to test performance improvements and regressions. -- `build.rs`: permits to create shell completions and man pages on build. -- `Cargo.toml`, `Cargo.lock`: manifest and dependencies. -- `config/`: example / test configuration files. Look at its git history to discover more. -- `Makefile`: Makefile. Resumes useful commands. -- `packaging/`: Files useful for .deb and .tar generation. -- `release.py`: Build process for a release. Handles cross-compilation, .tar and .deb generation. - -## Main source code - -- `tests/`: Integration tests. They test reaction runtime behavior, persistance, client-daemon communication, plugin integrations. -- `src/`: The source code, here we go! - -### Top-level files - -- `src/main.rs`: Main entrypoint -- `src/lib.rs`: Second main entrypoint -- `src/cli.rs`: Command-line arguments -- `src/tests.rs`: Test utilities -- `src/protocol.rs`: de/serialization and client/daemon protocol messages. - -### `src/concepts/` - -reaction really is about its configuration, which is at the center of the code. - -There is one file for each of its concepts: configuration, streams, filters, actions, patterns, plugins. - -### `src/client/` - -Client code: `reaction show`, `reaction flush`, `reaction trigger`, `reaction test-regex`. - -- `request.rs`: commands requiring client/server communication: `show`, `flush` & `trigger`. -- `test_config.rs`: `test-config` command. -- `test_regex.rs`: `test-regex` command. - -### `src/daemon` - -Daemon runtime structures and logic. - -This code has async code, to handle input streams and communication with clients, using the tokio runtime. - -- `mod.rs`: daemon main function. Initializes all tasks, handles synchronization and quitting, etc. -- `stream.rs`: Stream managers: start the stream `cmd` and dispatch its stdout lines to its Filter managers. -- `filter/`: Filter managers: handle lines, persistance, store matches and trigger actions. This is the main piece of runtime logic. - - `mod.rs`: High-level logic - - `state.rs`: Inner state operations -- `socket.rs`: The socket task, responsible for communication with clients. -- `plugin.rs`: Plugin startup, configuration loading and cleanup. - -### `crates/treedb` - -Persistence layer. - -This is a database highly adapted to reaction workload, making reaction faster than when used with general purpose key-value databases -(heed, sled and fjall crates have been tested). -Its design is explained in the comments of its files: - -- `lib.rs`: main database code, with its two API structs: Tree and Database. -- `raw.rs`: low-level part, directly interacting with de/serializisation and files. -- `time.rs`: time definitions shared with reaction. -- `helpers.rs`: utilities to ease db deserialization from disk. - -### `plugins/reaction-plugin` - -Shared plugin interface between reaction daemon and its plugins. - -Also defines some shared logic between them: -- `shutdown.rs`: Logic for passing shutdown signal across all tasks -- `parse_duration.rs` Duration parsing - -### `plugins/reaction-plugin-*` - -All core plugins. diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index c9a7b56..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,4985 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aead" -version = "0.6.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac8202ab55fcbf46ca829833f347a82a2a4ce0596f0304ac322c2d100030cd56" -dependencies = [ - "bytes", - "crypto-common", - "inout", -] - -[[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 = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "annotate-snippets" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e" -dependencies = [ - "unicode-width", - "yansi-term", -] - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -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 = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "assert_cmd" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" -dependencies = [ - "anstyle", - "bstr", - "libc", - "predicates", - "predicates-core", - "predicates-tree", - "wait-timeout", -] - -[[package]] -name = "assert_fs" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a652f6cb1f516886fcfee5e7a5c078b9ade62cfcb889524efe5a64d682dd27a9" -dependencies = [ - "anstyle", - "doc-comment", - "globwalk", - "predicates", - "predicates-core", - "predicates-tree", - "tempfile", -] - -[[package]] -name = "async-compat" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" -dependencies = [ - "futures-core", - "futures-io", - "once_cell", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "async_io_stream" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" -dependencies = [ - "futures", - "pharos", - "rustc_version", -] - -[[package]] -name = "atomic-polyfill" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" -dependencies = [ - "critical-section", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "attohttpc" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" -dependencies = [ - "base64 0.22.1", - "http", - "log", - "url", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "backon" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" -dependencies = [ - "fastrand", - "gloo-timers", - "tokio", -] - -[[package]] -name = "base16ct" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" - -[[package]] -name = "base32" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bindgen" -version = "0.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.114", -] - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - -[[package]] -name = "blake3" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", - "cpufeatures", -] - -[[package]] -name = "block-buffer" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" -dependencies = [ - "hybrid-array", - "zeroize", -] - -[[package]] -name = "bstr" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" -dependencies = [ - "memchr", - "regex-automata", - "serde", -] - -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" - -[[package]] -name = "cc" -version = "1.2.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chacha20" -version = "0.10.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd162f2b8af3e0639d83f28a637e4e55657b7a74508dba5a9bf4da523d5c9e9" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", - "zeroize", -] - -[[package]] -name = "chrono" -version = "0.4.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "cipher" -version = "0.5.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e12a13eb01ded5d32ee9658d94f553a19e804204f2dc811df69ab4d9e0cb8c7" -dependencies = [ - "block-buffer", - "crypto-common", - "inout", - "zeroize", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "clap" -version = "4.5.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_complete" -version = "4.5.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430b4dc2b5e3861848de79627b2bedc9f3342c7da5173a14eaa5d0f8dc18ae5d" -dependencies = [ - "clap", -] - -[[package]] -name = "clap_derive" -version = "4.5.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "clap_lex" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" - -[[package]] -name = "clap_mangen" -version = "0.2.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ea63a92086df93893164221ad4f24142086d535b3a0957b9b9bea2dc86301" -dependencies = [ - "clap", - "roff", -] - -[[package]] -name = "cobs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" -dependencies = [ - "thiserror 2.0.18", -] - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "const-oid" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" - -[[package]] -name = "constant_time_eq" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" - -[[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 = "cordyceps" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" -dependencies = [ - "loom", - "tracing", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.2.0-rc.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8235645834fbc6832939736ce2f2d08192652269e11010a6240f61b908a1c6" -dependencies = [ - "hybrid-array", - "rand_core 0.9.5", -] - -[[package]] -name = "crypto_box" -version = "0.10.0-pre.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bda4de3e070830cf3a27a394de135b6709aefcc54d1e16f2f029271254a6ed9" -dependencies = [ - "aead", - "chacha20", - "crypto_secretbox", - "curve25519-dalek", - "salsa20", - "serdect", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto_secretbox" -version = "0.2.0-pre.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54532aae6546084a52cef855593daf9555945719eeeda9974150e0def854873e" -dependencies = [ - "aead", - "chacha20", - "cipher", - "hybrid-array", - "poly1305", - "salsa20", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek" -version = "5.0.0-pre.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f9200d1d13637f15a6acb71e758f64624048d85b31a5fdbfd8eca1e2687d0b7" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rand_core 0.9.5", - "rustc_version", - "serde", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "data-encoding" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" - -[[package]] -name = "der" -version = "0.8.0-rc.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c1d73e9668ea6b6a28172aa55f3ebec38507131ce179051c8033b5c6037653" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "derive_more" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" -dependencies = [ - "derive_more-impl 1.0.0", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl 2.1.1", -] - -[[package]] -name = "derive_more-impl" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", - "unicode-xid", -] - -[[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 2.0.114", - "unicode-xid", -] - -[[package]] -name = "diatomic-waker" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" - -[[package]] -name = "difflib" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" - -[[package]] -name = "digest" -version = "0.11.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac89f8a64533a9b0eaa73a68e424db0fb1fd6271c74cc0125336a05f090568d" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "dlopen2" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" -dependencies = [ - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "doc-comment" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" - -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "ed25519" -version = "3.0.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "594435fe09e345ee388e4e8422072ff7dfeca8729389fbd997b3f5504c44cd47" -dependencies = [ - "pkcs8", - "serde", - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "3.0.0-pre.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad207ed88a133091f83224265eac21109930db09bedcad05d5252f2af2de20a1" -dependencies = [ - "curve25519-dalek", - "ed25519", - "rand_core 0.9.5", - "serde", - "sha2", - "signature", - "subtle", - "zeroize", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "embedded-io" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - -[[package]] -name = "embedded-io" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" - -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[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 = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fiat-crypto" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" - -[[package]] -name = "find-msvc-tools" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" - -[[package]] -name = "float-cmp" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" -dependencies = [ - "num-traits", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-buffered" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8e0e1f38ec07ba4abbde21eed377082f17ccb988be9d988a5adbf4bafc118fd" -dependencies = [ - "cordyceps", - "diatomic-waker", - "futures-core", - "pin-project-lite", - "spin 0.10.0", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generator" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" -dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows-link", - "windows-result", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "globset" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" -dependencies = [ - "aho-corasick", - "bstr", - "log", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "globwalk" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" -dependencies = [ - "bitflags", - "ignore", - "walkdir", -] - -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "h2" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hash32" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - -[[package]] -name = "heapless" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" -dependencies = [ - "atomic-polyfill", - "hash32", - "rustc_version", - "serde", - "spin 0.9.8", - "stable_deref_trait", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hickory-proto" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" -dependencies = [ - "async-trait", - "bytes", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "h2", - "http", - "idna", - "ipnet", - "once_cell", - "rand 0.9.2", - "ring", - "rustls", - "thiserror 2.0.18", - "tinyvec", - "tokio", - "tokio-rustls", - "tracing", - "url", -] - -[[package]] -name = "hickory-resolver" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" -dependencies = [ - "cfg-if", - "futures-util", - "hickory-proto", - "ipconfig", - "moka", - "once_cell", - "parking_lot", - "rand 0.9.2", - "resolv-conf", - "rustls", - "smallvec", - "thiserror 2.0.18", - "tokio", - "tokio-rustls", - "tracing", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hybrid-array" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f471e0a81b2f90ffc0cb2f951ae04da57de8baa46fa99112b062a5173a5088d0" -dependencies = [ - "typenum", - "zeroize", -] - -[[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] - -[[package]] -name = "hyper-util" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.6.1", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "igd-next" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516893339c97f6011282d5825ac94fc1c7aad5cad26bdc2d0cee068c0bf97f97" -dependencies = [ - "async-trait", - "attohttpc", - "bytes", - "futures", - "http", - "http-body-util", - "hyper", - "hyper-util", - "log", - "rand 0.9.2", - "tokio", - "url", - "xmltree", -] - -[[package]] -name = "ignore" -version = "0.4.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" -dependencies = [ - "crossbeam-deque", - "globset", - "log", - "memchr", - "regex-automata", - "same-file", - "walkdir", - "winapi-util", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "inout" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "ipconfig" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" -dependencies = [ - "socket2 0.5.10", - "widestring", - "windows-sys 0.48.0", - "winreg", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "ipset" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3f6539e7df6df265a48ac3bfa1cc1b9fb37b604c92e9ab01521865e2323787f" -dependencies = [ - "bindgen", - "cc", - "derive_more 1.0.0", - "ipset_derive", - "libc", -] - -[[package]] -name = "ipset_derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db6d64e879badf39e93df3831cb2902d290a2db61f8a46f299a681a2a601e0" -dependencies = [ - "quote", - "syn 2.0.114", -] - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "iroh" -version = "0.95.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2374ba3cdaac152dc6ada92d971f7328e6408286faab3b7350842b2ebbed4789" -dependencies = [ - "aead", - "backon", - "bytes", - "cfg_aliases", - "crypto_box", - "data-encoding", - "derive_more 2.1.1", - "ed25519-dalek", - "futures-util", - "getrandom 0.3.4", - "hickory-resolver", - "http", - "igd-next", - "instant", - "iroh-base", - "iroh-metrics", - "iroh-quinn", - "iroh-quinn-proto", - "iroh-quinn-udp", - "iroh-relay", - "n0-error", - "n0-future", - "n0-watcher", - "netdev", - "netwatch", - "pin-project", - "pkarr", - "pkcs8", - "portmapper", - "rand 0.9.2", - "reqwest", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "rustls-webpki", - "serde", - "smallvec", - "strum", - "time", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", - "url", - "wasm-bindgen-futures", - "webpki-roots", - "z32", -] - -[[package]] -name = "iroh-base" -version = "0.95.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a8c5fb1cc65589f0d7ab44269a76f615a8c4458356952c9b0ef1c93ea45ff8" -dependencies = [ - "curve25519-dalek", - "data-encoding", - "derive_more 2.1.1", - "ed25519-dalek", - "n0-error", - "rand_core 0.9.5", - "serde", - "url", - "zeroize", - "zeroize_derive", -] - -[[package]] -name = "iroh-metrics" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e3381da7c93c12d353230c74bba26131d1c8bf3a4d8af0fec041546454582e" -dependencies = [ - "iroh-metrics-derive", - "itoa", - "n0-error", - "postcard", - "ryu", - "serde", - "tracing", -] - -[[package]] -name = "iroh-metrics-derive" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e12bd0763fd16062f5cc5e8db15dd52d26e75a8af4c7fb57ccee3589b344b8" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "iroh-quinn" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde160ebee7aabede6ae887460cd303c8b809054224815addf1469d54a6fcf7" -dependencies = [ - "bytes", - "cfg_aliases", - "iroh-quinn-proto", - "iroh-quinn-udp", - "pin-project-lite", - "rustc-hash 2.1.1", - "rustls", - "socket2 0.5.10", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "iroh-quinn-proto" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "929d5d8fa77d5c304d3ee7cae9aede31f13908bd049f9de8c7c0094ad6f7c535" -dependencies = [ - "bytes", - "getrandom 0.2.17", - "rand 0.8.5", - "ring", - "rustc-hash 2.1.1", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "iroh-quinn-udp" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c53afaa1049f7c83ea1331f5ebb9e6ebc5fdd69c468b7a22dd598b02c9bcc973" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.5.10", - "tracing", - "windows-sys 0.59.0", -] - -[[package]] -name = "iroh-relay" -version = "0.95.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43fbdf2aeffa7d6ede1a31f6570866c2199b1cee96a0b563994623795d1bac2c" -dependencies = [ - "blake3", - "bytes", - "cfg_aliases", - "data-encoding", - "derive_more 2.1.1", - "getrandom 0.3.4", - "hickory-resolver", - "http", - "http-body-util", - "hyper", - "hyper-util", - "iroh-base", - "iroh-metrics", - "iroh-quinn", - "iroh-quinn-proto", - "lru", - "n0-error", - "n0-future", - "num_enum", - "pin-project", - "pkarr", - "postcard", - "rand 0.9.2", - "reqwest", - "rustls", - "rustls-pki-types", - "serde", - "serde_bytes", - "sha1", - "strum", - "tokio", - "tokio-rustls", - "tokio-util", - "tokio-websockets", - "tracing", - "url", - "webpki-roots", - "ws_stream_wasm", - "z32", -] - -[[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.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jrsonnet-evaluator" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fee60406dac44a01b37e120b43adb062047251e195db15392b825f6bdc948712" -dependencies = [ - "annotate-snippets", - "base64 0.13.1", - "bincode", - "jrsonnet-gc", - "jrsonnet-interner", - "jrsonnet-parser", - "jrsonnet-stdlib", - "jrsonnet-types", - "md5", - "pathdiff", - "rustc-hash 1.1.0", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "jrsonnet-gc" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68da8bc2f00117b1373bb8877af03b1d391e4c4800e6585d7279e5b99c919dde" -dependencies = [ - "jrsonnet-gc-derive", -] - -[[package]] -name = "jrsonnet-gc-derive" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcba9c387b64b054f06cc4d724905296e21edeeb7506847f3299117a2d92d12" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "synstructure 0.12.6", -] - -[[package]] -name = "jrsonnet-interner" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ff75843e778244f3476800e6f492950a6ecee1d9308019764983d311620bf9" -dependencies = [ - "jrsonnet-gc", - "rustc-hash 1.1.0", - "serde", -] - -[[package]] -name = "jrsonnet-parser" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daaad69b21c1dba904f3bb1640e02f8f60c5cd4eae8c9bc035b38a83324cdf45" -dependencies = [ - "jrsonnet-gc", - "jrsonnet-interner", - "peg", - "serde", - "unescape", -] - -[[package]] -name = "jrsonnet-stdlib" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840295ba3a8d65bf71e57a57acbef4c77d11c543739cfded27f91feef239f80e" - -[[package]] -name = "jrsonnet-types" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909b38de99711ef357a514af1ed112e6cf411ab8b204cc92507b6e219e65fe5c" -dependencies = [ - "jrsonnet-gc", - "peg", -] - -[[package]] -name = "js-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[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.180" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libnftables1-sys" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b290d0d41f0ad578660aeed371bcae4cf85f129a6fe31350dbd2e097518cd7f" -dependencies = [ - "bindgen", -] - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[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 = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "lru" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" -dependencies = [ - "hashbrown", -] - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "md5" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "moka" -version = "0.12.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3dec6bd31b08944e08b58fd99373893a6c17054d6f3ea5006cc894f4f4eee2a" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "parking_lot", - "portable-atomic", - "smallvec", - "tagptr", - "uuid", -] - -[[package]] -name = "n0-error" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4782b4baf92d686d161c15460c83d16ebcfd215918763903e9619842665cae" -dependencies = [ - "n0-error-macros", - "spez", -] - -[[package]] -name = "n0-error-macros" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03755949235714b2b307e5ae89dd8c1c2531fb127d9b8b7b4adf9c876cd3ed18" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "n0-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe" -dependencies = [ - "cfg_aliases", - "derive_more 2.1.1", - "futures-buffered", - "futures-lite", - "futures-util", - "js-sys", - "pin-project", - "send_wrapper", - "tokio", - "tokio-util", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-time", -] - -[[package]] -name = "n0-watcher" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38acf13c1ddafc60eb7316d52213467f8ccb70b6f02b65e7d97f7799b1f50be4" -dependencies = [ - "derive_more 2.1.1", - "n0-error", - "n0-future", -] - -[[package]] -name = "netdev" -version = "0.38.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ab878b4c90faf36dab10ea51d48c69ae9019bcca47c048a7c9b273d5d7a823" -dependencies = [ - "dlopen2", - "ipnet", - "libc", - "netlink-packet-core", - "netlink-packet-route", - "netlink-sys", - "once_cell", - "system-configuration", - "windows-sys 0.59.0", -] - -[[package]] -name = "netlink-packet-core" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" -dependencies = [ - "paste", -] - -[[package]] -name = "netlink-packet-route" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec2f5b6839be2a19d7fa5aab5bc444380f6311c2b693551cb80f45caaa7b5ef" -dependencies = [ - "bitflags", - "libc", - "log", - "netlink-packet-core", -] - -[[package]] -name = "netlink-proto" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" -dependencies = [ - "bytes", - "futures", - "log", - "netlink-packet-core", - "netlink-sys", - "thiserror 2.0.18", -] - -[[package]] -name = "netlink-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" -dependencies = [ - "bytes", - "futures", - "libc", - "log", - "tokio", -] - -[[package]] -name = "netwatch" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26f2acd376ef48b6c326abf3ba23c449e0cb8aa5c2511d189dd8a8a3bfac889b" -dependencies = [ - "atomic-waker", - "bytes", - "cfg_aliases", - "derive_more 2.1.1", - "iroh-quinn-udp", - "js-sys", - "libc", - "n0-error", - "n0-future", - "n0-watcher", - "netdev", - "netlink-packet-core", - "netlink-packet-route", - "netlink-proto", - "netlink-sys", - "pin-project-lite", - "serde", - "socket2 0.6.1", - "time", - "tokio", - "tokio-util", - "tracing", - "web-sys", - "windows", - "windows-result", - "wmi", -] - -[[package]] -name = "nftables" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c57e7343eed9e9330e084eef12651b15be3c8ed7825915a0ffa33736b852bed" -dependencies = [ - "schemars", - "serde", - "serde_json", - "serde_path_to_error", - "strum", - "strum_macros", - "thiserror 2.0.18", - "tokio", -] - -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "normalize-line-endings" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" - -[[package]] -name = "ntimestamp" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50f94c405726d3e0095e89e72f75ce7f6587b94a8bd8dc8054b73f65c0fd68c" -dependencies = [ - "base32", - "document-features", - "getrandom 0.2.17", - "httpdate", - "js-sys", - "once_cell", - "serde", -] - -[[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-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_enum" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -dependencies = [ - "critical-section", - "portable-atomic", -] - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "openssl-probe" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[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 = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[package]] -name = "peg" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c0b841ea54f523f7aa556956fbd293bcbe06f2e67d2eb732b7278aaf1d166a" -dependencies = [ - "peg-macros", - "peg-runtime", -] - -[[package]] -name = "peg-macros" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aa52829b8decbef693af90202711348ab001456803ba2a98eb4ec8fb70844c" -dependencies = [ - "peg-runtime", - "proc-macro2", - "quote", -] - -[[package]] -name = "peg-runtime" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" - -[[package]] -name = "pem-rfc7468" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pharos" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" -dependencies = [ - "futures", - "rustc_version", -] - -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkarr" -version = "5.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d346b545765a0ef58b6a7e160e17ddaa7427f439b7b9a287df6c88c9e04bf2" -dependencies = [ - "async-compat", - "base32", - "bytes", - "cfg_aliases", - "document-features", - "dyn-clone", - "ed25519-dalek", - "futures-buffered", - "futures-lite", - "getrandom 0.3.4", - "log", - "lru", - "ntimestamp", - "reqwest", - "self_cell", - "serde", - "sha1_smol", - "simple-dns", - "thiserror 2.0.18", - "tokio", - "tracing", - "url", - "wasm-bindgen-futures", -] - -[[package]] -name = "pkcs8" -version = "0.11.0-rc.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77089aec8290d0b7bb01b671b091095cf1937670725af4fd73d47249f03b12c0" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "poly1305" -version = "0.9.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb78a635f75d76d856374961deecf61031c0b6f928c83dc9c0924ab6c019c298" -dependencies = [ - "cpufeatures", - "universal-hash", -] - -[[package]] -name = "portable-atomic" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" - -[[package]] -name = "portmapper" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b575f975dcf03e258b0c7ab3f81497d7124f508884c37da66a7314aa2a8d467" -dependencies = [ - "base64 0.22.1", - "bytes", - "derive_more 2.1.1", - "futures-lite", - "futures-util", - "hyper-util", - "igd-next", - "iroh-metrics", - "libc", - "n0-error", - "netwatch", - "num_enum", - "rand 0.9.2", - "serde", - "smallvec", - "socket2 0.6.1", - "time", - "tokio", - "tokio-util", - "tower-layer", - "tracing", - "url", -] - -[[package]] -name = "postbag" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02aa900208f326b4fa5d7943ede192c1265a1519e7132aa6760e3440a1f4ceb0" -dependencies = [ - "serde", -] - -[[package]] -name = "postcard" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" -dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "heapless", - "postcard-derive", - "serde", -] - -[[package]] -name = "postcard-derive" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "predicates" -version = "3.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" -dependencies = [ - "anstyle", - "difflib", - "float-cmp", - "normalize-line-endings", - "predicates-core", - "regex", -] - -[[package]] -name = "predicates-core" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" - -[[package]] -name = "predicates-tree" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" -dependencies = [ - "predicates-core", - "termtree", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.114", -] - -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro2" -version = "1.0.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 2.1.1", - "rustls", - "socket2 0.6.1", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash 2.1.1", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.6.1", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "reaction" -version = "2.3.0" -dependencies = [ - "assert_cmd", - "assert_fs", - "chrono", - "clap", - "clap_complete", - "clap_mangen", - "futures", - "jrsonnet-evaluator", - "nix", - "num_cpus", - "predicates", - "rand 0.8.5", - "reaction-plugin", - "regex", - "remoc", - "serde", - "serde_json", - "serde_yaml", - "tempfile", - "thiserror 1.0.69", - "tokio", - "tokio-util", - "tracing", - "tracing-subscriber", - "treedb", -] - -[[package]] -name = "reaction-plugin" -version = "1.0.0" -dependencies = [ - "remoc", - "serde", - "serde_json", - "tokio", - "tokio-util", -] - -[[package]] -name = "reaction-plugin-cluster" -version = "0.1.0" -dependencies = [ - "assert_fs", - "chrono", - "data-encoding", - "futures", - "iroh", - "rand 0.9.2", - "reaction-plugin", - "remoc", - "serde", - "serde_json", - "tokio", - "treedb", -] - -[[package]] -name = "reaction-plugin-ipset" -version = "1.0.0" -dependencies = [ - "ipset", - "reaction-plugin", - "remoc", - "serde", - "serde_json", - "tokio", -] - -[[package]] -name = "reaction-plugin-nftables" -version = "0.1.0" -dependencies = [ - "libnftables1-sys", - "nftables", - "reaction-plugin", - "remoc", - "serde", - "serde_json", - "tokio", -] - -[[package]] -name = "reaction-plugin-virtual" -version = "1.0.0" -dependencies = [ - "reaction-plugin", - "remoc", - "serde", - "serde_json", - "tokio", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "remoc" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0491961ac4bc1ac4191743aa58a2ce778f4725693d29743fae957b2cf45f77f0" -dependencies = [ - "byteorder", - "bytes", - "futures", - "postbag", - "rand 0.9.2", - "remoc_macro", - "serde", - "tokio", - "tokio-util", - "tracing", - "uuid", -] - -[[package]] -name = "remoc_macro" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89479d9d87f65ef573faf0167dd0a9f40d3a63fd95e7a2935d662fa57dbc30d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "webpki-roots", -] - -[[package]] -name = "resolv-conf" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "roff" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[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 = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" -dependencies = [ - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs 0.26.11", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - -[[package]] -name = "rustls-webpki" -version = "0.103.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[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.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" - -[[package]] -name = "salsa20" -version = "0.11.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3ff3b81c8a6e381bc1673768141383f9328048a60edddcfc752a8291a138443" -dependencies = [ - "cfg-if", - "cipher", -] - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "schemars" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" -dependencies = [ - "dyn-clone", - "ref-cast", - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.114", -] - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "security-framework" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" -dependencies = [ - "bitflags", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "self_cell" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "send_wrapper" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" - -[[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_bytes" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[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 = "serde_path_to_error" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "itoa", - "serde", - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "serdect" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9af4a3e75ebd5599b30d4de5768e00b5095d518a79fefc3ecbaf77e665d1ec06" -dependencies = [ - "base16ct", - "serde", -] - -[[package]] -name = "sha1" -version = "0.11.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e046edf639aa2e7afb285589e5405de2ef7e61d4b0ac1e30256e3eab911af9" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - -[[package]] -name = "sha2" -version = "0.11.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1e3878ab0f98e35b2df35fe53201d088299b41a6bb63e3e34dada2ac4abd924" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "signature" -version = "3.0.0-rc.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a96996ccff7dfa16f052bd995b4cecc72af22c35138738dc029f0ead6608d" - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "simple-dns" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee851d0e5e7af3721faea1843e8015e820a234f81fda3dea9247e15bac9a86a" -dependencies = [ - "bitflags", -] - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "spez" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c87e960f4dca2788eeb86bbdde8dd246be8948790b7618d656e68f9b720a86e8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spin" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" - -[[package]] -name = "spki" -version = "0.8.0-rc.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8baeff88f34ed0691978ec34440140e1572b68c7dd4a495fd14a3dc1944daa80" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "unicode-xid", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - -[[package]] -name = "tempfile" -version = "3.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" -dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "termtree" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 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 2.0.114", -] - -[[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 2.0.114", -] - -[[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 = "time" -version = "0.3.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" -dependencies = [ - "deranged", - "js-sys", - "num-conv", - "powerfmt", - "serde_core", - "time-core", -] - -[[package]] -name = "time-core" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2 0.6.1", - "tokio-macros", - "tracing", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "futures-util", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-websockets" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1b6348ebfaaecd771cecb69e832961d277f59845d4220a584701f72728152b7" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "futures-sink", - "getrandom 0.3.4", - "http", - "httparse", - "rand 0.9.2", - "ring", - "rustls-pki-types", - "simdutf8", - "tokio", - "tokio-rustls", - "tokio-util", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.23.10+spec-1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" -dependencies = [ - "winnow", -] - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "log", - "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 2.0.114", -] - -[[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.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "treedb" -version = "1.0.0" -dependencies = [ - "chrono", - "futures", - "serde", - "serde_json", - "tempfile", - "thiserror 1.0.69", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "unescape" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccb97dac3243214f8d8507998906ca3e2e0b900bf9bf4870477f125b82e68f6e" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "universal-hash" -version = "0.6.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55be643b40a21558f44806b53ee9319595bc7ca6896372e4e08e5d7d83c9cd6" -dependencies = [ - "crypto-common", - "subtle", -] - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", - "serde_derive", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" -dependencies = [ - "getrandom 0.3.4", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" -dependencies = [ - "cfg-if", - "futures-util", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.114", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "web-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-root-certs" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" -dependencies = [ - "webpki-root-certs 1.0.5", -] - -[[package]] -name = "webpki-root-certs" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "webpki-roots" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "widestring" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core", - "windows-link", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[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.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[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-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[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.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[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.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[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.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[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.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[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.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[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.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[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 = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" - -[[package]] -name = "wmi" -version = "0.17.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120d8c2b6a7c96c27bf4a7947fd7f02d73ca7f5958b8bd72a696e46cb5521ee6" -dependencies = [ - "chrono", - "futures", - "log", - "serde", - "thiserror 2.0.18", - "windows", - "windows-core", -] - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "ws_stream_wasm" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" -dependencies = [ - "async_io_stream", - "futures", - "js-sys", - "log", - "pharos", - "rustc_version", - "send_wrapper", - "thiserror 2.0.18", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "xml-rs" -version = "0.8.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" - -[[package]] -name = "xmltree" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" -dependencies = [ - "xml-rs", -] - -[[package]] -name = "yansi-term" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1" -dependencies = [ - "winapi", -] - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", - "synstructure 0.13.2", -] - -[[package]] -name = "z32" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f" - -[[package]] -name = "zerocopy" -version = "0.8.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", - "synstructure 0.13.2", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "zmij" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2" diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index 66e95e3..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,105 +0,0 @@ -[package] -name = "reaction" -version = "2.3.0" -edition = "2024" -authors = ["ppom "] -license = "AGPL-3.0" -description = "Scan logs and take action" -readme = "README.md" -homepage = "https://reaction.ppom.me" -repository = "https://framagit.org/ppom/reaction" -keywords = ["security", "sysadmin", "fail2ban", "logs", "monitoring"] -build = "build.rs" -default-run = "reaction" - -[package.metadata.deb] -section = "net" -extended-description = """A daemon that scans program outputs for repeated patterns, and takes action. -A common usage is to scan ssh and webserver logs, and to ban hosts that cause multiple authentication errors. -reaction aims at being a successor to fail2ban.""" -maintainer-scripts = "packaging/" -systemd-units = { enable = false } -assets = [ - # Executables - [ "target/release/reaction", "/usr/bin/reaction", "755" ], - [ "target/release/reaction-plugin-virtual", "/usr/bin/reaction-plugin-virtual", "755" ], - # Man pages - [ "target/release/reaction*.1", "/usr/share/man/man1/", "644" ], - # Shell completions - [ "target/release/reaction.bash", "/usr/share/bash-completion/completions/reaction", "644" ], - [ "target/release/reaction.fish", "/usr/share/fish/completions/", "644" ], - [ "target/release/_reaction", "/usr/share/zsh/vendor-completions/", "644" ], - # Slice - [ "packaging/system-reaction.slice", "/usr/lib/systemd/system/", "644" ], -] - -[dependencies] -# Time types -chrono.workspace = true -# CLI parsing -clap = { version = "4.5.4", features = ["derive"] } -# Unix interfaces -nix = { version = "0.29.0", features = ["signal"] } -num_cpus = "1.16.0" -# Regex matching -regex = "1.10.4" -# Configuration languages, ser/deserialisation -serde.workspace = true -serde_json.workspace = true -serde_yaml = "0.9.34" -jrsonnet-evaluator = "0.4.2" -# Error macro -thiserror.workspace = true -# Async runtime & helpers -futures = { workspace = true } -tokio = { workspace = true, features = ["full", "tracing"] } -tokio-util = { workspace = true, features = ["codec"] } -# Async logging -tracing.workspace = true -tracing-subscriber = "0.3.18" -# Database -treedb.workspace = true -# Reaction plugin system -remoc.workspace = true -reaction-plugin.workspace = true - -[build-dependencies] -clap = { version = "4.5.4", features = ["derive"] } -clap_complete = "4.5.2" -clap_mangen = "0.2.24" -regex = "1.10.4" -tracing = "0.1.40" - -[dev-dependencies] -rand = "0.8.5" -treedb.workspace = true -treedb.features = ["test"] -tempfile.workspace = true -assert_fs.workspace = true -assert_cmd = "2.0.17" -predicates = "3.1.3" - -[workspace] -members = [ - "crates/treedb", - "plugins/reaction-plugin", - "plugins/reaction-plugin-cluster", - "plugins/reaction-plugin-ipset", - "plugins/reaction-plugin-nftables", - "plugins/reaction-plugin-virtual" -] - -[workspace.dependencies] -assert_fs = "1.1.3" -chrono = { version = "0.4.38", features = ["std", "clock", "serde"] } -futures = "0.3.30" -remoc = { version = "0.18.3" } -serde = { version = "1.0.203", features = ["derive"] } -serde_json = { version = "1.0.117", features = ["arbitrary_precision"] } -tempfile = "3.12.0" -thiserror = "1.0.63" -tokio = { version = "1.40.0" } -tokio-util = { version = "0.7.12" } -tracing = "0.1.40" -reaction-plugin = { path = "plugins/reaction-plugin" } -treedb = { path = "crates/treedb" } diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 1c5d54e..0000000 --- a/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -# This Dockerfile permits to build reaction and its plugins - -# Use debian old-stable, so that it runs on both old-stable and stable -FROM rust:bookworm - -RUN apt update && apt install -y \ - clang \ - libipset-dev \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /reaction diff --git a/Makefile b/Makefile index 9b31a54..1226d7b 100644 --- a/Makefile +++ b/Makefile @@ -1,23 +1,51 @@ CC ?= gcc PREFIX ?= /usr/local BINDIR = $(PREFIX)/bin -MANDIR = $(PREFIX)/share/man/man1 SYSTEMDDIR ?= /etc/systemd -all: reaction +all: reaction ip46tables nft46 clean: - cargo clean + rm -f reaction ip46tables nft46 reaction*.deb reaction.minisig ip46tables.minisig nft46.minisig reaction*.deb.minisig + rm -rf debian-packaging -reaction: - cargo build --release +ip46tables: helpers_c/ip46tables.c + $(CC) -s -static helpers_c/ip46tables.c -o ip46tables -install: reaction - install -m755 target/release/reaction $(DESTDIR)$(BINDIR) +nft46: helpers_c/nft46.c + $(CC) -s -static helpers_c/nft46.c -o nft46 + +reaction: app/* reaction.go go.mod go.sum + CGO_ENABLED=0 go build -buildvcs=false -ldflags "-s -X main.version=`git tag --sort=v:refname | tail -n1` -X main.commit=`git rev-parse --short HEAD`" + +reaction_%-1_amd64.deb: + apt-get -qq -y update + apt-get -qq -y install build-essential devscripts debhelper quilt wget + if [ -e debian-packaging ]; then rm -rf debian-packaging; fi + mkdir debian-packaging + wget "https://framagit.org/ppom/reaction/-/archive/v${*}/reaction-v${*}.tar.gz" -O "debian-packaging/reaction_${*}.orig.tar.gz" + cd debian-packaging && tar xf "reaction_${*}.orig.tar.gz" + cp -r debian "debian-packaging/reaction-v${*}" + if [ -e "debian/changelog" ]; then \ + cd "debian-packaging/reaction-v${*}" && \ + DEBFULLNAME=ppom DEBEMAIL=reaction@ppom.me dch --package reaction --newversion "${*}-1" "New upstream release."; \ + else \ + cd "debian-packaging/reaction-v${*}" && \ + DEBFULLNAME=ppom DEBEMAIL=reaction@ppom.me dch --create --package reaction --newversion "${*}-1" "Initial release."; \ + fi + cd "debian-packaging/reaction-v${*}" && DEBFULLNAME=ppom DEBEMAIL=reaction@ppom.me dch --release --distribution stable --urgency low "" + cd "debian-packaging/reaction-v${*}" && debuild --prepend-path=/go/bin:/usr/local/go/bin -us -uc + cp "debian-packaging/reaction-v${*}/debian/changelog" debian/ + cp "debian-packaging/reaction_${*}-1_amd64.deb" . + +signatures_%: reaction_%-1_amd64.deb reaction ip46tables nft46 + minisign -Sm nft46 ip46tables reaction reaction_${*}-1_amd64.deb + +install: all + install -m755 reaction $(DESTDIR)$(BINDIR) + install -m755 ip46tables $(DESTDIR)$(BINDIR) + install -m755 nft46 $(DESTDIR)$(BINDIR) install_systemd: install - install -m644 packaging/reaction.service $(SYSTEMDDIR)/system/reaction.service - sed -i 's#/usr/local/bin#$(DESTDIR)$(BINDIR)#' $(SYSTEMDDIR)/system/reaction.service - -release: - nix-shell release.py + install -m644 config/reaction.example.service $(SYSTEMDDIR)/system/reaction.service + sed -i 's#/usr/bin#$(DESTDIR)$(BINDIR)#' $(SYSTEMDDIR)/system/reaction.service diff --git a/README.md b/README.md index 7d33ab5..fb0d0d1 100644 --- a/README.md +++ b/README.md @@ -4,40 +4,32 @@ A daemon that scans program outputs for repeated patterns, and takes action. A common usage is to scan ssh and webserver logs, and to ban hosts that cause multiple authentication errors. -🚧 This program hasn't received external security audit yet. However, it already works well on many servers 🚧 +🚧 This program hasn't received external audit. however, it already works well on my servers 🚧 ## Rationale -I was using the honorable fail2ban since quite a long time, but i was a bit frustrated by its CPU consumption +I was using the honorable fail2ban since quite a long time, but i was a bit frustrated by its cpu consumption and all its heavy default configuration. In my view, a security-oriented program should be simple to configure and an always-running daemon should be implemented in a fast*er* language. -reaction does not have all the features of the honorable fail2ban, but it's more than 10x faster and has more manageable configuration. +reaction does not have all the features of the honorable fail2ban, but it's ~10x faster and has more manageable configuration. [📽️ quick french name explanation 😉](https://u.ppom.me/reaction.webm) [🇬🇧 in-depth blog article](https://blog.ppom.me/en-reaction) / [🇫🇷 french version](https://blog.ppom.me/fr-reaction) -## Rust rewrite - -reaction v2.x is a complete Rust rewrite of reaction. -It's in feature parity with the Go version, v1.x, which is now deprecated. - -See https://blog.ppom.me/en-reaction-v2. - ## Configuration YAML and [JSONnet](https://jsonnet.org/) (more powerful) are supported. both are extensions of JSON, so JSON is transitively supported. -- See [reaction.yml](./config/example.yml) or [reaction.jsonnet](./config/example.jsonnet) for a fully explained reference (ipv4 + ipv6) -- See the [wiki](https://reaction.ppom.me) for multiple examples, security recommendations and FAQ. -- See [server.jsonnet](https://reaction.ppom.me/configurations/ppom/server.jsonnet.html) for a real-world configuration -- See [reaction.service](./config/reaction.service) for a systemd service file -- This minimal example (ipv4 only) shows what's needed to prevent brute force attacks on an ssh server (please read at least the [Security](https://reaction.ppom.me/security.html) part of the wiki before starting 🆙): +- See [reaction.yml](./app/example.yml) or [reaction.jsonnet](./config/example.jsonnet) for a fully explained reference +- See [server.jsonnet](./config/server.jsonnet) for a real-world configuration +- See [reaction.example.service](./config/reaction.example.service) for a systemd service file +- This quick example shows what's needed to prevent brute force attacks on an ssh server:
@@ -45,19 +37,18 @@ both are extensions of JSON, so JSON is transitively supported. ```yaml patterns: - ip: - type: ipv4 + ip: '(([ 0-9 ]{1,3}\.){3}[0-9]{1,3})|([0-9a-fA-F:]{2,90})' start: - - [ 'iptables', '-w', '-N', 'reaction' ] - - [ 'iptables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ] - - [ 'iptables', '-w', '-I', 'FORWARD', '-p', 'all', '-j', 'reaction' ] + - [ 'ip46tables', '-w', '-N', 'reaction' ] + - [ 'ip46tables', '-w', '-A', 'reaction', '-j', 'ACCEPT' ] + - [ 'ip46tables', '-w', '-I', 'reaction', '1', '-s', '127.0.0.1', '-j', 'ACCEPT' ] + - [ 'ip46tables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ] stop: - - [ 'iptables', '-w', '-D', 'INPUT', '-p', 'all', '-j', 'reaction' ] - - [ 'iptables', '-w', '-D', 'FORWARD', '-p', 'all', '-j', 'reaction' ] - - [ 'iptables', '-w', '-F', 'reaction' ] - - [ 'iptables', '-w', '-X', 'reaction' ] + - [ 'ip46tables', '-w', '-D', 'INPUT', '-p', 'all', '-j', 'reaction' ] + - [ 'ip46tables', '-w', '-F', 'reaction' ] + - [ 'ip46tables', '-w', '-X', 'reaction' ] streams: ssh: @@ -66,16 +57,13 @@ streams: failedlogin: regex: - 'authentication failure;.*rhost=' - - 'Failed password for .* from ' - - 'Invalid user .* from ' - - 'banner exchange: Connection from port [0-9]*: invalid format' retry: 3 retryperiod: '6h' actions: ban: - cmd: [ 'iptables', '-w', '-I', 'reaction', '1', '-s', '', '-j', 'DROP' ] + cmd: [ 'ip46tables', '-w', '-I', 'reaction', '1', '-s', '', '-j', 'block' ] unban: - cmd: [ 'iptables', '-w', '-D', 'reaction', '1', '-s', '', '-j', 'DROP' ] + cmd: [ 'ip46tables', '-w', '-D', 'reaction', '1', '-s', '', '-j', 'block' ] after: '48h' ``` @@ -86,44 +74,39 @@ streams: /etc/reaction.jsonnet ```jsonnet +local iptables(args) = [ 'ip46tables', '-w' ] + args; local banFor(time) = { ban: { - cmd: ['iptables', '-w', '-A', 'reaction', '-s', '', '-j', 'DROP'], + cmd: iptables(['-A', 'reaction', '-s', '', '-j', 'reaction-log-refuse']), }, unban: { - cmd: ['iptables', '-w', '-D', 'reaction', '-s', '', '-j', 'DROP'], after: time, + cmd: iptables(['-D', 'reaction', '-s', '', '-j', 'reaction-log-refuse']), }, }; - { patterns: { ip: { - type: 'ipv4', + regex: @'(?:(?:[ 0-9 ]{1,3}\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:]{2,90})', }, }, start: [ - ['iptables', '-N', 'reaction'], - ['iptables', '-I', 'INPUT', '-p', 'all', '-j', 'reaction'], - ['iptables', '-I', 'FORWARD', '-p', 'all', '-j', 'reaction'], + iptables([ '-N', 'reaction' ]), + iptables([ '-A', 'reaction', '-j', 'ACCEPT' ]), + iptables([ '-I', 'reaction', '1', '-s', '127.0.0.1', '-j', 'ACCEPT' ]), + iptables([ '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ]), ], stop: [ - ['iptables', '-D', 'INPUT', '-p', 'all', '-j', 'reaction'], - ['iptables', '-D', 'FORWARD', '-p', 'all', '-j', 'reaction'], - ['iptables', '-F', 'reaction'], - ['iptables', '-X', 'reaction'], + iptables([ '-D', 'INPUT', '-p', 'all', '-j', 'reaction' ]), + iptables([ '-F', 'reaction' ]), + iptables([ '-X', 'reaction' ]), ], streams: { ssh: { - cmd: ['journalctl', '-fu', 'sshd.service'], + cmd: [ 'journalctl', '-fu', 'sshd.service' ], filters: { failedlogin: { - regex: [ - @'authentication failure;.*rhost=', - @'Failed password for .* from ', - @'banner exchange: Connection from port [0-9]*: invalid format', - @'Invalid user .* from ', - ], + regex: [ @'authentication failure;.*rhost=' ], retry: 3, retryperiod: '6h', actions: banFor('48h'), @@ -136,35 +119,30 @@ local banFor(time) = {
-> It is recommended to setup reaction with [`nftables`](https://reaction.ppom.me/actions/nftables.html) -> or [`ipset` + `iptables`](https://reaction.ppom.me/actions/ipset.html), which are much more performant -> solutions than `iptables` alone. ### Database -The embedded database is stored in the working directory (but can be overriden by the `state_directory` config option). +The embedded database is stored in the working directory. If you don't know where to start reaction, `/var/lib/reaction` should be a sane choice. ### CLI - `reaction start` runs the server -- `reaction show` show pending actions (ie. show current bans) +- `reaction show` show pending actions (ie. bans) - `reaction flush` permits to run pending actions (ie. clear bans) -- `reaction trigger` permits to manually trigger a filter (ie. run custom ban) - `reaction test-regex` permits to test regexes -- `reaction test-config` shows loaded configuration - `reaction help` for full usage. -### old binaries +### `ip46tables` -`ip46tables` and `nft46` binaries are no longer part of reaction. If you really need them, see -[the last commit that included them](https://framagit.org/ppom/reaction/-/tree/b7d997ca5e9a69c8572bb2ec9d27d0eb03b3cb9f/helpers_c). +`ip46tables` is a minimal c program present in its own subdirectory with only standard posix dependencies. + +It permits to configure `iptables` and `ip6tables` at the same time. +It will execute `iptables` when detecting ipv4, `ip6tables` when detecting ipv6 and both if no ip address is present on the command line. ## Wiki -You'll find more ressources, service configurations, etc. on [the wiki](https://reaction.ppom.me)! - -We recommend that you read the ***Good Practices*** chapters before starting. +You'll find more ressources, service configurations, etc. on the [Wiki](https://reaction.ppom.me)! ## Installation @@ -172,23 +150,53 @@ We recommend that you read the ***Good Practices*** chapters before starting. ### Binaries -Executables and .deb packages are provided [in the releases page](https://framagit.org/ppom/reaction/-/releases/), for x86-64/amd64 linux and aarch64/arm64 linux. +Executables are provided [here](https://framagit.org/ppom/reaction/-/releases/), for a standard x86-64 linux machine. -Signature verification and installation instructions are provided in the releases page. +A standard place to put such executables is `/usr/local/bin/`. -> Provided binaries are compiled by running `nix-shell release.py` on a NixOS machine with docker installed. +> Provided binaries in the previous section are compiled this way: +```shell +$ docker run -it --rm -e HOME=/tmp/ -v $(pwd):/tmp/code -w /tmp/code -u $(id -u) golang:1.20 make clean reaction.deb +$ make signaturese +``` +#### Signature verification + +Starting at v1.0.3, all binaries are signed with public key `RWSpLTPfbvllNqRrXUgZzM7mFjLUA7PQioAItz80ag8uU4A2wtoT2DzX`. You can check their authenticity with minisign: +```bash +minisign -VP RWSpLTPfbvllNqRrXUgZzM7mFjLUA7PQioAItz80ag8uU4A2wtoT2DzX -m nft46 +minisign -VP RWSpLTPfbvllNqRrXUgZzM7mFjLUA7PQioAItz80ag8uU4A2wtoT2DzX -m ip46tables +minisign -VP RWSpLTPfbvllNqRrXUgZzM7mFjLUA7PQioAItz80ag8uU4A2wtoT2DzX -m reaction +# or +minisign -VP RWSpLTPfbvllNqRrXUgZzM7mFjLUA7PQioAItz80ag8uU4A2wtoT2DzX -m reaction.deb +``` + +#### Debian + +The releases also contain a `reaction.deb` file, which packages reaction & ip46tables. +You can install it using `sudo apt install ./reaction.deb`. +You'll have to create a configuration at `/etc/reaction.jsonnet`. + +If you want to use another configuration format (YAML or JSON), you can override systemd's `ExecStart` command in `/etc/systemd/system/reaction.service` like this: +```systemd +[Service] +# First an empty directive to reset the default one +ExecStart= +# Then put what you want +ExecStart=/usr/bin/reaction start -c /etc/reaction.yml +``` #### NixOS -reaction is packaged, but the [**module**](https://framagit.org/ppom/nixos/-/blob/main/modules/common/reaction.nix) has not yet been upstreamed. +- [ package ](https://framagit.org/ppom/nixos/-/blob/main/pkgs/reaction/default.nix) +- [ module ](https://framagit.org/ppom/nixos/-/blob/main/modules/common/reaction.nix) #### OpenBSD -See the [wiki](https://reaction.ppom.me/configurations/OpenBSD.html). +[wiki](https://reaction.ppom.me/configs/openbsd.html) ### Compilation -You'll need a recent rust toolchain for reaction and a c compiler for ip46tables. +You'll need the go (>= 1.20) toolchain for reaction and a c compiler for ip46tables. ```shell $ make ``` @@ -206,46 +214,9 @@ To install the systemd file as well make install_systemd ``` -## Contributing +## Development -> We, as participants in the open source ecosystem, are ethically responsible for the software -> and hardware we help create - as it can be used to perpetuate inequalities or help empower -> marginalized communities, and fight against patriarchy, capitalism, sexism, gender violence, -> racism, ableism, homophobia, colonialism, fascism, surveillance, and oppressive control. +Contributions are welcome. For any substantial feature, please file an issue first, to be assured that we agree on the feature, and to avoid unnecessary work. -- [NGI's Diversity and Inclusion Guide](https://nlnet.nl/NGI0/bestpractices/DiversityAndInclusionGuide-v4.pdf) - -I'll do my best to maintain a safe contribution place, as free as possible from discrimination and elitism. - -### Ideas - -Please take a look at issues which have the "Opinion Welcome 👀" label! -*Your opinion is welcome.* - -Your ideas are welcome in the issues. - -### Code - -Contributions are welcome. - -For any substantial feature, please file an issue first, to be assured that we agree on the feature, and to avoid unnecessary work. - -I recommend reading [`ARCHITECTURE.md`](ARCHITECTURE.md) first. This is a quick tour of the codebase, which should save time to new contributors. - -You can also join this Matrix development room: [#reaction-dev-en:club1.fr](https://matrix.to/#/#reaction-dev-en:club1.fr). -French version: [#reaction-dev-fr:club1.fr](https://matrix.to/#/#reaction-dev-fr:club1.fr). - -## Help - -You can ask for help in the issues or in this Matrix room: [#reaction-users-en:club1.fr](https://matrix.to/#/#reaction-users-en:club1.fr). -French version: [#reaction-users-fr:club1.fr](https://matrix.to/#/#reaction-users-fr:club1.fr). -You can alternatively send a mail: `reaction` on domain `ppom.me`. - -## Funding - - - -This project is currenlty funded through the [NGI0 Core](https://nlnet.nl/core) Fund, a fund established by [NLnet](https://nlnet.nl) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu) programme. - -![NLnet logo](logo/nlnet.svg) +This is a free time project, so I'm not working on schedule. +However, if you're willing to fund the project, I can priorise and plan paid work. This includes features, documentation and specific JSONnet configurations. diff --git a/TODO b/TODO deleted file mode 100644 index 2b6071f..0000000 --- a/TODO +++ /dev/null @@ -1,3 +0,0 @@ -Test what happens when a Filter's pattern Set changes (I think it's shitty) -DB: add tests on stress testing (lines should always be in order) -conf: merge filters diff --git a/app/client.go b/app/client.go new file mode 100644 index 0000000..ad61bea --- /dev/null +++ b/app/client.go @@ -0,0 +1,393 @@ +package app + +import ( + "bufio" + "encoding/gob" + "encoding/json" + "fmt" + "net" + "os" + "regexp" + "slices" + "strings" + "time" + + "framagit.org/ppom/reaction/logger" + "sigs.k8s.io/yaml" +) + +const ( + Info = 0 + Flush = 1 +) + +type Request struct { + Request int + Flush PSF +} + +type Response struct { + Err error + // Config Conf + Matches MatchesMap + Actions ActionsMap +} + +func SendAndRetrieve(data Request) Response { + conn, err := net.Dial("unix", *SocketPath) + if err != nil { + logger.Fatalln("Error opening connection to daemon:", err) + } + defer conn.Close() + + err = gob.NewEncoder(conn).Encode(data) + if err != nil { + logger.Fatalln("Can't send message:", err) + } + + var response Response + err = gob.NewDecoder(conn).Decode(&response) + if err != nil { + logger.Fatalln("Invalid answer from daemon:", err) + } + return response +} + +type PatternStatus struct { + Matches int `json:"matches,omitempty"` + Actions map[string][]string `json:"actions,omitempty"` +} +type MapPatternStatus map[Match]*PatternStatus +type MapPatternStatusFlush MapPatternStatus + +type ClientStatus map[string]map[string]MapPatternStatus +type ClientStatusFlush ClientStatus + +func (mps MapPatternStatusFlush) MarshalJSON() ([]byte, error) { + for _, v := range mps { + return json.Marshal(v) + } + return []byte(""), nil +} + +func (csf ClientStatusFlush) MarshalJSON() ([]byte, error) { + ret := make(map[string]map[string]MapPatternStatusFlush) + for k, v := range csf { + ret[k] = make(map[string]MapPatternStatusFlush) + for kk, vv := range v { + ret[k][kk] = MapPatternStatusFlush(vv) + } + } + return json.Marshal(ret) +} + +func pfMatches(streamName string, filterName string, regexes map[string]*regexp.Regexp, match Match, filter *Filter) bool { + // Check stream and filter match + if streamName != "" && streamName != filter.Stream.Name { + return false + } + if filterName != "" && filterName != filter.Name { + return false + } + // Check that all user requested patterns are in this filter + var nbMatched int + var localMatches = match.Split() + // For each pattern of this filter + for i, pattern := range filter.Pattern { + // Check that this pattern has user requested name + if reg, ok := regexes[pattern.Name]; ok { + // Check that the PF.p[i] matches user requested pattern + if reg.MatchString(localMatches[i]) { + nbMatched++ + } + } + } + if len(regexes) != nbMatched { + return false + } + // All checks passed + return true +} + +func addMatchToCS(cs ClientStatus, pf PF, times map[time.Time]struct{}) { + patterns, streamName, filterName := pf.P, pf.F.Stream.Name, pf.F.Name + if cs[streamName] == nil { + cs[streamName] = make(map[string]MapPatternStatus) + } + if cs[streamName][filterName] == nil { + cs[streamName][filterName] = make(MapPatternStatus) + } + cs[streamName][filterName][patterns] = &PatternStatus{len(times), nil} +} + +func addActionToCS(cs ClientStatus, pa PA, times map[time.Time]struct{}) { + patterns, streamName, filterName, actionName := pa.P, pa.A.Filter.Stream.Name, pa.A.Filter.Name, pa.A.Name + if cs[streamName] == nil { + cs[streamName] = make(map[string]MapPatternStatus) + } + if cs[streamName][filterName] == nil { + cs[streamName][filterName] = make(MapPatternStatus) + } + if cs[streamName][filterName][patterns] == nil { + cs[streamName][filterName][patterns] = new(PatternStatus) + } + ps := cs[streamName][filterName][patterns] + if ps.Actions == nil { + ps.Actions = make(map[string][]string) + } + for then := range times { + ps.Actions[actionName] = append(ps.Actions[actionName], then.Format(time.DateTime)) + } +} + +func printClientStatus(cs ClientStatus, format string) { + var text []byte + var err error + if format == "json" { + text, err = json.MarshalIndent(cs, "", " ") + } else { + text, err = yaml.Marshal(cs) + } + if err != nil { + logger.Fatalln("Failed to convert daemon binary response to text format:", err) + } + + fmt.Println(strings.ReplaceAll(string(text), "\\0", " ")) +} + +func compileKVPatterns(kvpatterns []string) map[string]*regexp.Regexp { + var regexes map[string]*regexp.Regexp + regexes = make(map[string]*regexp.Regexp) + for _, p := range kvpatterns { + // p syntax already checked in Main + key, value, found := strings.Cut(p, "=") + if !found { + logger.Printf(logger.ERROR, "Bad argument: no `=` in %v", p) + logger.Fatalln("Patterns must be prefixed by their name (e.g. ip=1.1.1.1)") + } + if regexes[key] != nil { + logger.Fatalf("Bad argument: same pattern name provided multiple times: %v", key) + } + compiled, err := regexp.Compile(fmt.Sprintf("^%v$", value)) + if err != nil { + logger.Fatalf("Bad argument: Could not compile: `%v`: %v", value, err) + } + regexes[key] = compiled + } + return regexes +} + +func ClientShow(format, stream, filter string, kvpatterns []string) { + response := SendAndRetrieve(Request{Info, PSF{}}) + if response.Err != nil { + logger.Fatalln("Received error from daemon:", response.Err) + } + + cs := make(ClientStatus) + + var regexes map[string]*regexp.Regexp + + if len(kvpatterns) != 0 { + regexes = compileKVPatterns(kvpatterns) + } + + var found bool + + // Painful data manipulation + for pf, times := range response.Matches { + // Check this PF is not empty + if len(times) == 0 { + continue + } + if !pfMatches(stream, filter, regexes, pf.P, pf.F) { + continue + } + addMatchToCS(cs, pf, times) + found = true + } + + // Painful data manipulation + for pa, times := range response.Actions { + // Check this PF is not empty + if len(times) == 0 { + continue + } + if !pfMatches(stream, filter, regexes, pa.P, pa.A.Filter) { + continue + } + addActionToCS(cs, pa, times) + found = true + } + + if !found { + logger.Println(logger.WARN, "No matching stream.filter items found. This does not mean it doesn't exist, maybe it just didn't receive any match.") + os.Exit(1) + } + + printClientStatus(cs, format) + + os.Exit(0) +} + +// TODO : Show values we just flushed - for now we got no details : +/* + * % ./reaction flush -l ssh.failedlogin login=".*t" + * ssh: + * failedlogin: + * actions: + * unban: + * - "2024-04-30 15:27:28" + * - "2024-04-30 15:27:28" + * - "2024-04-30 15:27:28" + * - "2024-04-30 15:27:28" + * + */ +func ClientFlush(format, streamName, filterName string, patterns []string) { + requestedPatterns := compileKVPatterns(patterns) + + // Remember which Filters are compatible with the query + filterCompatibility := make(map[SF]bool) + isCompatible := func(filter *Filter) bool { + sf := SF{filter.Stream.Name, filter.Name} + compatible, ok := filterCompatibility[sf] + + // already tested + if ok { + return compatible + } + + for k := range requestedPatterns { + if -1 == slices.IndexFunc(filter.Pattern, func(pattern *Pattern) bool { + return pattern.Name == k + }) { + filterCompatibility[sf] = false + return false + } + } + filterCompatibility[sf] = true + return true + } + + // match functions + kvMatch := func(filter *Filter, filterPatterns []string) bool { + // For each user requested pattern + for k, v := range requestedPatterns { + // Find its index on the Filter.Pattern + for i, pattern := range filter.Pattern { + if k == pattern.Name { + // Test the match + if !v.MatchString(filterPatterns[i]) { + return false + } + } + } + } + return true + } + + var found bool + fullMatch := func(filter *Filter, match Match) bool { + // Test if we limit by stream + if streamName == "" || filter.Stream.Name == streamName { + // Test if we limit by filter + if filterName == "" || filter.Name == filterName { + found = true + filterPatterns := match.Split() + return isCompatible(filter) && kvMatch(filter, filterPatterns) + } + } + return false + } + + response := SendAndRetrieve(Request{Info, PSF{}}) + if response.Err != nil { + logger.Fatalln("Received error from daemon:", response.Err) + } + + commands := make([]PSF, 0) + + cs := make(ClientStatus) + + for pf, times := range response.Matches { + if fullMatch(pf.F, pf.P) { + commands = append(commands, PSF{pf.P, pf.F.Stream.Name, pf.F.Name}) + addMatchToCS(cs, pf, times) + } + } + + for pa, times := range response.Actions { + if fullMatch(pa.A.Filter, pa.P) { + commands = append(commands, PSF{pa.P, pa.A.Filter.Stream.Name, pa.A.Filter.Name}) + addActionToCS(cs, pa, times) + } + } + + if !found { + logger.Println(logger.WARN, "No matching stream.filter items found. This does not mean it doesn't exist, maybe it just didn't receive any match.") + os.Exit(1) + } + + for _, psf := range commands { + response := SendAndRetrieve(Request{Flush, psf}) + if response.Err != nil { + logger.Fatalln("Received error from daemon:", response.Err) + } + } + + printClientStatus(cs, format) + os.Exit(0) +} + +func TestRegex(confFilename, regex, line string) { + conf := parseConf(confFilename) + + // Code close to app/startup.go + var usedPatterns []*Pattern + for _, pattern := range conf.Patterns { + if strings.Contains(regex, pattern.nameWithBraces) { + usedPatterns = append(usedPatterns, pattern) + regex = strings.Replace(regex, pattern.nameWithBraces, pattern.Regex, 1) + } + } + reg, err := regexp.Compile(regex) + if err != nil { + logger.Fatalln("ERROR the specified regex is invalid: %v", err) + os.Exit(1) + } + + // Code close to app/daemon.go + match := func(line string) { + var ignored bool + if matches := reg.FindStringSubmatch(line); matches != nil { + if usedPatterns != nil { + var result []string + for _, p := range usedPatterns { + match := matches[reg.SubexpIndex(p.Name)] + result = append(result, match) + if !p.notAnIgnore(&match) { + ignored = true + } + } + if !ignored { + fmt.Printf("\033[32mmatching\033[0m %v: %v\n", WithBrackets(result), line) + } else { + fmt.Printf("\033[33mignore matching\033[0m %v: %v\n", WithBrackets(result), line) + } + } else { + fmt.Printf("\033[32mmatching\033[0m [%v]:\n", line) + } + } else { + fmt.Printf("\033[31mno match\033[0m: %v\n", line) + } + } + + if line != "" { + match(line) + } else { + logger.Println(logger.INFO, "no second argument: reading from stdin") + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + match(scanner.Text()) + } + } +} diff --git a/app/daemon.go b/app/daemon.go new file mode 100644 index 0000000..ba291bd --- /dev/null +++ b/app/daemon.go @@ -0,0 +1,447 @@ +package app + +import ( + "bufio" + "os" + "os/exec" + "os/signal" + "strings" + "sync" + "syscall" + "time" + + "framagit.org/ppom/reaction/logger" +) + +// Executes a command and channel-send its stdout +func cmdStdout(commandline []string) chan *string { + lines := make(chan *string) + + go func() { + cmd := exec.Command(commandline[0], commandline[1:]...) + stdout, err := cmd.StdoutPipe() + if err != nil { + logger.Fatalln("couldn't open stdout on command:", err) + } + if err := cmd.Start(); err != nil { + logger.Fatalln("couldn't start command:", err) + } + defer stdout.Close() + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + line := scanner.Text() + lines <- &line + logger.Println(logger.DEBUG, "stdout:", line) + } + close(lines) + }() + + return lines +} + +func runCommands(commands [][]string, moment string) bool { + ok := true + for _, command := range commands { + cmd := exec.Command(command[0], command[1:]...) + cmd.WaitDelay = time.Minute + + logger.Printf(logger.INFO, "%v command: run %v\n", moment, command) + + if err := cmd.Start(); err != nil { + logger.Printf(logger.ERROR, "%v command: run %v: %v", moment, command, err) + ok = false + } else { + err := cmd.Wait() + if err != nil { + logger.Printf(logger.ERROR, "%v command: run %v: %v", moment, command, err) + ok = false + } + } + } + return ok +} + +func (p *Pattern) notAnIgnore(match *string) bool { + for _, regex := range p.compiledIgnoreRegex { + if regex.MatchString(*match) { + return false + } + } + + for _, ignore := range p.Ignore { + if ignore == *match { + return false + } + } + return true +} + +// Whether one of the filter's regexes is matched on a line +func (f *Filter) match(line *string) Match { + for _, regex := range f.compiledRegex { + + if matches := regex.FindStringSubmatch(*line); matches != nil { + if f.Pattern != nil { + var result []string + for _, p := range f.Pattern { + match := matches[regex.SubexpIndex(p.Name)] + if p.notAnIgnore(&match) { + result = append(result, match) + } + } + if len(result) == len(f.Pattern) { + logger.Printf(logger.INFO, "%s.%s: match %s", f.Stream.Name, f.Name, WithBrackets(result)) + return JoinMatch(result) + } + } else { + logger.Printf(logger.INFO, "%s.%s: match [.]\n", f.Stream.Name, f.Name) + // No pattern, so this match will never actually be used + return "." + } + } + } + return "" +} + +func (f *Filter) sendActions(match Match, at time.Time) { + for _, a := range f.Actions { + actionsC <- PAT{match, a, at.Add(a.afterDuration)} + } +} + +func (a *Action) exec(match Match) { + defer wgActions.Done() + + var computedCommand []string + + if a.Filter.Pattern != nil { + computedCommand = make([]string, 0, len(a.Cmd)) + matches := match.Split() + + for _, item := range a.Cmd { + for i, p := range a.Filter.Pattern { + item = strings.ReplaceAll(item, p.nameWithBraces, matches[i]) + } + computedCommand = append(computedCommand, item) + } + } else { + computedCommand = a.Cmd + } + + logger.Printf(logger.INFO, "%s.%s.%s: run %s\n", a.Filter.Stream.Name, a.Filter.Name, a.Name, computedCommand) + + cmd := exec.Command(computedCommand[0], computedCommand[1:]...) + + if ret := cmd.Run(); ret != nil { + logger.Printf(logger.ERROR, "%s.%s.%s: run %s, code %s\n", a.Filter.Stream.Name, a.Filter.Name, a.Name, computedCommand, ret) + } +} + +func ActionsManager(concurrency int) { + // concurrency init + execActionsC := make(chan PA) + if concurrency > 0 { + for i := 0; i < concurrency; i++ { + go func() { + var pa PA + for { + pa = <-execActionsC + pa.A.exec(pa.P) + } + }() + } + } else { + go func() { + var pa PA + for { + pa = <-execActionsC + go func(pa PA) { + pa.A.exec(pa.P) + }(pa) + } + }() + } + execAction := func(a *Action, p Match) { + wgActions.Add(1) + execActionsC <- PA{p, a} + } + + // main + pendingActionsC := make(chan PAT) + for { + select { + case pat := <-actionsC: + pa := PA{pat.P, pat.A} + pattern, action, then := pat.P, pat.A, pat.T + now := time.Now() + // check if must be executed now + if then.Compare(now) <= 0 { + execAction(action, pattern) + } else { + if actions[pa] == nil { + actions[pa] = make(map[time.Time]struct{}) + } + actions[pa][then] = struct{}{} + go func(insidePat PAT, insideNow time.Time) { + time.Sleep(insidePat.T.Sub(insideNow)) + pendingActionsC <- insidePat + }(pat, now) + } + case pat := <-pendingActionsC: + pa := PA{pat.P, pat.A} + pattern, action, then := pat.P, pat.A, pat.T + if actions[pa] != nil { + delete(actions[pa], then) + execAction(action, pattern) + } + case fo := <-flushToActionsC: + for pa := range actions { + if fo.S == pa.A.Filter.Stream.Name && + fo.F == pa.A.Filter.Name && + fo.P == pa.P { + for range actions[pa] { + execAction(pa.A, pa.P) + } + delete(actions, pa) + break + } + } + case _, _ = <-stopActions: + for pa := range actions { + if pa.A.OnExit { + for range actions[pa] { + execAction(pa.A, pa.P) + } + } + } + wgActions.Done() + return + } + } +} + +func MatchesManager() { + var fo PSF + var pft PFT + end := false + + for !end { + select { + case fo = <-flushToMatchesC: + matchesManagerHandleFlush(fo) + case fo, ok := <-startupMatchesC: + if !ok { + end = true + } else { + _ = matchesManagerHandleMatch(fo) + } + } + } + + for { + select { + case fo = <-flushToMatchesC: + matchesManagerHandleFlush(fo) + case pft = <-matchesC: + + entry := LogEntry{pft.T, 0, pft.P, pft.F.Stream.Name, pft.F.Name, 0, false} + + entry.Exec = matchesManagerHandleMatch(pft) + + logsC <- entry + } + } +} + +func matchesManagerHandleFlush(fo PSF) { + matchesLock.Lock() + for pf := range matches { + if fo.S == pf.F.Stream.Name && + fo.F == pf.F.Name && + fo.P == pf.P { + delete(matches, pf) + break + } + } + matchesLock.Unlock() +} + +func matchesManagerHandleMatch(pft PFT) bool { + matchesLock.Lock() + defer matchesLock.Unlock() + + filter, patterns, then := pft.F, pft.P, pft.T + pf := PF{pft.P, pft.F} + + if filter.Retry > 1 { + // make sure map exists + if matches[pf] == nil { + matches[pf] = make(map[time.Time]struct{}) + } + // add new match + matches[pf][then] = struct{}{} + // remove match when expired + go func(pf PF, then time.Time) { + time.Sleep(then.Sub(time.Now()) + filter.retryDuration) + matchesLock.Lock() + if matches[pf] != nil { + // FIXME replace this and all similar occurences + // by clear() when switching to go 1.21 + delete(matches[pf], then) + } + matchesLock.Unlock() + }(pf, then) + } + + if filter.Retry <= 1 || len(matches[pf]) >= filter.Retry { + delete(matches, pf) + filter.sendActions(patterns, then) + return true + } + return false +} + +func StreamManager(s *Stream, endedSignal chan *Stream) { + defer wgStreams.Done() + logger.Printf(logger.INFO, "%s: start %s\n", s.Name, s.Cmd) + + lines := cmdStdout(s.Cmd) + for { + select { + case line, ok := <-lines: + if !ok { + endedSignal <- s + return + } + for _, filter := range s.Filters { + if match := filter.match(line); match != "" { + matchesC <- PFT{match, filter, time.Now()} + } + } + case _, _ = <-stopStreams: + return + } + } + +} + +var actions ActionsMap +var matches MatchesMap +var matchesLock sync.Mutex + +var stopStreams chan bool +var stopActions chan bool +var wgActions sync.WaitGroup +var wgStreams sync.WaitGroup + +/* + + ↓ +StreamManager onstartup:matches + ↓ ↓ ↑ + matches→ MatchesManager →logs→ DatabaseManager ←· + ↑ ↓ ↑ + ↑ actions→ ActionsManager ↑ + ↑ ↑ ↑ +SocketManager →flushes→→→→→→→→→→·→→→→→→→→→→→→→→→→· + ↑ + +*/ + +// DatabaseManager → MatchesManager +var startupMatchesC chan PFT + +// StreamManager → MatchesManager +var matchesC chan PFT + +// MatchesManager → DatabaseManager +var logsC chan LogEntry + +// MatchesManager → ActionsManager +var actionsC chan PAT + +// SocketManager, DatabaseManager → MatchesManager +var flushToMatchesC chan PSF + +// SocketManager → ActionsManager +var flushToActionsC chan PSF + +// SocketManager → DatabaseManager +var flushToDatabaseC chan LogEntry + +func Daemon(confFilename string) { + conf := parseConf(confFilename) + + startupMatchesC = make(chan PFT) + matchesC = make(chan PFT) + logsC = make(chan LogEntry) + actionsC = make(chan PAT) + flushToMatchesC = make(chan PSF) + flushToActionsC = make(chan PSF) + flushToDatabaseC = make(chan LogEntry) + stopActions = make(chan bool) + stopStreams = make(chan bool) + actions = make(ActionsMap) + matches = make(MatchesMap) + + _ = runCommands(conf.Start, "start") + + go DatabaseManager(conf) + go MatchesManager() + go ActionsManager(conf.Concurrency) + + // Ready to start + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + + endSignals := make(chan *Stream) + nbStreamsInExecution := len(conf.Streams) + + for _, stream := range conf.Streams { + wgStreams.Add(1) + go StreamManager(stream, endSignals) + } + + go SocketManager(conf) + + for { + select { + case finishedStream := <-endSignals: + logger.Printf(logger.ERROR, "%s stream finished", finishedStream.Name) + nbStreamsInExecution-- + if nbStreamsInExecution == 0 { + quit(conf, false) + } + case <-sigs: + logger.Printf(logger.INFO, "Received SIGINT/SIGTERM, exiting") + quit(conf, true) + } + } +} + +func quit(conf *Conf, graceful bool) { + // send stop to StreamManager·s + close(stopStreams) + logger.Println(logger.INFO, "Waiting for Streams to finish...") + wgStreams.Wait() + // ActionsManager calls wgActions.Done() when it has launched all pending actions + wgActions.Add(1) + // send stop to ActionsManager + close(stopActions) + // stop all actions + logger.Println(logger.INFO, "Waiting for Actions to finish...") + wgActions.Wait() + // run stop commands + stopOk := runCommands(conf.Stop, "stop") + // delete pipe + err := os.Remove(*SocketPath) + if err != nil { + logger.Println(logger.ERROR, "Failed to remove socket:", err) + } + + if !stopOk || !graceful { + os.Exit(1) + } + os.Exit(0) +} diff --git a/app/example.yml b/app/example.yml new file mode 100644 index 0000000..759f597 --- /dev/null +++ b/app/example.yml @@ -0,0 +1,108 @@ +--- +# This example configuration file is a good starting point, but you're +# strongly encouraged to take a look at the full documentation: https://reaction.ppom.me +# +# This file is using the well-established YAML configuration language. +# Note that the more powerful JSONnet configuration language is also supported +# and that the documentation uses JSONnet + +# definitions are just a place to put chunks of conf you want to reuse in another place +# using YAML anchors `&name` and pointers `*name` +# definitions are not readed by reaction +definitions: + - &iptablesban [ 'ip46tables', '-w', '-A', 'reaction', '-s', '', '-j', 'DROP' ] + - &iptablesunban [ 'ip46tables', '-w', '-D', 'reaction', '-s', '', '-j', 'DROP' ] +# ip46tables is a minimal C program (only POSIX dependencies) present as a subdirectory. +# it permits to handle both ipv4/iptables and ipv6/ip6tables commands + +# if set to a positive number → max number of concurrent actions +# if set to a negative number → no limit +# if not specified or set to 0 → defaults to the number of CPUs on the system +concurrency: 0 + +# patterns are substitued in regexes. +# when a filter performs an action, it replaces the found pattern +patterns: + ip: + # reaction regex syntax is defined here: https://github.com/google/re2/wiki/Syntax + # simple version: regex: '(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:]{2,90})' + regex: '(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|(?:(?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))' + ignore: + - 127.0.0.1 + - ::1 + # Patterns can be ignored based on regexes, it will try to match the whole string detected by the pattern + # ignoreregex: + # - '10\.0\.[0-9]{1,3}\.[0-9]{1,3}' + +# Those commands will be executed in order at start, before everything else +start: + - [ 'ip46tables', '-w', '-N', 'reaction' ] + - [ 'ip46tables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ] + - [ 'ip46tables', '-w', '-I', 'FORWARD', '-p', 'all', '-j', 'reaction' ] + +# Those commands will be executed in order at stop, after everything else +stop: + - [ 'ip46tables', '-w,', '-D', 'INPUT', '-p', 'all', '-j', 'reaction' ] + - [ 'ip46tables', '-w,', '-D', 'FORWARD', '-p', 'all', '-j', 'reaction' ] + - [ 'ip46tables', '-w', '-F', 'reaction' ] + - [ 'ip46tables', '-w', '-X', 'reaction' ] + +# streams are commands +# they are run and their ouptut is captured +# *example:* `tail -f /var/log/nginx/access.log` +# their output will be used by one or more filters +streams: + # streams have a user-defined name + ssh: + # note that if the command is not in environment's `PATH` + # its full path must be given. + cmd: [ 'journalctl', '-n0', '-fu', 'sshd.service' ] + # filters run actions when they match regexes on a stream + filters: + # filters have a user-defined name + failedlogin: + # reaction's regex syntax is defined here: https://github.com/google/re2/wiki/Syntax + regex: + # is predefined in the patterns section + # ip's regex is inserted in the following regex + - 'authentication failure;.*rhost=' + - 'Failed password for .* from ' + - 'Connection (reset|closed) by (authenticating|invalid) user .* ' + # if retry and retryperiod are defined, + # the actions will only take place if a same pattern is + # found `retry` times in a `retryperiod` interval + retry: 3 + # format is defined here: https://pkg.go.dev/time#ParseDuration + retryperiod: 6h + # actions are run by the filter when regexes are matched + actions: + # actions have a user-defined name + ban: + # YAML substitutes *reference by the value anchored at &reference + cmd: *iptablesban + unban: + cmd: *iptablesunban + # if after is defined, the action will not take place immediately, but after a specified duration + # same format as retryperiod + after: 48h + # let's say reaction is quitting. does it run all those pending commands which had an `after` duration set? + # if you want reaction to run those pending commands before exiting, you can set this: + # onexit: true + # (defaults to false) + # here it is not useful because we will flush and delete the chain containing the bans anyway + # (with the stop commands) + +# persistence +# tldr; when an `after` action is set in a filter, such filter acts as a 'jail', +# which is persisted after reboots. +# +# when a filter is triggered, there are 2 flows: +# +# if none of its actions have an `after` directive set: +# no action will be replayed. +# +# else (if at least one action has an `after` directive set): +# if reaction stops while `after` actions are pending: +# and reaction starts again while those actions would still be pending: +# reaction executes the past actions (actions without after or with then+after < now) +# and plans the execution of future actions (actions with then+after > now) diff --git a/app/main.go b/app/main.go new file mode 100644 index 0000000..8d3d284 --- /dev/null +++ b/app/main.go @@ -0,0 +1,230 @@ +package app + +import ( + _ "embed" + "flag" + "fmt" + "os" + "strings" + + "framagit.org/ppom/reaction/logger" +) + +func addStringFlag(names []string, defvalue string, f *flag.FlagSet) *string { + var value string + for _, name := range names { + f.StringVar(&value, name, defvalue, "") + } + return &value +} + +func addBoolFlag(names []string, f *flag.FlagSet) *bool { + var value bool + for _, name := range names { + f.BoolVar(&value, name, false, "") + } + return &value +} + +var SocketPath *string + +func addSocketFlag(f *flag.FlagSet) *string { + return addStringFlag([]string{"s", "socket"}, "/run/reaction/reaction.sock", f) +} + +func addConfFlag(f *flag.FlagSet) *string { + return addStringFlag([]string{"c", "config"}, "", f) +} + +func addFormatFlag(f *flag.FlagSet) *string { + return addStringFlag([]string{"f", "format"}, "yaml", f) +} + +func addLimitFlag(f *flag.FlagSet) *string { + return addStringFlag([]string{"l", "limit"}, "", f) +} + +func addLevelFlag(f *flag.FlagSet) *string { + return addStringFlag([]string{"l", "loglevel"}, "INFO", f) +} + +func subCommandParse(f *flag.FlagSet, maxRemainingArgs int) { + help := addBoolFlag([]string{"h", "help"}, f) + f.Parse(os.Args[2:]) + if *help { + basicUsage() + os.Exit(0) + } + // -1 = no limit to remaining args + if maxRemainingArgs > -1 && len(f.Args()) > maxRemainingArgs { + fmt.Printf("ERROR unrecognized argument(s): %v\n", f.Args()[maxRemainingArgs:]) + basicUsage() + os.Exit(1) + } +} + +func basicUsage() { + const ( + bold = "\033[1m" + reset = "\033[0m" + ) + fmt.Print( + bold + `reaction help` + reset + ` + # print this help message + +` + bold + `reaction start` + reset + ` + # start the daemon + + # options: + -c/--config CONFIG_FILE # configuration file in json, jsonnet or yaml format (required) + -l/--loglevel LEVEL # minimum log level to show, in DEBUG, INFO, WARN, ERROR, FATAL + # (default: INFO) + -s/--socket SOCKET # path to the client-daemon communication socket + # (default: /run/reaction/reaction.sock) + +` + bold + `reaction example-conf` + reset + ` + # print a configuration file example + +` + bold + `reaction show` + reset + ` [NAME=PATTERN...] + # show current matches and which actions are still to be run for the specified PATTERN regexe(s) + # (e.g know what is currenly banned) + + reaction show + reaction show "ip=192.168.1.1" + reaction show "ip=192\.168\..*" login=root + + # options: + -s/--socket SOCKET # path to the client-daemon communication socket + -f/--format yaml|json # (default: yaml) + -l/--limit STREAM[.FILTER] # only show items related to this STREAM (or STREAM.FILTER) + +` + bold + `reaction flush` + reset + ` NAME=PATTERN [NAME=PATTERN...] + # remove currently active matches and run currently pending actions for the specified PATTERN regexe(s) + # (then show flushed matches and actions) + + reaction flush "ip=192.168.1.1" + reaction flush "ip=192\.168\..*" login=root + + # options: + -s/--socket SOCKET # path to the client-daemon communication socket + -f/--format yaml|json # (default: yaml) + -l/--limit STREAM.FILTER # flush only items related to this STREAM.FILTER + +` + bold + `reaction test-regex` + reset + ` REGEX LINE # test REGEX against LINE +cat FILE | ` + bold + `reaction test-regex` + reset + ` REGEX # test REGEX against each line of FILE + + # options: + -c/--config CONFIG_FILE # configuration file in json, jsonnet or yaml format + # optional: permits to use configured patterns like in regex + +` + bold + `reaction version` + reset + ` + # print version information + +see usage examples, service configurations and good practices +on the ` + bold + `wiki` + reset + `: https://reaction.ppom.me +`) +} + +//go:embed example.yml +var exampleConf string + +func Main(version, commit string) { + if len(os.Args) <= 1 { + logger.Fatalln("No argument provided. Try `reaction help`") + basicUsage() + os.Exit(1) + } + f := flag.NewFlagSet(os.Args[1], flag.ExitOnError) + switch os.Args[1] { + case "help", "-h", "-help", "--help": + basicUsage() + + case "version", "-v", "--version": + fmt.Printf("reaction version %v commit %v\n", version, commit) + + case "example-conf": + subCommandParse(f, 0) + fmt.Print(exampleConf) + + case "start": + SocketPath = addSocketFlag(f) + confFilename := addConfFlag(f) + logLevel := addLevelFlag(f) + subCommandParse(f, 0) + if *confFilename == "" { + logger.Fatalln("no configuration file provided") + basicUsage() + os.Exit(1) + } + logLevelType := logger.FromString(*logLevel) + if logLevelType == logger.UNKNOWN { + logger.Fatalf("Log Level %v not recognized", logLevel) + basicUsage() + os.Exit(1) + } + logger.SetLogLevel(logLevelType) + Daemon(*confFilename) + + case "show": + SocketPath = addSocketFlag(f) + queryFormat := addFormatFlag(f) + limit := addLimitFlag(f) + subCommandParse(f, -1) + if *queryFormat != "yaml" && *queryFormat != "json" { + logger.Fatalln("only yaml and json formats are supported") + } + stream, filter := "", "" + if *limit != "" { + splitSF := strings.Split(*limit, ".") + stream = splitSF[0] + if len(splitSF) == 2 { + filter = splitSF[1] + } else if len(splitSF) > 2 { + logger.Fatalln("-l/--limit: only one . separator is supported") + } + } + ClientShow(*queryFormat, stream, filter, f.Args()) + + case "flush": + SocketPath = addSocketFlag(f) + queryFormat := addFormatFlag(f) + limit := addLimitFlag(f) + subCommandParse(f, -1) + if *queryFormat != "yaml" && *queryFormat != "json" { + logger.Fatalln("only yaml and json formats are supported") + } + if len(f.Args()) == 0 { + logger.Fatalln("subcommand flush takes at least one TARGET argument") + } + stream, filter := "", "" + if *limit != "" { + splitSF := strings.Split(*limit, ".") + stream = splitSF[0] + if len(splitSF) == 2 { + filter = splitSF[1] + } else if len(splitSF) > 2 { + logger.Fatalln("-l/--limit: only one . separator is supported") + } + } + ClientFlush(*queryFormat, stream, filter, f.Args()) + + case "test-regex": + // socket not needed, no interaction with the daemon + confFilename := addConfFlag(f) + subCommandParse(f, 2) + if *confFilename == "" { + logger.Println(logger.WARN, "no configuration file provided. Can't make use of registered patterns.") + } + if f.Arg(0) == "" { + logger.Fatalln("subcommand test-regex takes at least one REGEX argument") + basicUsage() + os.Exit(1) + } + TestRegex(*confFilename, f.Arg(0), f.Arg(1)) + + default: + logger.Fatalf("subcommand %v not recognized. Try `reaction help`", os.Args[1]) + basicUsage() + os.Exit(1) + } +} diff --git a/app/persist.go b/app/persist.go new file mode 100644 index 0000000..78e78a1 --- /dev/null +++ b/app/persist.go @@ -0,0 +1,264 @@ +package app + +import ( + "encoding/gob" + "errors" + "io" + "os" + "time" + + "framagit.org/ppom/reaction/logger" +) + +const ( + logDBName = "./reaction-matches.db" + logDBNewName = "./reaction-matches.new.db" + flushDBName = "./reaction-flushes.db" +) + +func openDB(path string) (bool, *ReadDB) { + file, err := os.Open(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + logger.Printf(logger.WARN, "No DB found at %s. It's ok if this is the first time reaction is running.\n", path) + return true, nil + } + logger.Fatalln("Failed to open DB:", err) + } + return false, &ReadDB{file, gob.NewDecoder(file)} +} + +func createDB(path string) *WriteDB { + file, err := os.Create(path) + if err != nil { + logger.Fatalln("Failed to create DB:", err) + } + return &WriteDB{file, gob.NewEncoder(file)} +} + +func DatabaseManager(c *Conf) { + logDB, flushDB := c.RotateDB(true) + close(startupMatchesC) + c.manageLogs(logDB, flushDB) +} + +func (c *Conf) manageLogs(logDB *WriteDB, flushDB *WriteDB) { + cpt := 0 + writeSF2int := make(map[SF]int) + writeCpt := 1 + for { + select { + case entry := <-flushToDatabaseC: + flushDB.enc.Encode(entry) + case entry := <-logsC: + encodeOrFatal(logDB.enc, entry, writeSF2int, &writeCpt) + cpt++ + // let's say 100 000 entries ~ 10 MB + if cpt == 500_000 { + cpt = 0 + logger.Printf(logger.INFO, "Rotating database...") + logDB.file.Close() + flushDB.file.Close() + logDB, flushDB = c.RotateDB(false) + logger.Printf(logger.INFO, "Rotated database") + } + } + } +} + +func (c *Conf) RotateDB(startup bool) (*WriteDB, *WriteDB) { + var ( + doesntExist bool + err error + logReadDB *ReadDB + flushReadDB *ReadDB + logWriteDB *WriteDB + flushWriteDB *WriteDB + ) + doesntExist, logReadDB = openDB(logDBName) + if doesntExist { + return createDB(logDBName), createDB(flushDBName) + } + doesntExist, flushReadDB = openDB(flushDBName) + if doesntExist { + logger.Println(logger.WARN, "Strange! No flushes db, opening /dev/null instead") + doesntExist, flushReadDB = openDB("/dev/null") + if doesntExist { + logger.Fatalln("Opening dummy /dev/null failed") + } + } + + logWriteDB = createDB(logDBNewName) + + rotateDB(c, logReadDB.dec, flushReadDB.dec, logWriteDB.enc, startup) + + err = logReadDB.file.Close() + if err != nil { + logger.Fatalln("Failed to close old DB:", err) + } + + // It should be ok to rename an open file + err = os.Rename(logDBNewName, logDBName) + if err != nil { + logger.Fatalln("Failed to replace old DB with new one:", err) + } + + err = os.Remove(flushDBName) + if err != nil && !errors.Is(err, os.ErrNotExist) { + logger.Fatalln("Failed to delete old DB:", err) + } + + flushWriteDB = createDB(flushDBName) + return logWriteDB, flushWriteDB +} + +func rotateDB(c *Conf, logDec *gob.Decoder, flushDec *gob.Decoder, logEnc *gob.Encoder, startup bool) { + // This mapping is a space optimization feature + // It permits to compress stream+filter to a small number (which is a byte in gob) + // We do this only for matches, not for flushes + readSF2int := make(map[int]SF) + writeSF2int := make(map[SF]int) + writeCounter := 1 + // This extra code is made to warn only one time for each non-existant filter + discardedEntries := make(map[SF]int) + malformedEntries := 0 + defer func() { + for sf, t := range discardedEntries { + if t > 0 { + logger.Printf(logger.WARN, "info discarded %v times from the DBs: stream/filter not found: %s.%s\n", t, sf.S, sf.F) + } + } + if malformedEntries > 0 { + logger.Printf(logger.WARN, "%v malformed entries discarded from the DBs\n", malformedEntries) + } + }() + + // pattern, stream, fitler → last flush + flushes := make(map[*PSF]time.Time) + for { + var entry LogEntry + var filter *Filter + // decode entry + err := flushDec.Decode(&entry) + if err != nil { + if err == io.EOF { + break + } + malformedEntries++ + continue + } + + // retrieve related filter + if entry.Stream != "" || entry.Filter != "" { + if stream := c.Streams[entry.Stream]; stream != nil { + filter = stream.Filters[entry.Filter] + } + if filter == nil { + discardedEntries[SF{entry.Stream, entry.Filter}]++ + continue + } + } + + // store + flushes[&PSF{entry.Pattern, entry.Stream, entry.Filter}] = entry.T + } + + lastTimeCpt := int64(0) + now := time.Now() + for { + var entry LogEntry + var filter *Filter + + // decode entry + err := logDec.Decode(&entry) + if err != nil { + if err == io.EOF { + break + } + malformedEntries++ + continue + } + + // retrieve related stream & filter + if entry.Stream == "" && entry.Filter == "" { + sf, ok := readSF2int[entry.SF] + if !ok { + discardedEntries[SF{"", ""}]++ + continue + } + entry.Stream = sf.S + entry.Filter = sf.F + } + if stream := c.Streams[entry.Stream]; stream != nil { + filter = stream.Filters[entry.Filter] + } + if filter == nil { + discardedEntries[SF{entry.Stream, entry.Filter}]++ + continue + } + if entry.SF != 0 { + readSF2int[entry.SF] = SF{entry.Stream, entry.Filter} + } + + // check if number of patterns is in sync + if len(entry.Pattern.Split()) != len(filter.Pattern) { + continue + } + + // check if it hasn't been flushed + lastGlobalFlush := flushes[&PSF{entry.Pattern, "", ""}].Unix() + lastLocalFlush := flushes[&PSF{entry.Pattern, entry.Stream, entry.Filter}].Unix() + entryTime := entry.T.Unix() + if lastLocalFlush > entryTime || lastGlobalFlush > entryTime { + continue + } + + // restore time + if entry.T.IsZero() { + entry.T = time.Unix(entry.S, lastTimeCpt) + } + lastTimeCpt++ + + // store matches + if !entry.Exec && entry.T.Add(filter.retryDuration).Unix() > now.Unix() { + if startup { + startupMatchesC <- PFT{entry.Pattern, filter, entry.T} + } + + encodeOrFatal(logEnc, entry, writeSF2int, &writeCounter) + } + + // replay executions + if entry.Exec && entry.T.Add(*filter.longuestActionDuration).Unix() > now.Unix() { + if startup { + flushToMatchesC <- PSF{entry.Pattern, entry.Stream, entry.Filter} + filter.sendActions(entry.Pattern, entry.T) + } + + encodeOrFatal(logEnc, entry, writeSF2int, &writeCounter) + } + } +} + +func encodeOrFatal(enc *gob.Encoder, entry LogEntry, writeSF2int map[SF]int, writeCounter *int) { + // Stream/Filter reduction + sf, ok := writeSF2int[SF{entry.Stream, entry.Filter}] + if ok { + entry.SF = sf + entry.Stream = "" + entry.Filter = "" + } else { + entry.SF = *writeCounter + writeSF2int[SF{entry.Stream, entry.Filter}] = *writeCounter + *writeCounter++ + } + // Time reduction + if !entry.T.IsZero() { + entry.S = entry.T.Unix() + entry.T = time.Time{} + } + err := enc.Encode(entry) + if err != nil { + logger.Fatalln("Failed to write to new DB:", err) + } +} diff --git a/app/pipe.go b/app/pipe.go new file mode 100644 index 0000000..0d5b4ac --- /dev/null +++ b/app/pipe.go @@ -0,0 +1,81 @@ +package app + +import ( + "encoding/gob" + "errors" + "net" + "os" + "path" + "time" + + "framagit.org/ppom/reaction/logger" +) + +func createOpenSocket() net.Listener { + err := os.MkdirAll(path.Dir(*SocketPath), 0755) + if err != nil { + logger.Fatalln("Failed to create socket directory") + } + _, err = os.Stat(*SocketPath) + if err == nil { + logger.Println(logger.WARN, "socket", SocketPath, "already exists: Is the daemon already running? Deleting.") + err = os.Remove(*SocketPath) + if err != nil { + logger.Fatalln("Failed to remove socket:", err) + } + } + ln, err := net.Listen("unix", *SocketPath) + if err != nil { + logger.Fatalln("Failed to create socket:", err) + } + return ln +} + +// Handle connections +//func SocketManager(streams map[string]*Stream) { +func SocketManager(conf *Conf) { + ln := createOpenSocket() + defer ln.Close() + for { + conn, err := ln.Accept() + if err != nil { + logger.Println(logger.ERROR, "Failed to open connection from cli:", err) + continue + } + go func(conn net.Conn) { + defer conn.Close() + var request Request + var response Response + + err := gob.NewDecoder(conn).Decode(&request) + if err != nil { + logger.Println(logger.ERROR, "Invalid Message from cli:", err) + return + } + + switch request.Request { + case Info: + // response.Config = *conf + response.Matches = matches + response.Actions = actions + case Flush: + le := LogEntry{time.Now(), 0, request.Flush.P, request.Flush.S, request.Flush.F, 0, false} + + flushToMatchesC <- request.Flush + flushToActionsC <- request.Flush + flushToDatabaseC <- le + + default: + logger.Println(logger.ERROR, "Invalid Message from cli: unrecognised command type") + response.Err = errors.New("unrecognised command type") + return + } + + err = gob.NewEncoder(conn).Encode(response) + if err != nil { + logger.Println(logger.ERROR, "Can't respond to cli:", err) + return + } + }(conn) + } +} diff --git a/app/startup.go b/app/startup.go new file mode 100644 index 0000000..d8cf167 --- /dev/null +++ b/app/startup.go @@ -0,0 +1,178 @@ +package app + +import ( + "encoding/json" + "fmt" + "os" + "regexp" + "runtime" + "slices" + "strings" + "time" + + "framagit.org/ppom/reaction/logger" + + "github.com/google/go-jsonnet" +) + +func (c *Conf) setup() { + if c.Concurrency == 0 { + c.Concurrency = runtime.NumCPU() + } + + // Assure we iterate through c.Patterns map in reproductible order + sortedPatternNames := make([]string, 0, len(c.Patterns)) + for k := range c.Patterns { + sortedPatternNames = append(sortedPatternNames, k) + } + slices.Sort(sortedPatternNames) + + for _, patternName := range sortedPatternNames { + pattern := c.Patterns[patternName] + pattern.Name = patternName + pattern.nameWithBraces = fmt.Sprintf("<%s>", pattern.Name) + + if pattern.Regex == "" { + logger.Fatalf("Bad configuration: pattern's regex %v is empty!", patternName) + } + + compiled, err := regexp.Compile(fmt.Sprintf("^%v$", pattern.Regex)) + if err != nil { + logger.Fatalf("Bad configuration: pattern %v: %v", patternName, err) + } + pattern.Regex = fmt.Sprintf("(?P<%s>%s)", patternName, pattern.Regex) + for _, ignore := range pattern.Ignore { + if !compiled.MatchString(ignore) { + logger.Fatalf("Bad configuration: pattern ignore '%v' doesn't match pattern %v! It should be fixed or removed.", ignore, pattern.nameWithBraces) + } + } + + // Compile ignore regexes + for _, regex := range pattern.IgnoreRegex { + // Enclose the regex to make sure that it matches the whole detected string + compiledRegex, err := regexp.Compile("^" + regex + "$") + if err != nil { + logger.Fatalf("Bad configuration: in ignoreregex of pattern %s: %v", pattern.Name, err) + } + + pattern.compiledIgnoreRegex = append(pattern.compiledIgnoreRegex, *compiledRegex) + } + } + + if len(c.Streams) == 0 { + logger.Fatalln("Bad configuration: no streams configured!") + } + for streamName := range c.Streams { + + stream := c.Streams[streamName] + stream.Name = streamName + + if strings.Contains(stream.Name, ".") { + logger.Fatalf("Bad configuration: character '.' is not allowed in stream names: '%v'", stream.Name) + } + + if len(stream.Filters) == 0 { + logger.Fatalf("Bad configuration: no filters configured in %v", stream.Name) + } + for filterName := range stream.Filters { + + filter := stream.Filters[filterName] + filter.Stream = stream + filter.Name = filterName + + if strings.Contains(filter.Name, ".") { + logger.Fatalf("Bad configuration: character '.' is not allowed in filter names: '%v'", filter.Name) + } + // Parse Duration + if filter.RetryPeriod == "" { + if filter.Retry > 1 { + logger.Fatalf("Bad configuration: retry but no retryperiod in %v.%v", stream.Name, filter.Name) + } + } else { + retryDuration, err := time.ParseDuration(filter.RetryPeriod) + if err != nil { + logger.Fatalf("Bad configuration: Failed to parse retry time in %v.%v: %v", stream.Name, filter.Name, err) + } + filter.retryDuration = retryDuration + } + + if len(filter.Regex) == 0 { + logger.Fatalf("Bad configuration: no regexes configured in %v.%v", stream.Name, filter.Name) + } + // Compute Regexes + // Look for Patterns inside Regexes + for _, regex := range filter.Regex { + // iterate through patterns in reproductible order + for _, patternName := range sortedPatternNames { + pattern := c.Patterns[patternName] + if strings.Contains(regex, pattern.nameWithBraces) { + if !slices.Contains(filter.Pattern, pattern) { + filter.Pattern = append(filter.Pattern, pattern) + } + regex = strings.Replace(regex, pattern.nameWithBraces, pattern.Regex, 1) + } + } + compiledRegex, err := regexp.Compile(regex) + if err != nil { + logger.Fatalf("Bad configuration: regex of filter %s.%s: %v", stream.Name, filter.Name, err) + } + filter.compiledRegex = append(filter.compiledRegex, *compiledRegex) + } + + if len(filter.Actions) == 0 { + logger.Fatalln("Bad configuration: no actions configured in", stream.Name, ".", filter.Name) + } + for actionName := range filter.Actions { + + action := filter.Actions[actionName] + action.Filter = filter + action.Name = actionName + + if strings.Contains(action.Name, ".") { + logger.Fatalln("Bad configuration: character '.' is not allowed in action names", action.Name) + } + // Parse Duration + if action.After != "" { + afterDuration, err := time.ParseDuration(action.After) + if err != nil { + logger.Fatalln("Bad configuration: Failed to parse after time in ", stream.Name, ".", filter.Name, ".", action.Name, ":", err) + } + action.afterDuration = afterDuration + } else if action.OnExit { + logger.Fatalln("Bad configuration: Cannot have `onexit: true` without an `after` directive in", stream.Name, ".", filter.Name, ".", action.Name) + } + if filter.longuestActionDuration == nil || filter.longuestActionDuration.Milliseconds() < action.afterDuration.Milliseconds() { + filter.longuestActionDuration = &action.afterDuration + } + } + } + } +} + +func parseConf(filename string) *Conf { + + data, err := os.Open(filename) + if err != nil { + logger.Fatalln("Failed to read configuration file:", err) + } + + var conf Conf + if filename[len(filename)-4:] == ".yml" || filename[len(filename)-5:] == ".yaml" { + err = jsonnet.NewYAMLToJSONDecoder(data).Decode(&conf) + if err != nil { + logger.Fatalln("Failed to parse yaml configuration file:", err) + } + } else { + var jsondata string + jsondata, err = jsonnet.MakeVM().EvaluateFile(filename) + if err == nil { + err = json.Unmarshal([]byte(jsondata), &conf) + } + if err != nil { + logger.Fatalln("Failed to parse json configuration file:", err) + } + } + + conf.setup() + return &conf +} diff --git a/app/types.go b/app/types.go new file mode 100644 index 0000000..b49b075 --- /dev/null +++ b/app/types.go @@ -0,0 +1,200 @@ +package app + +import ( + "bytes" + "encoding/gob" + "fmt" + "os" + "regexp" + "strings" + "time" +) + +type Conf struct { + Concurrency int `json:"concurrency"` + Patterns map[string]*Pattern `json:"patterns"` + Streams map[string]*Stream `json:"streams"` + Start [][]string `json:"start"` + Stop [][]string `json:"stop"` +} + +type Pattern struct { + Regex string `json:"regex"` + Ignore []string `json:"ignore"` + + IgnoreRegex []string `json:"ignoreregex"` + compiledIgnoreRegex []regexp.Regexp `json:"-"` + + Name string `json:"-"` + nameWithBraces string `json:"-"` +} + +// Stream, Filter & Action structures must never be copied. +// They're always referenced through pointers + +type Stream struct { + Name string `json:"-"` + + Cmd []string `json:"cmd"` + Filters map[string]*Filter `json:"filters"` +} +type LilStream struct { + Name string +} + +func (s *Stream) GobEncode() ([]byte, error) { + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + err := enc.Encode(LilStream{s.Name}) + return buf.Bytes(), err +} + +func (s *Stream) GobDecode(b []byte)(error) { + var ls LilStream + dec := gob.NewDecoder(bytes.NewReader(b)) + err := dec.Decode(&ls) + s.Name = ls.Name + return err +} + +type Filter struct { + Stream *Stream `json:"-"` + Name string `json:"-"` + + Regex []string `json:"regex"` + compiledRegex []regexp.Regexp `json:"-"` + Pattern []*Pattern `json:"-"` + + Retry int `json:"retry"` + RetryPeriod string `json:"retryperiod"` + retryDuration time.Duration `json:"-"` + + Actions map[string]*Action `json:"actions"` + longuestActionDuration *time.Duration +} + +// those small versions are needed to prevent infinite recursion in gob because of +// data cycles: Stream <-> Filter, Filter <-> Action +type LilFilter struct { + Stream *Stream + Name string + Pattern []*Pattern +} + +func (f *Filter) GobDecode(b []byte)(error) { + var lf LilFilter + dec := gob.NewDecoder(bytes.NewReader(b)) + err := dec.Decode(&lf) + f.Stream = lf.Stream + f.Name = lf.Name + f.Pattern = lf.Pattern + return err +} + +func (f *Filter) GobEncode() ([]byte, error) { + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + err := enc.Encode(LilFilter{f.Stream, f.Name, f.Pattern}) + return buf.Bytes(), err +} + +type Action struct { + Filter *Filter `json:"-"` + Name string `json:"-"` + + Cmd []string `json:"cmd"` + + After string `json:"after"` + afterDuration time.Duration `json:"-"` + + OnExit bool `json:"onexit"` +} +type LilAction struct { + Filter *Filter + Name string +} + +func (a *Action) GobEncode() ([]byte, error) { + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + err := enc.Encode(LilAction{a.Filter, a.Name}) + return buf.Bytes(), err +} + +func (a *Action) GobDecode(b []byte)(error) { + var la LilAction + dec := gob.NewDecoder(bytes.NewReader(b)) + err := dec.Decode(&la) + a.Filter = la.Filter + a.Name = la.Name + return err +} + +type LogEntry struct { + T time.Time + S int64 + Pattern Match + Stream, Filter string + SF int + Exec bool +} + +type ReadDB struct { + file *os.File + dec *gob.Decoder +} + +type WriteDB struct { + file *os.File + enc *gob.Encoder +} + +type MatchesMap map[PF]map[time.Time]struct{} +type ActionsMap map[PA]map[time.Time]struct{} + +// This is a "\x00" Joined string +// which contains all matches on a line. +type Match string + +func (m *Match) Split() []string { + return strings.Split(string(*m), "\x00") +} +func JoinMatch(mm []string) Match { + return Match(strings.Join(mm, "\x00")) +} +func WithBrackets(mm []string) string { + var b strings.Builder + for _, match := range mm { + fmt.Fprintf(&b, "[%s]", match) + } + return b.String() +} + +// Helper structs made to carry information +// Stream, Filter +type SF struct{ S, F string } + +// Pattern, Stream, Filter +type PSF struct { + P Match + S, F string +} + +type PF struct { + P Match + F *Filter +} +type PFT struct { + P Match + F *Filter + T time.Time +} +type PA struct { + P Match + A *Action +} +type PAT struct { + P Match + A *Action + T time.Time +} diff --git a/bench/bench.sh b/bench/bench.sh deleted file mode 100755 index 9f42ec2..0000000 --- a/bench/bench.sh +++ /dev/null @@ -1,24 +0,0 @@ -set -e - -if test "$(realpath "$PWD")" != "$(realpath "$(dirname "$0")/..")" -then - echo "You must be in reaction root directory" - exit 1 -fi - -if test ! -f "$1" -then - # shellcheck disable=SC2016 - echo '$1 must be a configuration file (most probably in ./bench)' - exit 1 -fi - -rm -f reaction.db -cargo build --release --bins -sudo systemd-run --wait \ - -p User="$(id -nu)" \ - -p MemoryAccounting=yes \ - -p IOAccounting=yes \ - -p WorkingDirectory="$(pwd)" \ - -p Environment=PATH=/run/current-system/sw/bin/ \ - sh -c "for i in 1 2; do ./target/release/reaction start -c '$1' -l ERROR -s ./reaction.sock; done" diff --git a/bench/heavy-load.yml b/bench/heavy-load.yml deleted file mode 100644 index 73c7a3e..0000000 --- a/bench/heavy-load.yml +++ /dev/null @@ -1,74 +0,0 @@ ---- -# This configuration permits to test reaction's performance -# under a very high load -# -# It keeps regexes super simple, to avoid benchmarking the `regex` crate, -# and benchmark reaction's internals instead. -concurrency: 32 - -patterns: - num: - regex: '[0-9]{3}' - ip: - regex: '(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:]{2,90})' - ignore: - - 1.0.0.1 - -streams: - tailDown1: - cmd: [ 'sh', '-c', 'sleep 2; seq 10001 | while read i; do echo found $i; done' ] - filters: - find1: - regex: - - '^found ' - retry: 9 - retryperiod: 6m - actions: - damn: - cmd: [ 'sleep', '0.0' ] - undamn: - cmd: [ 'sleep', '0.0' ] - after: 1m - onexit: false - tailDown2: - cmd: [ 'sh', '-c', 'sleep 2; seq 1000100 | while read i; do echo found $i; echo trouvé $i; done' ] - filters: - find2: - regex: - - '^found ' - retry: 480 - retryperiod: 6m - actions: - damn: - cmd: [ 'sleep', '0.0' ] - undamn: - cmd: [ 'sleep', '0.0' ] - after: 1m - onexit: false - tailDown3: - cmd: [ 'sh', '-c', 'sleep 2; seq 1000100 | while read i; do echo found $i; echo trouvé $i; done' ] - filters: - find3: - regex: - - '^found ' - retry: 480 - retryperiod: 6m - actions: - damn: - cmd: [ 'sleep', '0.0' ] - undamn: - cmd: [ 'sleep', '0.0' ] - after: 1m - onexit: false - find4: - regex: - - '^trouvé ' - retry: 480 - retryperiod: 6m - actions: - damn: - cmd: [ 'sleep', '0.0' ] - undamn: - cmd: [ 'sleep', '0.0' ] - after: 1m - onexit: false diff --git a/bench/nginx.yml b/bench/nginx.yml deleted file mode 100644 index a328d88..0000000 --- a/bench/nginx.yml +++ /dev/null @@ -1,130 +0,0 @@ -# This is an extract of a real life configuration -# -# It reads an nginx's access.log in the following format: -# log_format '$remote_addr - $remote_user [$time_local] ' -# '$host ' -# '"$request" $status $bytes_sent ' -# '"$http_referer" "$http_user_agent"'; -# -# I can't make my access.log public for obvious privacy reasons. -# -# On the opposite of heavy-load.yml, this test is closer to real-life regex complexity. -# -# It has been created to test the performance improvements of -# the previous commit: ad6b0faa30c1af84360f66074a917b4bf6cda10a -# -# On this test, most lines don't match anything, so most time is spent matching regexes. - -concurrency: 0 -patterns: - ip: - ignore: - - 192.168.1.253 - - 10.1.1.1 - - 10.1.1.5 - - 10.1.1.4 - - 127.0.0.1 - - ::1 - regex: (?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|(?:(?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])) - untilEOL: - regex: .*$ -streams: - nginx: - cmd: - - cat - - /tmp/access.log - filters: - directusFailedLogin: - actions: - ban: - cmd: - - sleep - - 0.01 - unban: - after: 4h - cmd: - - sleep - - 0.01 - regex: - - ^ .* "POST /repertoire/auth/login HTTP/..." 401 [0-9]+ .https://babos.land - - ^ .* "POST /pompeani.art/auth/login HTTP/..." 401 [0-9]+ .https://edit.ppom.me - - ^ .* "POST /leborddeleau/auth/login HTTP/..." 401 [0-9]+ .https://edit.ppom.me - - ^ .* "POST /5eroue/auth/login HTTP/..." 401 [0-9]+ .https://edit.ppom.me - - ^ .* "POST /edit/auth/login HTTP/..." 401 [0-9]+ .https://edit.ppom.me - - ^ .* "POST /auth/login HTTP/..." 401 [0-9]+ .https://edit.ppom.fr - retry: 6 - retryperiod: 4h - gptbot: - actions: - ban: - cmd: - - sleep - - 0.01 - unban: - after: 4h - cmd: - - sleep - - 0.01 - regex: - - ^.*"[^"]*AI2Bot[^"]*"$ - - ^.*"[^"]*Amazonbot[^"]*"$ - - ^.*"[^"]*Applebot[^"]*"$ - - ^.*"[^"]*Applebot-Extended[^"]*"$ - - ^.*"[^"]*Bytespider[^"]*"$ - - ^.*"[^"]*CCBot[^"]*"$ - - ^.*"[^"]*ChatGPT-User[^"]*"$ - - ^.*"[^"]*ClaudeBot[^"]*"$ - - ^.*"[^"]*Diffbot[^"]*"$ - - ^.*"[^"]*DuckAssistBot[^"]*"$ - - ^.*"[^"]*FacebookBot[^"]*"$ - - ^.*"[^"]*GPTBot[^"]*"$ - - ^.*"[^"]*Google-Extended[^"]*"$ - - ^.*"[^"]*Kangaroo Bot[^"]*"$ - - ^.*"[^"]*Meta-ExternalAgent[^"]*"$ - - ^.*"[^"]*Meta-ExternalFetcher[^"]*"$ - - ^.*"[^"]*OAI-SearchBot[^"]*"$ - - ^.*"[^"]*PerplexityBot[^"]*"$ - - ^.*"[^"]*Timpibot[^"]*"$ - - ^.*"[^"]*Webzio-Extended[^"]*"$ - - ^.*"[^"]*YouBot[^"]*"$ - - ^.*"[^"]*omgili[^"]*"$ - slskd-failedLogin: - actions: - ban: - cmd: - - sleep - - 0.01 - unban: - after: 4h - cmd: - - sleep - - 0.01 - regex: - - ^ .* "POST /slskd/api/v0/session HTTP/..." 401 [0-9]+ .https://ppom.me - - ^ .* "POST /kiosque/api/v0/session HTTP/..." 401 [0-9]+ .https://babos.land - retry: 3 - retryperiod: 1h - suspectRequests: - actions: - ban: - cmd: - - sleep - - 0.01 - unban: - after: 4h - cmd: - - sleep - - 0.01 - regex: - - ^ .*"GET /(?:[^/" ]*/)*wp-login\.php - - ^ .*"GET /(?:[^/" ]*/)*wp-includes - - '^ .*"GET /(?:[^/" ]*/)*\.env ' - - '^ .*"GET /(?:[^/" ]*/)*config\.json ' - - '^ .*"GET /(?:[^/" ]*/)*info\.php ' - - '^ .*"GET /(?:[^/" ]*/)*owa/auth/logon.aspx ' - - '^ .*"GET /(?:[^/" ]*/)*auth.html ' - - '^ .*"GET /(?:[^/" ]*/)*auth1.html ' - - '^ .*"GET /(?:[^/" ]*/)*password.txt ' - - '^ .*"GET /(?:[^/" ]*/)*passwords.txt ' - - '^ .*"GET /(?:[^/" ]*/)*dns-query ' - - '^ .*"GET /(?:[^/" ]*/)*\.git/ ' diff --git a/bench/small-heavy-load-virtual.yml b/bench/small-heavy-load-virtual.yml deleted file mode 100644 index 0f074b9..0000000 --- a/bench/small-heavy-load-virtual.yml +++ /dev/null @@ -1,86 +0,0 @@ ---- -# This configuration permits to test reaction's performance -# under a very high load -# -# It keeps regexes super simple, to avoid benchmarking the `regex` crate, -# and benchmark reaction's internals instead. -concurrency: 32 - -plugins: - - path: "/home/ppom/prg/reaction/target/release/reaction-plugin-virtual" - -patterns: - num: - regex: '[0-9]{3}' - ip: - regex: '(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:]{2,90})' - ignore: - - 1.0.0.1 - -streams: - virtual: - type: virtual - filters: - find0: - regex: - - '^$' - actions: - damn: - cmd: [ 'sleep', '0.0' ] - undamn: - cmd: [ 'sleep', '0.0' ] - after: 1m - onexit: false - tailDown1: - cmd: [ 'sh', '-c', 'sleep 2; seq 1001 | while read i; do echo found $i; done' ] - filters: - find1: - regex: - - '^found ' - retry: 9 - retryperiod: 6m - actions: - virtual: - type: virtual - options: - send: '' - to: virtual - tailDown2: - cmd: [ 'sh', '-c', 'sleep 2; seq 100100 | while read i; do echo found $i; echo trouvé $i; done' ] - filters: - find2: - regex: - - '^found ' - retry: 480 - retryperiod: 6m - actions: - virtual: - type: virtual - options: - send: '' - to: virtual - tailDown3: - cmd: [ 'sh', '-c', 'sleep 2; seq 100100 | while read i; do echo found $i; echo trouvé $i; done' ] - filters: - find3: - regex: - - '^found ' - retry: 480 - retryperiod: 6m - actions: - virtual: - type: virtual - options: - send: '' - to: virtual - find4: - regex: - - '^trouvé ' - retry: 480 - retryperiod: 6m - actions: - virtual: - type: virtual - options: - send: '' - to: virtual diff --git a/bench/small-heavy-load.yml b/bench/small-heavy-load.yml deleted file mode 100644 index 931a456..0000000 --- a/bench/small-heavy-load.yml +++ /dev/null @@ -1,74 +0,0 @@ ---- -# This configuration permits to test reaction's performance -# under a very high load -# -# It keeps regexes super simple, to avoid benchmarking the `regex` crate, -# and benchmark reaction's internals instead. -concurrency: 32 - -patterns: - num: - regex: '[0-9]{3}' - ip: - regex: '(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:]{2,90})' - ignore: - - 1.0.0.1 - -streams: - tailDown1: - cmd: [ 'sh', '-c', 'sleep 2; seq 1001 | while read i; do echo found $i; done' ] - filters: - find1: - regex: - - '^found ' - retry: 9 - retryperiod: 6m - actions: - damn: - cmd: [ 'sleep', '0.0' ] - undamn: - cmd: [ 'sleep', '0.0' ] - after: 1m - onexit: false - tailDown2: - cmd: [ 'sh', '-c', 'sleep 2; seq 100100 | while read i; do echo found $i; echo trouvé $i; done' ] - filters: - find2: - regex: - - '^found ' - retry: 480 - retryperiod: 6m - actions: - damn: - cmd: [ 'sleep', '0.0' ] - undamn: - cmd: [ 'sleep', '0.0' ] - after: 1m - onexit: false - tailDown3: - cmd: [ 'sh', '-c', 'sleep 2; seq 100100 | while read i; do echo found $i; echo trouvé $i; done' ] - filters: - find3: - regex: - - '^found ' - retry: 480 - retryperiod: 6m - actions: - damn: - cmd: [ 'sleep', '0.0' ] - undamn: - cmd: [ 'sleep', '0.0' ] - after: 1m - onexit: false - find4: - regex: - - '^trouvé ' - retry: 480 - retryperiod: 6m - actions: - damn: - cmd: [ 'sleep', '0.0' ] - undamn: - cmd: [ 'sleep', '0.0' ] - after: 1m - onexit: false diff --git a/build.rs b/build.rs deleted file mode 100644 index ff070e0..0000000 --- a/build.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::{ - env::var_os, - io::{self, ErrorKind}, -}; - -use clap_complete::shells; - -// SubCommand defined here -include!("src/cli.rs"); - -fn main() -> io::Result<()> { - if var_os("PROFILE").ok_or(ErrorKind::NotFound)? == "release" { - let out_dir = PathBuf::from(var_os("OUT_DIR").ok_or(ErrorKind::NotFound)?).join("../../.."); - - // Build CLI - let cli = clap::Command::new("reaction"); - let cli = SubCommand::augment_subcommands(cli); - // We have to manually add metadata because it is lost: only subcommands are appended - let cli = cli.about("Scan logs and take action").long_about( -"A daemon that scans program outputs for repeated patterns, and takes action. - -Aims at being more versatile and flexible than fail2ban, while being faster and having simpler configuration. - -See usage examples, service configurations and good practices on the wiki: https://reaction.ppom.me"); - - // Generate completions - clap_complete::generate_to(shells::Bash, &mut cli.clone(), "reaction", out_dir.clone())?; - clap_complete::generate_to(shells::Fish, &mut cli.clone(), "reaction", out_dir.clone())?; - clap_complete::generate_to(shells::Zsh, &mut cli.clone(), "reaction", out_dir.clone())?; - - // Generate manpages - clap_mangen::generate_to(cli, out_dir.clone())?; - } - - println!("cargo::rerun-if-changed=build.rs"); - println!("cargo::rerun-if-changed=src/cli.rs"); - - Ok(()) -} diff --git a/config/README.md b/config/README.md deleted file mode 100644 index cbf7d67..0000000 --- a/config/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Configuration - -Here reside two equal configurations, one in YAML and one in JSONnet. - -Those serve as a configuration reference for now, waiting for a more complete reference in the wiki. - -Please take a look at the [wiki](https://reaction.ppom.me) for security implications of using reaction, -FAQ, JSONnet tips, and multiple examples of filters and actions. diff --git a/config/activitywatch.jsonnet b/config/activitywatch.jsonnet new file mode 100644 index 0000000..33e4083 --- /dev/null +++ b/config/activitywatch.jsonnet @@ -0,0 +1,101 @@ +// Those strings will be substitued in each shell() call +local substitutions = [ + ['OUTFILE', '"$HOME/.local/share/watch/logs-$(date +%F)"'], + ['DATE', '"$(date "+%F %T")"'], +]; + +// Substitue each substitutions' item in string +local sub(str) = std.foldl( + (function(changedstr, kv) std.strReplace(changedstr, kv[0], kv[1])), + substitutions, + str +); +local shell(prg) = [ + 'sh', + '-c', + sub(prg), +]; + +local log(line) = shell('echo DATE ' + std.strReplace(line, '\n', ' ') + '>> OUTFILE'); + +{ + start: [ + shell('mkdir -p "$(dirname OUTFILE)"'), + log('start'), + ], + + stop: [ + log('stop'), + ], + + patterns: { + all: { regex: '.*' }, + }, + + streams: { + // Be notified about each window focus change + // FIXME DOESN'T WORK + sway: { + cmd: shell(||| + swaymsg -rm -t subscribe "['window']" | jq -r 'select(.change == "focus") | .container | if has("app_id") and .app_id != null then .app_id else .window_properties.class end' + |||), + filters: { + send: { + regex: ['^$'], + actions: { + send: { cmd: log('focus ') }, + }, + }, + }, + }, + + // Be notified when user is away + swayidle: { + // FIXME echo stop and start instead? + cmd: ['swayidle', 'timeout', '30', 'echo sleep', 'resume', 'echo resume'], + filters: { + send: { + regex: ['^$'], + actions: { + send: { cmd: log('') }, + }, + }, + }, + }, + + // Be notified about tmux activity + // Limitation: can't handle multiple concurrently attached sessions + // tmux: { + // cmd: shell(||| + // LAST_TIME="0" + // LAST_ACTIVITY="" + // while true; + // do + // NEW_TIME=$(tmux display -p '#{session_activity}') + // if [ -n "$NEW_TIME" ] && [ "$NEW_TIME" -gt "$LAST_TIME" ] + // then + // LAST_TIME="$NEW_TIME" + // NEW_ACTIVITY="$(tmux display -p '#{pane_current_command} #{pane_current_path}')" + // if [ -n "$NEW_ACTIVITY" ] && [ "$NEW_ACTIVITY" != "$LAST_ACTIVITY" ] + // then + // LAST_ACTIVITY="$NEW_ACTIVITY" + // echo "tmux $NEW_ACTIVITY" + // fi + // fi + // sleep 10 + // done + // |||), + // filters: { + // send: { + // regex: ['^tmux $'], + // actions: { + // send: { cmd: log('tmux ') }, + // }, + // }, + // }, + // }, + + // Be notified about firefox activity + // TODO + }, +} diff --git a/config/example.jsonnet b/config/example.jsonnet index c9e9abb..6d84230 100644 --- a/config/example.jsonnet +++ b/config/example.jsonnet @@ -7,106 +7,61 @@ // strongly encouraged to take a look at the full documentation: https://reaction.ppom.me // JSONnet functions -local ipBan(cmd) = [cmd, '-w', '-A', 'reaction', '-s', '', '-j', 'DROP']; -local ipUnban(cmd) = [cmd, '-w', '-D', 'reaction', '-s', '', '-j', 'DROP']; +local iptables(args) = ['ip46tables', '-w'] + args; +// ip46tables is a minimal C program (only POSIX dependencies) present in a +// subdirectory of this repo. +// it permits to handle both ipv4/iptables and ipv6/ip6tables commands -// See meaning and usage of this function around L180 +// See meaning and usage of this function around L106 local banFor(time) = { - ban4: { - cmd: ipBan('iptables'), - ipv4only: true, + ban: { + cmd: iptables(['-A', 'reaction', '-s', '', '-j', 'DROP']), }, - ban6: { - cmd: ipBan('ip6tables'), - ipv6only: true, - }, - unban4: { - cmd: ipUnban('iptables'), + unban: { after: time, - ipv4only: true, - }, - unban6: { - cmd: ipUnban('ip6tables'), - after: time, - ipv6only: true, + cmd: iptables(['-D', 'reaction', '-s', '', '-j', 'DROP']), }, }; -// See usage of this function around L90 -// Generates a command for iptables and ip46tables -local ip46tables(arguments) = [ - ['iptables', '-w'] + arguments, - ['ip6tables', '-w'] + arguments, -]; - { // patterns are substitued in regexes. // when a filter performs an action, it replaces the found pattern patterns: { - - name: { - // reaction regex syntax is defined here: https://docs.rs/regex/latest/regex/#syntax - // common patterns have a 'regex' field - regex: '[a-z]+', - // patterns can ignore specific strings - ignore: ['cecilia'], - // patterns can also be ignored based on regexes, it will try to match the whole string detected by the pattern - ignoreregex: [ - // ignore names starting with 'jo' - 'jo.*', - ], - }, - ip: { - // patterns can have a special 'ip' type that matches both ipv4 and ipv6 - // or 'ipv4' or 'ipv6' to match only that ip version - type: 'ip', + // reaction regex syntax is defined here: https://github.com/google/re2/wiki/Syntax + // jsonnet's @'string' is for verbatim strings + // simple version: regex: @'(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:]{2,90})', + regex: @'(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|(?:(?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))', ignore: ['127.0.0.1', '::1'], - // they can also ignore whole CIDR ranges of ip - ignorecidr: ['10.0.0.0/8'], - // last but not least, patterns of type ip, ipv4, ipv6 can also group their matched ips by mask - // ipv4mask: 30 - // this means that ipv6 matches will be converted to their network part. - ipv6mask: 64, - // for example,"2001:db8:85a3:9de5::8a2e:370:7334" will be converted to "2001:db8:85a3:9de5::/64". + // Patterns can be ignored based on regexes, it will try to match the whole string detected by the pattern + // ignoreregex: [@'10\.0\.[0-9]{1,3}\.[0-9]{1,3}'], }, - - // ipv4: { - // type: 'ipv4', - // ignore: ... - // ipv4mask: ... - // }, - }, - // where the state (database) must be read - // defaults to . which means reaction's working directory. - // The systemd service starts reaction in /var/lib/reaction. - state_directory: '.', - // if set to a positive number → max number of concurrent actions // if set to a negative number → no limit // if not specified or set to 0 → defaults to the number of CPUs on the system concurrency: 0, // Those commands will be executed in order at start, before everything else - start: + start: [ // Create an iptables chain for reaction - ip46tables(['-N', 'reaction']) + + iptables(['-N', 'reaction']), // Insert this chain as the first item of the INPUT & FORWARD chains (for incoming connections) - ip46tables(['-I', 'INPUT', '-p', 'all', '-j', 'reaction']) + - ip46tables(['-I', 'FORWARD', '-p', 'all', '-j', 'reaction']), + iptables(['-I', 'INPUT', '-p', 'all', '-j', 'reaction']), + iptables(['-I', 'FORWARD', '-p', 'all', '-j', 'reaction']), + ], // Those commands will be executed in order at stop, after everything else - stop: + stop: [ // Remove the chain from the INPUT & FORWARD chains - ip46tables(['-D', 'INPUT', '-p', 'all', '-j', 'reaction']) + - ip46tables(['-D', 'FORWARD', '-p', 'all', '-j', 'reaction']) + + iptables(['-D', 'INPUT', '-p', 'all', '-j', 'reaction']), + iptables(['-D', 'FORWARD', '-p', 'all', '-j', 'reaction']), // Empty the chain - ip46tables(['-F', 'reaction']) + + iptables(['-F', 'reaction']), // Delete the chain - ip46tables(['-X', 'reaction']), - + iptables(['-X', 'reaction']), + ], // streams are commands // they are run and their ouptut is captured @@ -118,85 +73,41 @@ local ip46tables(arguments) = [ // note that if the command is not in environment's `PATH` // its full path must be given. cmd: ['journalctl', '-n0', '-fu', 'sshd.service'], - // filters run actions when they match regexes on a stream filters: { // filters have a user-defined name failedlogin: { - // reaction's regex syntax is defined here: https://docs.rs/regex/latest/regex/#syntax + // reaction's regex syntax is defined here: https://github.com/google/re2/wiki/Syntax regex: [ // is predefined in the patterns section // ip's regex is inserted in the following regex @'authentication failure;.*rhost=', @'Failed password for .* from ', - @'Invalid user .* from ', @'Connection (reset|closed) by (authenticating|invalid) user .* ', - @'banner exchange: Connection from port [0-9]*: invalid format', ], - // if retry and retryperiod are defined, // the actions will only take place if a same pattern is // found `retry` times in a `retryperiod` interval retry: 3, - // format is defined as follows: - // - whitespace between the integer and unit is optional - // - integer must be positive (>= 0) - // - unit can be one of: - // - ms / millis / millisecond / milliseconds - // - s / sec / secs / second / seconds - // - m / min / mins / minute / minutes - // - h / hour / hours - // - d / day / days + // format is defined here: https://pkg.go.dev/time#ParseDuration retryperiod: '6h', - - // duplicate specify how to handle matches after an action has already been taken. - // 3 options are possible: - // - extend (default): update the pending actions' time, so they run later - // - ignore: don't do anything, ignore the match - // - rerun: run the actions again. so we may have the same pending actions multiple times. - // (this was the default before 2.2.0) - // duplicate: extend - // actions are run by the filter when regexes are matched actions: { // actions have a user-defined name - ban4: { - cmd: ['iptables', '-w', '-A', 'reaction', '-s', '', '-j', 'DROP'], - // this optional field permits to run an action only when a pattern of type ip contains an ipv4 - ipv4only: true, + ban: { + cmd: iptables(['-A', 'reaction', '-s', '', '-j', 'DROP']), }, - - ban6: { - cmd: ['ip6tables', '-w', '-A', 'reaction', '-s', '', '-j', 'DROP'], - // this optional field permits to run an action only when a pattern of type ip contains an ipv6 - ipv6only: true, - }, - - unban4: { - cmd: ['iptables', '-w', '-D', 'reaction', '-s', '', '-j', 'DROP'], + unban: { + cmd: iptables(['-D', 'reaction', '-s', '', '-j', 'DROP']), // if after is defined, the action will not take place immediately, but after a specified duration // same format as retryperiod - after: '2 days', + after: '48h', // let's say reaction is quitting. does it run all those pending commands which had an `after` duration set? // if you want reaction to run those pending commands before exiting, you can set this: // onexit: true, // (defaults to false) // here it is not useful because we will flush and delete the chain containing the bans anyway // (with the stop commands) - ipv4only: true, - }, - - unban6: { - cmd: ['ip6tables', '-w', '-D', 'reaction', '-s', '', '-j', 'DROP'], - after: '2 days', - ipv6only: true, - }, - - mail: { - cmd: ['sendmail', '...', ''], - // some commands, such as alerting commands, are "oneshot". - // this means they'll be run only once, and won't be executed again when reaction is restarted - oneshot: true, }, }, // or use the banFor function defined at the beginning! diff --git a/config/example.yml b/config/example.yml deleted file mode 100644 index 40f2764..0000000 --- a/config/example.yml +++ /dev/null @@ -1,184 +0,0 @@ ---- -# This example configuration file is a good starting point, but you're -# strongly encouraged to take a look at the full documentation: https://reaction.ppom.me -# -# This file is using the well-established YAML configuration language. -# Note that the more powerful JSONnet configuration language is also supported -# and that the documentation uses JSONnet - -# definitions are just a place to put chunks of conf you want to reuse in another place -# using YAML anchors `&name` and pointers `*name` -# definitions are not readed by reaction -definitions: - - &ip4tablesban [ 'iptables', '-w', '-A', 'reaction', '-s', '', '-j', 'DROP' ] - - &ip6tablesban [ 'ip6tables', '-w', '-A', 'reaction', '-s', '', '-j', 'DROP' ] - - &ip4tablesunban [ 'iptables', '-w', '-D', 'reaction', '-s', '', '-j', 'DROP' ] - - &ip6tablesunban [ 'ip6tables', '-w', '-D', 'reaction', '-s', '', '-j', 'DROP' ] -# ip46tables is a minimal C program (only POSIX dependencies) present as a subdirectory. -# it permits to handle both ipv4/iptables and ipv6/ip6tables commands - -# where the state (database) must be read -# defaults to . which means reaction's working directory. -# The systemd service starts reaction in /var/lib/reaction. -state_directory: . - -# if set to a positive number → max number of concurrent actions -# if set to a negative number → no limit -# if not specified or set to 0 → defaults to the number of CPUs on the system -concurrency: 0 - -# patterns are substitued in regexes. -# when a filter performs an action, it replaces the found pattern -patterns: - name: - # reaction regex syntax is defined here: https://docs.rs/regex/latest/regex/#syntax - # common patterns have a 'regex' field - regex: '[a-z]+' - # patterns can ignore specific strings - ignore: - - 'cecilia' - # patterns can also be ignored based on regexes, it will try to match the whole string detected by the pattern - ignoreregex: - # ignore names starting with 'jo' - - 'jo.*' - - ip: - # patterns can have a special 'ip' type that matches both ipv4 and ipv6 - # or 'ipv4' or 'ipv6' to match only that ip version - type: ip - ignore: - - 127.0.0.1 - - ::1 - # they can also ignore whole CIDR ranges of ip - ignorecidr: - - 10.0.0.0/8 - # last but not least, patterns of type ip, ipv4, ipv6 can also group their matched ips by mask - # ipv4mask: 30 - # this means that ipv6 matches will be converted to their network part. - ipv6mask: 64 - # for example,"2001:db8:85a3:9de5::8a2e:370:7334" will be converted to "2001:db8:85a3:9de5::/64". - - # ipv4: - # type: ipv4 - # ignore: ... - -# Those commands will be executed in order at start, before everything else -start: - - [ 'iptables', '-w', '-N', 'reaction' ] - - [ 'ip6tables', '-w', '-N', 'reaction' ] - - [ 'iptables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ] - - [ 'ip6tables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ] - - [ 'iptables', '-w', '-I', 'FORWARD', '-p', 'all', '-j', 'reaction' ] - - [ 'ip6tables', '-w', '-I', 'FORWARD', '-p', 'all', '-j', 'reaction' ] - -# Those commands will be executed in order at stop, after everything else -stop: - - [ 'iptables', '-w', '-D', 'INPUT', '-p', 'all', '-j', 'reaction' ] - - [ 'ip6tables', '-w', '-D', 'INPUT', '-p', 'all', '-j', 'reaction' ] - - [ 'iptables', '-w', '-D', 'FORWARD', '-p', 'all', '-j', 'reaction' ] - - [ 'ip6tables', '-w', '-D', 'FORWARD', '-p', 'all', '-j', 'reaction' ] - - [ 'iptables', '-w', '-F', 'reaction' ] - - [ 'ip6tables', '-w', '-F', 'reaction' ] - - [ 'iptables', '-w', '-X', 'reaction' ] - - [ 'ip6tables', '-w', '-X', 'reaction' ] - -# streams are commands -# they are run and their ouptut is captured -# *example:* `tail -f /var/log/nginx/access.log` -# their output will be used by one or more filters -streams: - # streams have a user-defined name - ssh: - # note that if the command is not in environment's `PATH` - # its full path must be given. - cmd: [ 'journalctl', '-n0', '-fu', 'sshd.service' ] - - # filters run actions when they match regexes on a stream - filters: - # filters have a user-defined name - failedlogin: - # reaction's regex syntax is defined here: https://docs.rs/regex/latest/regex/#syntax - regex: - # is predefined in the patterns section - # ip's regex is inserted in the following regex - - 'authentication failure;.*rhost=' - - 'Failed password for .* from ' - - 'Invalid user .* from ' - - 'Connection (reset|closed) by (authenticating|invalid) user .* ' - - 'banner exchange: Connection from port [0-9]*: invalid format' - - # if retry and retryperiod are defined, - # the actions will only take place if a same pattern is - # found `retry` times in a `retryperiod` interval - retry: 3 - # format is defined as follows: - # - whitespace between the integer and unit is optional - # - integer must be positive (>= 0) - # - unit can be one of: - # - ms / millis / millisecond / milliseconds - # - s / sec / secs / second / seconds - # - m / min / mins / minute / minutes - # - h / hour / hours - # - d / day / days - retryperiod: 6h - - # duplicate specify how to handle matches after an action has already been taken. - # 3 options are possible: - # - extend (default): update the pending actions' time, so they run later - # - ignore: don't do anything, ignore the match - # - rerun: run the actions again. so we may have the same pending actions multiple times. - # (this was the default before 2.2.0) - # duplicate: extend - - # actions are run by the filter when regexes are matched - actions: - # actions have a user-defined name - ban4: - # YAML substitutes *reference by the value anchored at &reference - cmd: *ip4tablesban - # this optional field permits to run an action only when a pattern of type ip contains an ipv4 - ipv4only: true - - ban6: - cmd: *ip6tablesban - # this optional field permits to run an action only when a pattern of type ip contains an ipv6 - ipv6only: true - - unban4: - cmd: *ip4tablesunban - # if after is defined, the action will not take place immediately, but after a specified duration - # same format as retryperiod - after: '2 days' - # let's say reaction is quitting. does it run all those pending commands which had an `after` duration set? - # if you want reaction to run those pending commands before exiting, you can set this: - # onexit: true - # (defaults to false) - # here it is not useful because we will flush and delete the chain containing the bans anyway - # (with the stop commands) - ipv4only: true - - unban6: - cmd: *ip6tablesunban - after: '2 days' - ipv6only: true - - mail: - cmd: ['sendmail', '...', ''] - # some commands, such as alerting commands, are "oneshot". - # this means they'll be run only once, and won't be executed again when reaction is restarted - oneshot: true - -# persistence -# tldr; when an `after` action is set in a filter, such filter acts as a 'jail', -# which is persisted after reboots. -# -# when a filter is triggered, there are 2 flows: -# -# if none of its actions have an `after` directive set: -# no action will be replayed. -# -# else (if at least one action has an `after` directive set): -# if reaction stops while `after` actions are pending: -# and reaction starts again while those actions would still be pending: -# reaction executes the past actions (actions without after or with then+after < now) -# and plans the execution of future actions (actions with then+after > now) diff --git a/config/example.yml b/config/example.yml new file mode 120000 index 0000000..1efef98 --- /dev/null +++ b/config/example.yml @@ -0,0 +1 @@ +../app/example.yml \ No newline at end of file diff --git a/config/heavy-load.yml b/config/heavy-load.yml new file mode 100644 index 0000000..1c149c3 --- /dev/null +++ b/config/heavy-load.yml @@ -0,0 +1,72 @@ +--- +patterns: + num: + regex: '[0-9]+' + ip: + regex: '(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:]{2,90})' + ignore: + - 1.0.0.1 + +concurrency: 0 + +streams: + tailDown1: + cmd: [ 'sh', '-c', 'sleep 2; seq 100010 | while read i; do echo found $(($i % 100)); done' ] + filters: + findIP: + regex: + - '^found $' + retry: 50 + retryperiod: 1m + actions: + damn: + cmd: [ 'sleep', '0.' ] + undamn: + cmd: [ 'sleep', '0.' ] + after: 1m + onexit: false + tailDown2: + cmd: [ 'sh', '-c', 'sleep 2; seq 100010 | while read i; do echo prout $(($i % 100)); done' ] + filters: + findIP: + regex: + - '^prout $' + retry: 50 + retryperiod: 1m + actions: + damn: + cmd: [ 'sleep', '0.' ] + undamn: + cmd: [ 'sleep', '0.' ] + after: 1m + onexit: false + tailDown3: + cmd: [ 'sh', '-c', 'sleep 2; seq 100010 | while read i; do echo nanana $(($i % 100)); done' ] + filters: + findIP: + regex: + - '^nanana $' + retry: 50 + retryperiod: 2m + actions: + damn: + cmd: [ 'sleep', '0.' ] + undamn: + cmd: [ 'sleep', '0.' ] + after: 1m + onexit: false + tailDown4: + cmd: [ 'sh', '-c', 'sleep 2; seq 100010 | while read i; do echo nanana $(($i % 100)); done' ] + filters: + findIP: + regex: + - '^nomatch $' + retry: 50 + retryperiod: 2m + actions: + damn: + cmd: [ 'sleep', '0.' ] + undamn: + cmd: [ 'sleep', '0.' ] + after: 1m + onexit: false diff --git a/config/persistence.jsonnet b/config/persistence.jsonnet new file mode 100644 index 0000000..f3f58c1 --- /dev/null +++ b/config/persistence.jsonnet @@ -0,0 +1,50 @@ +{ + patterns: { + num: { + regex: '[0-9]+', + }, + }, + + streams: { + tailDown1: { + cmd: ['sh', '-c', "echo 01 02 03 04 05 | tr ' ' '\n' | while read i; do sleep 0.5; echo found $i; done"], + filters: { + findIP1: { + regex: ['^found $'], + retry: 1, + retryperiod: '2m', + actions: { + damn: { + cmd: ['echo', ''], + }, + undamn: { + cmd: ['echo', 'undamn', ''], + after: '1m', + onexit: true, + }, + }, + }, + }, + }, + tailDown2: { + cmd: ['sh', '-c', "echo 11 12 13 14 15 11 13 15 | tr ' ' '\n' | while read i; do sleep 0.3; echo found $i; done"], + filters: { + findIP2: { + regex: ['^found $'], + retry: 2, + retryperiod: '2m', + actions: { + damn: { + cmd: ['echo', ''], + }, + undamn: { + cmd: ['echo', 'undamn', ''], + after: '1m', + onexit: true, + }, + }, + }, + }, + }, + }, +} diff --git a/config/reaction.service b/config/reaction.example.service similarity index 85% rename from config/reaction.service rename to config/reaction.example.service index 897e869..22eed48 100644 --- a/config/reaction.service +++ b/config/reaction.example.service @@ -2,12 +2,15 @@ [Unit] Description=A daemon that scans program outputs for repeated patterns, and takes action. Documentation=https://reaction.ppom.me + +[Install] +WantedBy=multi-user.target # Ensure reaction will insert its chain after docker has inserted theirs. Only useful when iptables & docker are used # After=docker.service # See `man systemd.exec` and `man systemd.service` for most options below [Service] -ExecStart=/usr/local/bin/reaction start -c /etc/reaction/ +ExecStart=/usr/bin/reaction start -c /etc/reaction.jsonnet # Ask systemd to create /var/lib/reaction (/var/lib/ is implicit) StateDirectory=reaction @@ -15,8 +18,3 @@ StateDirectory=reaction RuntimeDirectory=reaction # Start reaction in its state directory WorkingDirectory=/var/lib/reaction -# Let reaction kill its child processes first -KillMode=mixed - -[Install] -WantedBy=multi-user.target diff --git a/config/server.jsonnet b/config/server.jsonnet new file mode 100644 index 0000000..2886c28 --- /dev/null +++ b/config/server.jsonnet @@ -0,0 +1,160 @@ +// This is the extensive configuration used on a **real** server! + +local banFor(time) = { + ban: { + cmd: ['ip46tables', '-w', '-A', 'reaction', '-s', '', '-j', 'DROP'], + }, + unban: { + after: time, + cmd: ['ip46tables', '-w', '-D', 'reaction', '-s', '', '-j', 'DROP'], + }, +}; + +{ + patterns: { + // IPs can be IPv4 or IPv6 + // ip46tables (C program also in this repo) handles running the good commands + ip: { + regex: @'(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|(?:(?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))', + // Ignore all from 192.168.1.1 to 192.168.1.255 + ignore: std.makeArray(255, function(i) '192.168.1.' + (i + 1)), + }, + }, + + start: [ + ['ip46tables', '-w', '-N', 'reaction'], + ['ip46tables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction'], + ], + stop: [ + ['ip46tables', '-w', '-D', 'INPUT', '-p', 'all', '-j', 'reaction'], + ['ip46tables', '-w', '-F', 'reaction'], + ['ip46tables', '-w', '-X', 'reaction'], + ], + + streams: { + // Ban hosts failing to connect via ssh + ssh: { + cmd: ['journalctl', '-fn0', '-u', 'sshd.service'], + filters: { + failedlogin: { + regex: [ + @'authentication failure;.*rhost=', + @'Connection (reset|closed) by (authenticating|invalid) user .* ', + @'Failed password for .* from ', + ], + retry: 3, + retryperiod: '6h', + actions: banFor('48h'), + }, + }, + }, + + // Ban hosts which knock on closed ports. + // It needs this iptables chain to be used to drop packets: + // ip46tables -N log-refuse + // ip46tables -A log-refuse -p tcp --syn -j LOG --log-level info --log-prefix 'refused connection: ' + // ip46tables -A log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse + // ip46tables -A log-refuse -j DROP + kernel: { + cmd: ['journalctl', '-fn0', '-k'], + filters: { + portscan: { + regex: ['refused connection: .*SRC='], + retry: 4, + retryperiod: '1h', + actions: banFor('720h'), + }, + }, + }, + // Note: nextcloud and vaultwarden could also be filters on the nginx stream + // I did use their own logs instead because it's less logs to parse than the front webserver + + // Ban hosts failing to connect to Nextcloud + nextcloud: { + cmd: ['journalctl', '-fn0', '-u', 'phpfpm-nextcloud.service'], + filters: { + failedLogin: { + regex: [ + @'"remoteAddr":"".*"message":"Login failed:', + @'"remoteAddr":"".*"message":"Trusted domain error.', + ], + retry: 3, + retryperiod: '1h', + actions: banFor('1h'), + }, + }, + }, + + // Ban hosts failing to connect to vaultwarden + vaultwarden: { + cmd: ['journalctl', '-fn0', '-u', 'vaultwarden.service'], + filters: { + failedlogin: { + actions: banFor('2h'), + regex: [@'Username or password is incorrect\. Try again\. IP: \. Username:'], + retry: 3, + retryperiod: '1h', + }, + }, + }, + + // Used with this nginx log configuration: + // log_format withhost '$remote_addr - $remote_user [$time_local] $host "$request" $status $bytes_sent "$http_referer" "$http_user_agent"'; + // access_log /var/log/nginx/access.log withhost; + nginx: { + cmd: ['tail', '-n0', '-f', '/var/log/nginx/access.log'], + filters: { + // Ban hosts failing to connect to Directus + directus: { + regex: [ + @'^ .* "POST /auth/login HTTP/..." 401 [0-9]+ .https://directusdomain', + ], + retry: 6, + retryperiod: '4h', + actions: banFor('4h'), + }, + + // Ban hosts presenting themselves as bots of ChatGPT + gptbot: { + regex: [@'^.*GPTBot/1.0'], + actions: banFor('720h'), + }, + + // Ban hosts failing to connect to slskd + slskd: { + regex: [ + @'^ .* "POST /api/v0/session HTTP/..." 401 [0-9]+ .https://slskddomain', + ], + retry: 3, + retryperiod: '1h', + actions: banFor('6h'), + }, + + // Ban suspect HTTP requests + // Those are frequent malicious requests I got from bots + // Make sure you don't have honnest use cases for those requests, or your clients may be banned for 2 weeks! + suspectRequests: { + regex: [ + // (?:[^/" ]*/)* is a "non-capturing group" regex that allow for subpath(s) + // example: /code/.env should be matched as well as /.env + // ^^^^^ + @'^.*"GET /(?:[^/" ]*/)*\.env ', + @'^.*"GET /(?:[^/" ]*/)*info\.php ', + @'^.*"GET /(?:[^/" ]*/)*owa/auth/logon.aspx ', + @'^.*"GET /(?:[^/" ]*/)*auth.html ', + @'^.*"GET /(?:[^/" ]*/)*auth1.html ', + @'^.*"GET /(?:[^/" ]*/)*password.txt ', + @'^.*"GET /(?:[^/" ]*/)*passwords.txt ', + @'^.*"GET /(?:[^/" ]*/)*dns-query ', + // Do not include this if you have a Wordpress website ;) + @'^.*"GET /(?:[^/" ]*/)*wp-login\.php', + @'^.*"GET /(?:[^/" ]*/)*wp-includes', + // Do not include this if a client must retrieve a config.json file ;) + @'^.*"GET /(?:[^/" ]*/)*config\.json ', + ], + actions: banFor('720h'), + }, + }, + }, + }, +} diff --git a/config/test.jsonnet b/config/test.jsonnet new file mode 100644 index 0000000..9210a80 --- /dev/null +++ b/config/test.jsonnet @@ -0,0 +1,63 @@ +{ + patterns: { + num: { + regex: '[0-9]+', + ignore: ['1'], + // ignoreregex: ['2.?'], + }, + letter: { + regex: '[a-z]+', + ignore: ['b'], + // ignoreregex: ['b.?'], + }, + }, + + streams: { + tailDown1: { + cmd: ['sh', '-c', "echo 1_abc 2_abc 3_abc abc_1 abc_2 abc_3 | tr ' ' '\n' | while read i; do sleep 1; echo found $i; done; sleep 30"], + filters: { + findIP: { + regex: [ + '^found _$', + '^found _$', + ], + retry: 2, + retryperiod: '30s', + actions: { + damn: { + cmd: ['echo', ''], + }, + undamn: { + cmd: ['echo', 'undamn', ''], + after: '28s', + onexit: true, + }, + }, + }, + }, + }, + tailDown2: { + cmd: ['sh', '-c', "echo 1_abc 2_abc 3_abc abc_1 abc_2 abc_3 | tr ' ' '\n' | while read i; do sleep 1; echo found $i; done; sleep 30"], + filters: { + findIP: { + regex: [ + '^found _$', + '^found _$', + ], + retry: 2, + retryperiod: '30s', + actions: { + damn: { + cmd: ['echo', ''], + }, + undamn: { + cmd: ['echo', 'undamn', ''], + after: '28s', + onexit: true, + }, + }, + }, + }, + }, + }, +} diff --git a/crates/treedb/Cargo.toml b/crates/treedb/Cargo.toml deleted file mode 100644 index 9f2c4d3..0000000 --- a/crates/treedb/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "treedb" -version = "1.0.0" -edition = "2024" - -[features] -test = [] - -[dependencies] -chrono.workspace = true -futures.workspace = true -serde.workspace = true -serde_json.workspace = true -thiserror.workspace = true -tokio.workspace = true -tokio.features = ["rt-multi-thread", "macros", "io-util", "time", "fs", "tracing"] -tokio-util.workspace = true -tokio-util.features = ["rt"] -tracing.workspace = true - -[dev-dependencies] -tempfile.workspace = true - diff --git a/crates/treedb/src/helpers.rs b/crates/treedb/src/helpers.rs deleted file mode 100644 index 9e4c3b0..0000000 --- a/crates/treedb/src/helpers.rs +++ /dev/null @@ -1,188 +0,0 @@ -use std::{ - collections::{BTreeMap, BTreeSet}, - time::Duration, -}; - -use chrono::DateTime; -use serde_json::Value; - -use crate::time::Time; - -/// Tries to convert a [`Value`] into a [`String`] -pub fn to_string(val: &Value) -> Result { - Ok(val.as_str().ok_or("not a string")?.to_owned()) -} - -/// Tries to convert a [`Value`] into a [`u64`] -pub fn to_u64(val: &Value) -> Result { - val.as_u64().ok_or("not a u64".into()) -} - -/// Old way of converting time: with chrono's serialization -fn old_string_to_time(val: &str) -> Result { - let time = DateTime::parse_from_rfc3339(val).map_err(|err| err.to_string())?; - Ok(Duration::new(time.timestamp() as u64, time.timestamp_subsec_nanos()).into()) -} - -/// New way of converting time: with our own implem -fn new_string_to_time(val: &str) -> Result { - let nanos: u128 = val.parse().map_err(|_| "not a number")?; - Ok(Duration::new( - (nanos / 1_000_000_000) as u64, - (nanos % 1_000_000_000) as u32, - ) - .into()) -} - -/// Tries to convert a [`&str`] into a [`Time`] -fn string_to_time(val: &str) -> Result { - match new_string_to_time(val) { - Err(err) => match old_string_to_time(val) { - Err(_) => Err(err), - ok => ok, - }, - ok => ok, - } -} - -/// Tries to convert a [`Value`] into a [`Time`] -pub fn to_time(val: &Value) -> Result { - string_to_time(val.as_str().ok_or("not a string number")?) -} - -/// Tries to convert a [`Value`] into a [`Vec`] -pub fn to_match(val: &Value) -> Result, String> { - val.as_array() - .ok_or("not an array")? - .iter() - .map(to_string) - .collect() -} - -/// Tries to convert a [`Value`] into a [`BTreeSet