diff --git a/.ccls b/.ccls new file mode 100644 index 0000000..e69de29 diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..4a4726a --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use_nix diff --git a/.gitignore b/.gitignore index 0f1d2c6..07180cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,15 @@ /reaction -/ip46tables -/nft46 -/reaction*.db +reaction.db +reaction.db.old +/data /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 deleted file mode 100644 index 78d7601..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,15 +0,0 @@ ---- -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 new file mode 100644 index 0000000..4289e65 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,81 @@ +# 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 new file mode 100644 index 0000000..c9a7b56 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4985 @@ +# 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 new file mode 100644 index 0000000..66e95e3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,105 @@ +[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 new file mode 100644 index 0000000..1c5d54e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +# 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 1226d7b..9b31a54 100644 --- a/Makefile +++ b/Makefile @@ -1,51 +1,23 @@ CC ?= gcc PREFIX ?= /usr/local BINDIR = $(PREFIX)/bin +MANDIR = $(PREFIX)/share/man/man1 SYSTEMDDIR ?= /etc/systemd -all: reaction ip46tables nft46 +all: reaction clean: - rm -f reaction ip46tables nft46 reaction*.deb reaction.minisig ip46tables.minisig nft46.minisig reaction*.deb.minisig - rm -rf debian-packaging + cargo clean -ip46tables: helpers_c/ip46tables.c - $(CC) -s -static helpers_c/ip46tables.c -o ip46tables +reaction: + cargo build --release -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: reaction + install -m755 target/release/reaction $(DESTDIR)$(BINDIR) install_systemd: install - install -m644 config/reaction.example.service $(SYSTEMDDIR)/system/reaction.service - sed -i 's#/usr/bin#$(DESTDIR)$(BINDIR)#' $(SYSTEMDDIR)/system/reaction.service + 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 diff --git a/README.md b/README.md index fb0d0d1..7d33ab5 100644 --- a/README.md +++ b/README.md @@ -4,32 +4,40 @@ 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 audit. however, it already works well on my servers 🚧 +🚧 This program hasn't received external security audit yet. However, it already works well on many 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 ~10x faster and has more manageable configuration. +reaction does not have all the features of the honorable fail2ban, but it's more than 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](./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: +- 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 🆙):
@@ -37,18 +45,19 @@ both are extensions of JSON, so JSON is transitively supported. ```yaml patterns: - ip: '(([ 0-9 ]{1,3}\.){3}[0-9]{1,3})|([0-9a-fA-F:]{2,90})' + ip: + type: ipv4 start: - - [ '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' ] + - [ 'iptables', '-w', '-N', 'reaction' ] + - [ 'iptables', '-w', '-I', 'INPUT', '-p', 'all', '-j', 'reaction' ] + - [ 'iptables', '-w', '-I', 'FORWARD', '-p', 'all', '-j', 'reaction' ] stop: - - [ 'ip46tables', '-w', '-D', 'INPUT', '-p', 'all', '-j', 'reaction' ] - - [ 'ip46tables', '-w', '-F', 'reaction' ] - - [ 'ip46tables', '-w', '-X', 'reaction' ] + - [ 'iptables', '-w', '-D', 'INPUT', '-p', 'all', '-j', 'reaction' ] + - [ 'iptables', '-w', '-D', 'FORWARD', '-p', 'all', '-j', 'reaction' ] + - [ 'iptables', '-w', '-F', 'reaction' ] + - [ 'iptables', '-w', '-X', 'reaction' ] streams: ssh: @@ -57,13 +66,16 @@ 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: [ 'ip46tables', '-w', '-I', 'reaction', '1', '-s', '', '-j', 'block' ] + cmd: [ 'iptables', '-w', '-I', 'reaction', '1', '-s', '', '-j', 'DROP' ] unban: - cmd: [ 'ip46tables', '-w', '-D', 'reaction', '1', '-s', '', '-j', 'block' ] + cmd: [ 'iptables', '-w', '-D', 'reaction', '1', '-s', '', '-j', 'DROP' ] after: '48h' ``` @@ -74,39 +86,44 @@ streams: /etc/reaction.jsonnet ```jsonnet -local iptables(args) = [ 'ip46tables', '-w' ] + args; local banFor(time) = { ban: { - cmd: iptables(['-A', 'reaction', '-s', '', '-j', 'reaction-log-refuse']), + cmd: ['iptables', '-w', '-A', 'reaction', '-s', '', '-j', 'DROP'], }, unban: { + cmd: ['iptables', '-w', '-D', 'reaction', '-s', '', '-j', 'DROP'], after: time, - cmd: iptables(['-D', 'reaction', '-s', '', '-j', 'reaction-log-refuse']), }, }; + { patterns: { ip: { - regex: @'(?:(?:[ 0-9 ]{1,3}\.){3}[0-9]{1,3})|(?:[0-9a-fA-F:]{2,90})', + type: 'ipv4', }, }, start: [ - 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' ]), + ['iptables', '-N', 'reaction'], + ['iptables', '-I', 'INPUT', '-p', 'all', '-j', 'reaction'], + ['iptables', '-I', 'FORWARD', '-p', 'all', '-j', 'reaction'], ], stop: [ - iptables([ '-D', 'INPUT', '-p', 'all', '-j', 'reaction' ]), - iptables([ '-F', 'reaction' ]), - iptables([ '-X', 'reaction' ]), + ['iptables', '-D', 'INPUT', '-p', 'all', '-j', 'reaction'], + ['iptables', '-D', 'FORWARD', '-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=' ], + regex: [ + @'authentication failure;.*rhost=', + @'Failed password for .* from ', + @'banner exchange: Connection from port [0-9]*: invalid format', + @'Invalid user .* from ', + ], retry: 3, retryperiod: '6h', actions: banFor('48h'), @@ -119,30 +136,35 @@ 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. +The embedded database is stored in the working directory (but can be overriden by the `state_directory` config option). 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. bans) +- `reaction show` show pending actions (ie. show current 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. -### `ip46tables` +### old binaries -`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. +`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). ## Wiki -You'll find more ressources, service configurations, etc. on the [Wiki](https://reaction.ppom.me)! +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. ## Installation @@ -150,53 +172,23 @@ You'll find more ressources, service configurations, etc. on the [Wiki](https:// ### Binaries -Executables are provided [here](https://framagit.org/ppom/reaction/-/releases/), for a standard x86-64 linux machine. +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. -A standard place to put such executables is `/usr/local/bin/`. +Signature verification and installation instructions are provided in the releases page. -> 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 -``` +> Provided binaries are compiled by running `nix-shell release.py` on a NixOS machine with docker installed. #### NixOS -- [ package ](https://framagit.org/ppom/nixos/-/blob/main/pkgs/reaction/default.nix) -- [ module ](https://framagit.org/ppom/nixos/-/blob/main/modules/common/reaction.nix) +reaction is packaged, but the [**module**](https://framagit.org/ppom/nixos/-/blob/main/modules/common/reaction.nix) has not yet been upstreamed. #### OpenBSD -[wiki](https://reaction.ppom.me/configs/openbsd.html) +See the [wiki](https://reaction.ppom.me/configurations/OpenBSD.html). ### Compilation -You'll need the go (>= 1.20) toolchain for reaction and a c compiler for ip46tables. +You'll need a recent rust toolchain for reaction and a c compiler for ip46tables. ```shell $ make ``` @@ -214,9 +206,46 @@ To install the systemd file as well make install_systemd ``` -## Development +## Contributing -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. +> 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. -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. +- [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) diff --git a/TODO b/TODO new file mode 100644 index 0000000..2b6071f --- /dev/null +++ b/TODO @@ -0,0 +1,3 @@ +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 deleted file mode 100644 index ad61bea..0000000 --- a/app/client.go +++ /dev/null @@ -1,393 +0,0 @@ -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 deleted file mode 100644 index ba291bd..0000000 --- a/app/daemon.go +++ /dev/null @@ -1,447 +0,0 @@ -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 deleted file mode 100644 index 759f597..0000000 --- a/app/example.yml +++ /dev/null @@ -1,108 +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: - - &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 deleted file mode 100644 index 8d3d284..0000000 --- a/app/main.go +++ /dev/null @@ -1,230 +0,0 @@ -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 deleted file mode 100644 index 78e78a1..0000000 --- a/app/persist.go +++ /dev/null @@ -1,264 +0,0 @@ -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 deleted file mode 100644 index 0d5b4ac..0000000 --- a/app/pipe.go +++ /dev/null @@ -1,81 +0,0 @@ -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 deleted file mode 100644 index d8cf167..0000000 --- a/app/startup.go +++ /dev/null @@ -1,178 +0,0 @@ -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 deleted file mode 100644 index b49b075..0000000 --- a/app/types.go +++ /dev/null @@ -1,200 +0,0 @@ -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 new file mode 100755 index 0000000..9f42ec2 --- /dev/null +++ b/bench/bench.sh @@ -0,0 +1,24 @@ +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 new file mode 100644 index 0000000..73c7a3e --- /dev/null +++ b/bench/heavy-load.yml @@ -0,0 +1,74 @@ +--- +# 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 new file mode 100644 index 0000000..a328d88 --- /dev/null +++ b/bench/nginx.yml @@ -0,0 +1,130 @@ +# 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 new file mode 100644 index 0000000..0f074b9 --- /dev/null +++ b/bench/small-heavy-load-virtual.yml @@ -0,0 +1,86 @@ +--- +# 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 new file mode 100644 index 0000000..931a456 --- /dev/null +++ b/bench/small-heavy-load.yml @@ -0,0 +1,74 @@ +--- +# 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 new file mode 100644 index 0000000..ff070e0 --- /dev/null +++ b/build.rs @@ -0,0 +1,39 @@ +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 new file mode 100644 index 0000000..cbf7d67 --- /dev/null +++ b/config/README.md @@ -0,0 +1,8 @@ +# 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 deleted file mode 100644 index 33e4083..0000000 --- a/config/activitywatch.jsonnet +++ /dev/null @@ -1,101 +0,0 @@ -// 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 6d84230..c9e9abb 100644 --- a/config/example.jsonnet +++ b/config/example.jsonnet @@ -7,61 +7,106 @@ // strongly encouraged to take a look at the full documentation: https://reaction.ppom.me // JSONnet functions -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 +local ipBan(cmd) = [cmd, '-w', '-A', 'reaction', '-s', '', '-j', 'DROP']; +local ipUnban(cmd) = [cmd, '-w', '-D', 'reaction', '-s', '', '-j', 'DROP']; -// See meaning and usage of this function around L106 +// See meaning and usage of this function around L180 local banFor(time) = { - ban: { - cmd: iptables(['-A', 'reaction', '-s', '', '-j', 'DROP']), + ban4: { + cmd: ipBan('iptables'), + ipv4only: true, }, - unban: { + ban6: { + cmd: ipBan('ip6tables'), + ipv6only: true, + }, + unban4: { + cmd: ipUnban('iptables'), after: time, - cmd: iptables(['-D', 'reaction', '-s', '', '-j', 'DROP']), + ipv4only: true, + }, + unban6: { + cmd: ipUnban('ip6tables'), + after: time, + ipv6only: true, }, }; +// 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: { - 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'], - // 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}'], + + 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: ... + // 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 - iptables(['-N', 'reaction']), + ip46tables(['-N', 'reaction']) + // Insert this chain as the first item of the INPUT & FORWARD chains (for incoming connections) - iptables(['-I', 'INPUT', '-p', 'all', '-j', 'reaction']), - iptables(['-I', 'FORWARD', '-p', 'all', '-j', 'reaction']), - ], + ip46tables(['-I', 'INPUT', '-p', 'all', '-j', 'reaction']) + + ip46tables(['-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 - iptables(['-D', 'INPUT', '-p', 'all', '-j', 'reaction']), - iptables(['-D', 'FORWARD', '-p', 'all', '-j', 'reaction']), + ip46tables(['-D', 'INPUT', '-p', 'all', '-j', 'reaction']) + + ip46tables(['-D', 'FORWARD', '-p', 'all', '-j', 'reaction']) + // Empty the chain - iptables(['-F', 'reaction']), + ip46tables(['-F', 'reaction']) + // Delete the chain - iptables(['-X', 'reaction']), - ], + ip46tables(['-X', 'reaction']), + // streams are commands // they are run and their ouptut is captured @@ -73,41 +118,85 @@ local banFor(time) = { // 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 + // 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 here: https://pkg.go.dev/time#ParseDuration + // 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 - ban: { - cmd: iptables(['-A', 'reaction', '-s', '', '-j', 'DROP']), + 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, }, - unban: { - cmd: iptables(['-D', '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'], // if after is defined, the action will not take place immediately, but after a specified duration // same format as retryperiod - after: '48h', + 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: ['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 120000 index 1efef98..0000000 --- a/config/example.yml +++ /dev/null @@ -1 +0,0 @@ -../app/example.yml \ No newline at end of file diff --git a/config/example.yml b/config/example.yml new file mode 100644 index 0000000..40f2764 --- /dev/null +++ b/config/example.yml @@ -0,0 +1,184 @@ +--- +# 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/heavy-load.yml b/config/heavy-load.yml deleted file mode 100644 index 1c149c3..0000000 --- a/config/heavy-load.yml +++ /dev/null @@ -1,72 +0,0 @@ ---- -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 deleted file mode 100644 index f3f58c1..0000000 --- a/config/persistence.jsonnet +++ /dev/null @@ -1,50 +0,0 @@ -{ - 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.example.service b/config/reaction.service similarity index 85% rename from config/reaction.example.service rename to config/reaction.service index 22eed48..897e869 100644 --- a/config/reaction.example.service +++ b/config/reaction.service @@ -2,15 +2,12 @@ [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/bin/reaction start -c /etc/reaction.jsonnet +ExecStart=/usr/local/bin/reaction start -c /etc/reaction/ # Ask systemd to create /var/lib/reaction (/var/lib/ is implicit) StateDirectory=reaction @@ -18,3 +15,8 @@ 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 deleted file mode 100644 index 2886c28..0000000 --- a/config/server.jsonnet +++ /dev/null @@ -1,160 +0,0 @@ -// 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 deleted file mode 100644 index 9210a80..0000000 --- a/config/test.jsonnet +++ /dev/null @@ -1,63 +0,0 @@ -{ - 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 new file mode 100644 index 0000000..9f2c4d3 --- /dev/null +++ b/crates/treedb/Cargo.toml @@ -0,0 +1,23 @@ +[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 new file mode 100644 index 0000000..9e4c3b0 --- /dev/null +++ b/crates/treedb/src/helpers.rs @@ -0,0 +1,188 @@ +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